Skip to content

Commit 7cd94a1

Browse files
committed
feat!: TypeVar resolution and Python3.9 EOL
- Add support for resolving subscripted generics and typevars. - Drop Python3.9 as a supported version. - Official support for Python3.13
1 parent cac901c commit 7cd94a1

File tree

16 files changed

+147
-233
lines changed

16 files changed

+147
-233
lines changed

.github/workflows/validate.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: Validate
22
on:
33
push:
44
branches:
5-
- '**'
5+
- "**"
66
tags-ignore:
7-
- '**'
7+
- "**"
88

99
jobs:
1010
ci:
@@ -15,7 +15,7 @@ jobs:
1515
fail-fast: false
1616
matrix:
1717
os: [ubuntu-latest, macos-latest, windows-latest]
18-
python-version: ["3.9", "3.10", "3.11", "3.12"]
18+
python-version: ["3.10", "3.11", "3.12", "3.13"]
1919
with:
2020
runner: ${{ matrix.os }}
2121
python-version: ${{ matrix.python-version }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,4 @@ venv.bak/
114114
*.DS_Store
115115
coverage.db
116116
release-notes.md
117+
.zed/

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ test: ## Run this app's tests with a test db. Target a specific path `target=pat
4747
$(RUN_PREFIX) pytest $(target) $(TEST_ARGS)
4848
.PHONY: test
4949

50-
TEST_ARGS ?= --cov --cov-config=.coveragerc --cov-report=xml --cov-report=term --junit-xml=junit.xml
50+
TEST_ARGS ?=
5151

5252
rule ?= patch
5353
ref ?= main

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ dynamic = ["version"]
88
description = "A toolkit for marshalling, unmarshalling, and runtime validation leveraging type annotations."
99
authors = [{ name = "Sean Stewart", email = "sean.stewart@hey.com" }]
1010
readme = "README.md"
11-
requires-python = "~=3.9"
11+
requires-python = ">=3.10,<4.0"
1212
keywords = ["typing", "data", "annotations", "validation", "serdes"]
1313
classifiers = [
1414
"Development Status :: 3 - Alpha",
@@ -17,10 +17,10 @@ classifiers = [
1717
"License :: OSI Approved :: MIT License",
1818
"Operating System :: OS Independent",
1919
"Programming Language :: Python",
20-
"Programming Language :: Python :: 3.9",
2120
"Programming Language :: Python :: 3.10",
2221
"Programming Language :: Python :: 3.11",
2322
"Programming Language :: Python :: 3.12",
23+
"Programming Language :: Python :: 3.13",
2424
"Programming Language :: Python :: 3 :: Only",
2525
"Programming Language :: Python :: Implementation :: CPython",
2626
"Topic :: Utilities",

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
addopts = -vv --cov --cov-config=.coveragerc --cov-report=xml --cov-report=term --junit-xml=junit.xml

src/typelib/marshals/routines.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,9 @@ def __call__(self, val: compat.TupleT) -> MarshalledIterableT:
434434
"""
435435
return [
436436
routine(v)
437-
for routine, v in zip(self.ordered_routines, serdes.itervalues(val))
437+
for routine, v in zip(
438+
self.ordered_routines, serdes.itervalues(val), strict=False
439+
)
438440
]
439441

440442

src/typelib/py/inspection.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,15 +319,23 @@ def get_type_hints(
319319
Whether to pull type hints from the signature of the object if
320320
none can be found via [`typing.get_type_hints`][]. (defaults True)
321321
"""
322+
tvar_to_type = {}
322323
try:
323-
hints = tp.get_type_hints(obj)
324+
t = obj
325+
if issubscriptedgeneric(t) and not isforwardref(t):
326+
t = obj.__origin__ # type: ignore[attr-defined,union-attr]
327+
tvar_to_type = dict(zip(obj.__parameters__, obj.__args__, strict=False)) # type: ignore[attr-defined,union-attr]
328+
hints = tp.get_type_hints(t)
324329
except (NameError, TypeError):
325330
hints = {}
326331
# KW_ONLY is a special sentinel to denote kw-only params in a dataclass.
327332
# We don't want to do anything with this hint/field. It's not real.
328333
hints = {f: t for f, t in hints.items() if t is not compat.KW_ONLY}
329334
if not hints and exhaustive:
330335
hints = _hints_from_signature(obj)
336+
337+
for name, annotation in hints.items():
338+
hints[name] = tvar_to_type.get(annotation, annotation)
331339
return hints
332340

333341

@@ -1489,6 +1497,22 @@ def istypealiastype(t: tp.Any) -> compat.TypeIs[compat.TypeAliasType]:
14891497
return isinstance(t, compat.TypeAliasType)
14901498

14911499

1500+
@compat.cache
1501+
def istypevartype(t: tp.Any) -> compat.TypeIs[compat.TypeVar]:
1502+
"""Detect if the given object is a [`typing.TypeAliasType`][].
1503+
1504+
Examples:
1505+
>>> type IntList = list[int]
1506+
>>> istypealiastype(IntList)
1507+
True
1508+
>>> IntList = compat.TypeAliasType("IntList", list[int])
1509+
>>> istypealiastype(IntList)
1510+
True
1511+
1512+
"""
1513+
return isinstance(t, tp.TypeVar)
1514+
1515+
14921516
@compat.cache
14931517
def unwrap(t: tp.Any) -> tp.Any:
14941518
lt = None
@@ -1505,6 +1529,16 @@ def unwrap(t: tp.Any) -> tp.Any:
15051529
t = tv
15061530
continue
15071531

1532+
if istypevartype(t):
1533+
lt = t
1534+
if t.__bound__:
1535+
t = t.__bound__
1536+
elif t.__constraints__:
1537+
t = tp.Union[t.__constraints__]
1538+
else:
1539+
t = tp.Any
1540+
continue
1541+
15081542
if hasattr(t, "__supertype__"):
15091543
lt = t
15101544
t = t.__supertype__

src/typelib/serdes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ def get_items_iter(tp: type) -> t.Callable[[t.Any], t.Iterable[tuple[t.Any, t.An
378378

379379

380380
def _namedtupleitems(val: t.NamedTuple) -> t.Iterable[tuple[str, t.Any]]:
381-
return zip(val._fields, val)
381+
return zip(val._fields, val, strict=False)
382382

383383

384384
def _make_fields_iterator(

src/typelib/unmarshals/routines.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -933,7 +933,9 @@ def __call__(self, val: tp.Any) -> compat.TupleT:
933933
decoded = serdes.load(val)
934934
return self.origin(
935935
routine(v)
936-
for routine, v in zip(self.ordered_routines, serdes.itervalues(decoded))
936+
for routine, v in zip(
937+
self.ordered_routines, serdes.itervalues(decoded), strict=False
938+
)
937939
)
938940

939941

tests/models.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ class TDict(typing.TypedDict):
5555
value: int
5656

5757

58+
IntVar = typing.TypeVar("IntVar", bound=int)
59+
StrVar = typing.TypeVar("StrVar", bound=str)
60+
61+
62+
@dataclasses.dataclass
63+
class DataGeneric(typing.Generic[StrVar, IntVar]):
64+
field: StrVar
65+
value: IntVar
66+
67+
5868
class GivenEnum(enum.Enum):
5969
one = "one"
6070

@@ -104,3 +114,16 @@ class NestedTypeAliasType:
104114
Record = compat.TypeAliasType(
105115
"Record", "dict[str, list[Record] | list[ScalarValue] | Record | ScalarValue]"
106116
)
117+
118+
119+
TVar = typing.TypeVar("TVar", bound=int)
120+
121+
122+
@dataclasses.dataclass
123+
class SimpleGeneric(typing.Generic[TVar]):
124+
field: TVar
125+
126+
127+
@dataclasses.dataclass
128+
class NestedGeneric(typing.Generic[TVar]):
129+
gen: SimpleGeneric[TVar]

0 commit comments

Comments
 (0)