Skip to content

Commit 6d668e9

Browse files
Merge branch 'master' into python3.14
2 parents 356e707 + 2a77257 commit 6d668e9

File tree

10 files changed

+133
-47
lines changed

10 files changed

+133
-47
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
<!--next-version-placeholder-->
44

5+
## v3.17.1 (2025-09-20)
6+
7+
### Fix
8+
9+
* Smarter adapt so Fields and Tables can also be safely inserted in `sql_expression` ([`d5cad6a`](https://github.com/trialandsuccess/TypeDAL/commit/d5cad6a13ddf50ec6e5762075fbeae1e44067da6))
10+
11+
## v3.17.0 (2025-09-20)
12+
13+
### Feature
14+
15+
* Improved Expression support via `db.sql_expression` for more flexibility ([`2892aa4`](https://github.com/trialandsuccess/TypeDAL/commit/2892aa452a0b9563ff742a2a7a00927b89b440b9))
16+
517
## v3.16.5 (2025-09-08)
618

719
### Fix

src/typedal/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
# SPDX-FileCopyrightText: 2023-present Robin van der Noord <robinvandernoord@gmail.com>
66
#
77
# SPDX-License-Identifier: MIT
8-
__version__ = "3.16.5"
8+
__version__ = "3.17.1"

src/typedal/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
"""
44

55
from . import fields
6-
from .core import Relationship, TypeDAL, TypedField, TypedRows, TypedTable, relationship, sql_expression
6+
from .core import (
7+
Relationship,
8+
TypeDAL,
9+
TypedField,
10+
TypedRows,
11+
TypedTable,
12+
relationship,
13+
)
14+
from .helpers import sql_expression
715

816
try:
917
from .for_py4web import DAL as P4W_DAL

src/typedal/cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,8 @@ def fake_migrations(
392392

393393
previously_migrated = (
394394
db(
395-
db.ewh_implemented_features.name.belongs(to_fake) & (db.ewh_implemented_features.installed == True) # noqa E712
395+
db.ewh_implemented_features.name.belongs(to_fake)
396+
& (db.ewh_implemented_features.installed == True) # noqa E712
396397
)
397398
.select(db.ewh_implemented_features.name)
398399
.column("name")

src/typedal/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ def load_config(
338338
# combine and fill with fallback values
339339
# load typedal config or fail
340340
toml_path, toml = _load_toml(_use_pyproject)
341-
dotenv_path, dotenv = _load_dotenv(_use_env)
341+
_dotenv_path, dotenv = _load_dotenv(_use_env)
342342

343343
expand_env_vars_into_toml_values(toml, dotenv)
344344

src/typedal/core.py

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,9 @@
2525

2626
import pydal
2727
from pydal._globals import DEFAULT
28-
29-
# from pydal.objects import Field as _Field
30-
# from pydal.objects import Query as _Query
3128
from pydal.objects import Row
32-
33-
# from pydal.objects import Table as _Table
3429
from typing_extensions import Self, Unpack
3530

36-
# from .annotations import resolve_annotation
3731
from .config import TypeDALConfig, load_config
3832
from .helpers import (
3933
SYSTEM_SUPPORTS_TEMPLATES,
@@ -249,7 +243,7 @@ class Relationship(typing.Generic[To_Type]):
249243
Define a relationship to another table.
250244
"""
251245

252-
_type: To_Type
246+
_type: Type[To_Type]
253247
table: Type["TypedTable"] | type | str
254248
condition: Condition
255249
condition_and: Condition
@@ -259,7 +253,7 @@ class Relationship(typing.Generic[To_Type]):
259253

260254
def __init__(
261255
self,
262-
_type: To_Type,
256+
_type: Type[To_Type],
263257
condition: Condition = None,
264258
join: JOIN_OPTIONS = None,
265259
on: OnQuery = None,
@@ -282,7 +276,7 @@ def __init__(
282276
self.table = unwrap_type(args[0])
283277
self.multiple = True
284278
else:
285-
self.table = _type
279+
self.table = typing.cast(type[TypedTable], _type)
286280
self.multiple = False
287281

288282
if isinstance(self.table, str):
@@ -977,7 +971,7 @@ def sql_expression(
977971
*raw_args: Any,
978972
output_type: str | None = None,
979973
**raw_kwargs: Any,
980-
):
974+
) -> Expression:
981975
"""
982976
Creates a pydal Expression object representing a raw SQL fragment.
983977
@@ -999,7 +993,7 @@ def default_representer(field: TypedField[T], value: T, table: Type[TypedTable])
999993
Simply call field.represent on the value.
1000994
"""
1001995
if represent := getattr(field, "represent", None):
1002-
return represent(value, table)
996+
return str(represent(value, table))
1003997
else:
1004998
return repr(value)
1005999

@@ -1012,7 +1006,7 @@ def default_representer(field: TypedField[T], value: T, table: Type[TypedTable])
10121006

10131007
def reorder_fields(
10141008
table: pydal.objects.Table,
1015-
fields: typing.Iterable[str | Field | TypedField],
1009+
fields: typing.Iterable[str | Field | TypedField[Any]],
10161010
keep_others: bool = True,
10171011
) -> None:
10181012
"""
@@ -1588,7 +1582,7 @@ def after_delete_once(
15881582
"""
15891583
return cls._hook_once(cls._after_delete, fn)
15901584

1591-
def reorder_fields(cls, *fields: str | Field | TypedField, keep_others: bool = True):
1585+
def reorder_fields(cls, *fields: str | Field | TypedField[Any], keep_others: bool = True) -> None:
15921586
"""
15931587
Reorder fields of a typedal table.
15941588
@@ -1598,7 +1592,6 @@ def reorder_fields(cls, *fields: str | Field | TypedField, keep_others: bool = T
15981592
- True (default): keep other fields at the end, in their original order.
15991593
- False: remove other fields (only keep what's specified).
16001594
"""
1601-
16021595
return reorder_fields(cls._table, fields, keep_others=keep_others)
16031596

16041597

@@ -1935,13 +1928,13 @@ def __getattr__(self, item: str) -> Any:
19351928

19361929
raise AttributeError(item)
19371930

1938-
def keys(self):
1931+
def keys(self) -> list[str]:
19391932
"""
19401933
Return the combination of row + relationship keys.
19411934
19421935
Used by dict(row).
19431936
"""
1944-
return list(self._row.keys()) + getattr(self, "_with", [])
1937+
return list(self._row.keys() if self._row else ()) + getattr(self, "_with", [])
19451938

19461939
def get(self, item: str, default: Any = None) -> Any:
19471940
"""
@@ -2202,7 +2195,17 @@ def _sql(cls) -> str:
22022195

22032196
return pydal2sql.generate_sql(cls)
22042197

2205-
def render(self, fields=None, compact=False) -> Self:
2198+
def render(self, fields: list[Field] = None, compact: bool = False) -> Self:
2199+
"""
2200+
Renders a copy of the object with potentially modified values.
2201+
2202+
Args:
2203+
fields: A list of fields to render. Defaults to all representable fields in the table.
2204+
compact: Whether to return only the value of the first field if there is only one field.
2205+
2206+
Returns:
2207+
A copy of the object with potentially modified values.
2208+
"""
22062209
row = copy.deepcopy(self)
22072210
keys = list(row)
22082211
if not fields:
@@ -2256,7 +2259,7 @@ def render(self, fields=None, compact=False) -> Self:
22562259
)
22572260

22582261
if compact and len(keys) == 1 and keys[0] != "_extra": # pragma: no cover
2259-
return row[keys[0]]
2262+
return typing.cast(Self, row[keys[0]])
22602263
return row
22612264

22622265

@@ -2455,11 +2458,11 @@ def as_csv(self) -> str:
24552458

24562459
def as_dict(
24572460
self,
2458-
key: str | Field = None,
2461+
key: str | Field | None = None,
24592462
compact: bool = False,
24602463
storage_to_dict: bool = False,
24612464
datetime_to_str: bool = False,
2462-
custom_types: list[type] = None,
2465+
custom_types: list[type] | None = None,
24632466
) -> dict[int, AnyDict]:
24642467
"""
24652468
Get the data in a dict of dicts.
@@ -2633,10 +2636,12 @@ def __setstate__(self, state: AnyDict) -> None:
26332636
self.__dict__.update(state)
26342637
# db etc. set after undill by caching.py
26352638

2636-
def render(self, i=None, fields=None) -> typing.Generator[T_MetaInstance, None, None]:
2639+
def render(
2640+
self, i: int | None = None, fields: list[Field] | None = None
2641+
) -> typing.Generator[T_MetaInstance, None, None]:
26372642
"""
2638-
Takes an index and returns a copy of the indexed row with values
2639-
transformed via the "represent" attributes of the associated fields.
2643+
Takes an index and returns a copy of the indexed row with values \
2644+
transformed via the "represent" attributes of the associated fields.
26402645
26412646
Args:
26422647
i: index. If not specified, a generator is returned for iteration
@@ -2646,7 +2651,7 @@ def render(self, i=None, fields=None) -> typing.Generator[T_MetaInstance, None,
26462651
"""
26472652
if i is None:
26482653
# difference: uses .keys() instead of index
2649-
return (self.render(i, fields=fields) for i in self.records.keys())
2654+
return (self.render(i, fields=fields) for i in self.records)
26502655

26512656
if not self.db.has_representer("rows_render"): # pragma: no cover
26522657
raise RuntimeError(
@@ -2668,10 +2673,10 @@ def render(self, i=None, fields=None) -> typing.Generator[T_MetaInstance, None,
26682673
)
26692674

26702675

2671-
def normalize_table_keys(row: Row, pattern: re.Pattern = re.compile(r"^([a-zA-Z_]+)_(\d{5,})$")) -> Row:
2676+
def normalize_table_keys(row: Row, pattern: re.Pattern[str] = re.compile(r"^([a-zA-Z_]+)_(\d{5,})$")) -> Row:
26722677
"""
2673-
Normalize table keys in a PyDAL Row object by stripping numeric hash suffixes
2674-
from table names, only if the suffix is 5 or more digits.
2678+
Normalize table keys in a PyDAL Row object by stripping numeric hash suffixes from table names, \
2679+
only if the suffix is 5 or more digits.
26752680
26762681
For example:
26772682
Row({'articles_12345': {...}}) -> Row({'articles': {...}})
@@ -2810,7 +2815,7 @@ def select(self, *fields: Any, **options: Unpack[SelectKwargs]) -> "QueryBuilder
28102815

28112816
def where(
28122817
self,
2813-
*queries_or_lambdas: Query | typing.Callable[[Type[T_MetaInstance]], Query] | dict,
2818+
*queries_or_lambdas: Query | typing.Callable[[Type[T_MetaInstance]], Query] | dict[str, Any],
28142819
**filters: Any,
28152820
) -> "QueryBuilder[T_MetaInstance]":
28162821
"""
@@ -2892,8 +2897,9 @@ def join(
28922897
if isinstance(condition, pydal.objects.Query):
28932898
condition = as_lambda(condition)
28942899

2900+
to_field = typing.cast(Type[TypedTable], fields[0])
28952901
relationships = {
2896-
str(fields[0]): Relationship(fields[0], condition=condition, join=method, condition_and=condition_and)
2902+
str(to_field): Relationship(to_field, condition=condition, join=method, condition_and=condition_and)
28972903
}
28982904
elif on:
28992905
if len(fields) != 1:
@@ -2904,7 +2910,9 @@ def join(
29042910

29052911
if isinstance(on, list):
29062912
on = as_lambda(on)
2907-
relationships = {str(fields[0]): Relationship(fields[0], on=on, join=method, condition_and=condition_and)}
2913+
2914+
to_field = typing.cast(Type[TypedTable], fields[0])
2915+
relationships = {str(to_field): Relationship(to_field, on=on, join=method, condition_and=condition_and)}
29082916

29092917
else:
29102918
if fields:

src/typedal/fields.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,18 +256,50 @@ def TimestampField(**kw: Unpack[FieldSettings]) -> TypedField[dt.datetime]:
256256
)
257257

258258

259-
def safe_decode_native_point(value: str | None):
259+
def safe_decode_native_point(value: str | None) -> tuple[float, ...]:
260+
"""
261+
Safely decode a string into a tuple of floats.
262+
263+
The function attempts to parse the input string using `ast.literal_eval`.
264+
If the parsing is successful, the function casts the parsed value to a tuple of floats and returns it.
265+
Otherwise, the function returns an empty tuple.
266+
267+
Args:
268+
value: The string to decode.
269+
270+
Returns:
271+
A tuple of floats.
272+
"""
260273
if not value:
261274
return ()
262275

263276
try:
264-
return ast.literal_eval(value)
277+
parsed = ast.literal_eval(value)
278+
return typing.cast(tuple[float, ...], parsed)
265279
except ValueError: # pragma: no cover
266280
# should not happen when inserted with `safe_encode_native_point` but you never know
267281
return ()
268282

269283

270-
def safe_encode_native_point(value: tuple[str, str] | str) -> str:
284+
def safe_encode_native_point(value: tuple[str, str] | tuple[float, float] | str) -> str:
285+
"""
286+
287+
Safe encodes a point value.
288+
289+
The function takes a point value as input.
290+
It can be a string in the format "x,y" or a tuple of two numbers.
291+
The function converts the string to a tuple if necessary, validates the tuple,
292+
and formats it into the expected string format.
293+
294+
Args:
295+
value: The point value to be encoded.
296+
297+
Returns:
298+
The encoded point value as a string in the format "x,y".
299+
300+
Raises:
301+
ValueError: If the input value is not a valid point.
302+
"""
271303
if not value:
272304
return ""
273305

@@ -276,13 +308,15 @@ def safe_encode_native_point(value: tuple[str, str] | str) -> str:
276308
value = value.strip("() ")
277309
if not value:
278310
return ""
279-
value = tuple(float(x.strip()) for x in value.split(","))
311+
value_tup = tuple(float(x.strip()) for x in value.split(","))
312+
else:
313+
value_tup = value # type: ignore
280314

281315
# Validate and format
282-
if len(value) != 2:
316+
if len(value_tup) != 2:
283317
raise ValueError("Point must have exactly 2 coordinates")
284318

285-
x, y = value
319+
x, y = value_tup
286320
return f"({x},{y})"
287321

288322

0 commit comments

Comments
 (0)