From 0136eefc70f2966899afcb45f81511cc33b1a290 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 4 Feb 2026 17:14:43 -0800 Subject: [PATCH 1/2] Make `typemap_extensions` the canonical import for users The point here is to make sure that *clients* import the `typemap.typing` stuff under a different name that `type_eval` internals do, so that `typemap.type_eval` internals can still be typechecked against the real `typemap.typing` while client code will import `typemap_extensions` and will be able to see the proper mypy/typeshed magic for it. If we want to keep things under `typemap`, we could leave the public version as `typemap.typing` and make the internal module `typemap._typing_internals`. Thoughts? Also do some fixups so that qblike_2 and fastapilike_2 basically pass typechecking. --- README.md | 9 +++++++++ pep.rst | 5 +++-- pyproject.toml | 5 ++++- tests/format_helper.py | 4 ++-- tests/test_astlike_1.py | 5 +++-- tests/test_call.py | 2 +- tests/test_eval_call_with_types.py | 2 +- tests/test_fastapilike_1.py | 2 +- tests/test_fastapilike_2.py | 9 +++++++-- tests/test_nplike.py | 2 +- tests/test_qblike.py | 2 +- tests/test_qblike_2.py | 20 ++++++++++++++++---- tests/test_qblike_3.py | 2 +- tests/test_schemalike.py | 2 +- tests/test_type_dir.py | 2 +- tests/test_type_eval.py | 7 ++++--- typemap/type_eval/_eval_operators.py | 2 +- typemap_extensions/__init__.py | 8 ++++++++ typemap_extensions/__init__.pyi | 1 + 19 files changed, 66 insertions(+), 25 deletions(-) create mode 100644 typemap_extensions/__init__.py create mode 100644 typemap_extensions/__init__.pyi diff --git a/README.md b/README.md index cdde2c0..c585ca7 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,12 @@ See [pep.rst](pep.rst) for the PEP draft and [design-qs.rst](design-qs.rst) for 2. `$ cd typemap` 3. `$ uv sync` 4. `$ uv run pytest` + +## Running the typechecker + +If you have https://github.com/msullivan/mypy/tree/typemap active in a +venv, you can run it against at least some of the tests with +invocations like: + `mypy --python-version=3.14 tests/test_qblike_2.py` + +Not all of them run cleanly yet though. diff --git a/pep.rst b/pep.rst index 857d4c9..2db410c 100644 --- a/pep.rst +++ b/pep.rst @@ -1005,7 +1005,8 @@ as a literal type--all of these mechanisms lean very heavily on literal types. for c in typing.Iter[typing.Attrs[K]] ] ] - ]: ... + ]: + raise NotImplementedError ConvertField is our first type helper, and it is a conditional type alias, which decides between two types based on a (limited) @@ -1035,7 +1036,7 @@ grabs the argument to a ``Pointer``). :: - type PointerArg[T: Pointer] = typing.GetArg[T, Pointer, Literal[0]] + type PointerArg[T] = typing.GetArg[T, Pointer, Literal[0]] ``AdjustLink`` sticks a ``list`` around ``MultiLink``, using features we've discussed already. diff --git a/pyproject.toml b/pyproject.toml index 2c90019..088f942 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,9 @@ readme = "README.md" requires-python = ">=3.14" dependencies = [] +[tool.setuptools.packages.find] +include = ["typemap", "typemap_extensions"] + [dependency-groups] test = [ "pytest>=7.0", @@ -68,7 +71,7 @@ extend-ignore = [ [tool.ruff] line-length = 80 indent-width = 4 -include = ["pyproject.toml", "typemap/**/*.py", "tests/**/*.py"] +include = ["pyproject.toml", "typemap/**/*.py", "typemap_extensions/**/*.py", "tests/**/*.py"] [tool.ruff.format] quote-style = "preserve" diff --git a/tests/format_helper.py b/tests/format_helper.py index 2eb9073..0b9fb5e 100644 --- a/tests/format_helper.py +++ b/tests/format_helper.py @@ -25,10 +25,10 @@ def format_meth(name, meth): code += f" {attr_name}: {attr_type_s}{eq}\n" for name, attr in cls.__dict__.items(): - if attr is typing._no_init_or_replace_init: + if attr is typing._no_init_or_replace_init: # type: ignore[attr-defined] continue if isinstance(attr, classmethod): - attr = inspect.unwrap(attr) + attr = inspect.unwrap(attr) # type: ignore[arg-type] code += f" @classmethod\n" elif isinstance(attr, staticmethod): attr = inspect.unwrap(attr) diff --git a/tests/test_astlike_1.py b/tests/test_astlike_1.py index 6c90194..f4e2728 100644 --- a/tests/test_astlike_1.py +++ b/tests/test_astlike_1.py @@ -2,7 +2,9 @@ import typing from typemap.type_eval import eval_call_with_types, eval_typing, TypeMapError -from typemap.typing import ( +from typemap.typing import _BoolLiteral + +from typemap_extensions import ( Attrs, BaseTypedDict, Bool, @@ -15,7 +17,6 @@ Member, NewProtocol, RaiseError, - _BoolLiteral, ) diff --git a/tests/test_call.py b/tests/test_call.py index e983aa7..3bbccc8 100644 --- a/tests/test_call.py +++ b/tests/test_call.py @@ -3,7 +3,7 @@ from typing import Unpack from typemap.type_eval import eval_call -from typemap.typing import ( +from typemap_extensions import ( Attrs, BaseTypedDict, NewProtocol, diff --git a/tests/test_eval_call_with_types.py b/tests/test_eval_call_with_types.py index 22b1c61..c2ed54a 100644 --- a/tests/test_eval_call_with_types.py +++ b/tests/test_eval_call_with_types.py @@ -3,7 +3,7 @@ from typing import Callable, Generic, Literal, Self, TypeVar from typemap.type_eval import eval_call_with_types -from typemap.typing import ( +from typemap_extensions import ( GenericCallable, GetArg, GetName, diff --git a/tests/test_fastapilike_1.py b/tests/test_fastapilike_1.py index 894cfe0..ea76af7 100644 --- a/tests/test_fastapilike_1.py +++ b/tests/test_fastapilike_1.py @@ -5,7 +5,7 @@ from typing import Annotated, Callable, Literal, Union, Self from typemap.type_eval import eval_typing -from typemap.typing import ( +from typemap_extensions import ( NewProtocol, Iter, Attrs, diff --git a/tests/test_fastapilike_2.py b/tests/test_fastapilike_2.py index fc17638..088d3f8 100644 --- a/tests/test_fastapilike_2.py +++ b/tests/test_fastapilike_2.py @@ -6,9 +6,10 @@ TypedDict, Never, Self, + TYPE_CHECKING, ) -from typemap import typing +import typemap_extensions as typing class FieldArgs(TypedDict, total=False): @@ -25,7 +26,7 @@ class Field[T: FieldArgs](typing.InitField[T]): #### # TODO: Should this go into the stdlib? -type GetFieldItem[T: typing.InitField, K] = typing.GetMemberType[ +type GetFieldItem[T, K] = typing.GetMemberType[ typing.GetArg[T, typing.InitField, Literal[0]], K ] @@ -207,6 +208,10 @@ class Hero: secret_name: str = Field(hidden=True) +if TYPE_CHECKING: + pubhero: Public[Hero] + reveal_type(pubhero) # noqa + ####### import textwrap diff --git a/tests/test_nplike.py b/tests/test_nplike.py index 9d16f78..8014355 100644 --- a/tests/test_nplike.py +++ b/tests/test_nplike.py @@ -1,6 +1,6 @@ from typing import Literal -from typemap import typing +import typemap_extensions as typing import pytest diff --git a/tests/test_qblike.py b/tests/test_qblike.py index dfe0f25..39200ca 100644 --- a/tests/test_qblike.py +++ b/tests/test_qblike.py @@ -7,7 +7,7 @@ eval_call_with_types, eval_typing, ) -from typemap.typing import ( +from typemap_extensions import ( BaseTypedDict, NewProtocol, Iter, diff --git a/tests/test_qblike_2.py b/tests/test_qblike_2.py index f59848e..a27ffbe 100644 --- a/tests/test_qblike_2.py +++ b/tests/test_qblike_2.py @@ -1,9 +1,9 @@ import textwrap -from typing import Literal, Unpack +from typing import Literal, Unpack, TYPE_CHECKING from typemap.type_eval import eval_call, eval_typing -from typemap import typing +import typemap_extensions as typing from . import format_helper @@ -66,7 +66,8 @@ def select[ModelT, K: typing.BaseTypedDict]( for c in typing.Iter[typing.Attrs[K]] ] ] -]: ... +]: + raise NotImplementedError """ConvertField is our first type helper, and it is a conditional type @@ -95,7 +96,7 @@ def select[ModelT, K: typing.BaseTypedDict]( grabs the argument to a ``Pointer``). """ -type PointerArg[T: Pointer] = typing.GetArg[T, Pointer, Literal[0]] +type PointerArg[T] = typing.GetArg[T, Pointer, Literal[0]] """ ``AdjustLink`` sticks a ``list`` around ``MultiLink``, using features @@ -151,6 +152,17 @@ class User: posts: MultiLink[Post] +def test_qblike_typing_only_1() -> None: + if TYPE_CHECKING: + _test_select = select( + Post, + title=True, + comments=True, + author=True, + ) + reveal_type(_test_select) # noqa + + def test_qblike2_1(): ret = eval_call( select, diff --git a/tests/test_qblike_3.py b/tests/test_qblike_3.py index 77b6221..1cd323f 100644 --- a/tests/test_qblike_3.py +++ b/tests/test_qblike_3.py @@ -13,7 +13,7 @@ ) from typemap.type_eval import eval_call_with_types, eval_typing -from typemap.typing import ( +from typemap_extensions import ( Attrs, Bool, Length, diff --git a/tests/test_schemalike.py b/tests/test_schemalike.py index 0ddf396..5a80a47 100644 --- a/tests/test_schemalike.py +++ b/tests/test_schemalike.py @@ -3,7 +3,7 @@ from typing import Callable, Literal from typemap.type_eval import eval_typing -from typemap.typing import ( +from typemap_extensions import ( NewProtocol, Iter, Attrs, diff --git a/tests/test_type_dir.py b/tests/test_type_dir.py index 0ef7ff9..3f7c29b 100644 --- a/tests/test_type_dir.py +++ b/tests/test_type_dir.py @@ -3,7 +3,7 @@ from typing import Literal, Never, TypeVar, TypedDict, Union, ReadOnly from typemap.type_eval import eval_typing -from typemap.typing import ( +from typemap_extensions import ( Attrs, FromUnion, GetArg, diff --git a/tests/test_type_eval.py b/tests/test_type_eval.py index 5d954a8..4cf7072 100644 --- a/tests/test_type_eval.py +++ b/tests/test_type_eval.py @@ -18,7 +18,9 @@ import pytest from typemap.type_eval import eval_typing -from typemap.typing import ( +from typemap.typing import _BoolLiteral + +from typemap_extensions import ( Attrs, Bool, FromUnion, @@ -44,7 +46,6 @@ SpecialFormEllipsis, StrConcat, Uppercase, - _BoolLiteral, ) from . import format_helper @@ -1675,7 +1676,7 @@ def test_type_eval_annotated_04(): ############## # RaiseError tests -from typemap.typing import RaiseError +from typemap_extensions import RaiseError from typemap.type_eval import TypeMapError diff --git a/typemap/type_eval/_eval_operators.py b/typemap/type_eval/_eval_operators.py index 0a89de6..1b50c81 100644 --- a/typemap/type_eval/_eval_operators.py +++ b/typemap/type_eval/_eval_operators.py @@ -577,7 +577,7 @@ def _ann(x): # TODO: Is doing the tuple for staticmethod/classmethod legit? # Putting a list in makes it unhashable... - f: typing.Any + f: typing.Any # type: ignore[annotation-unchecked] if isinstance(func, staticmethod): f = staticmethod[tuple[*params], ret] elif isinstance(func, classmethod): diff --git a/typemap_extensions/__init__.py b/typemap_extensions/__init__.py new file mode 100644 index 0000000..4acc1ee --- /dev/null +++ b/typemap_extensions/__init__.py @@ -0,0 +1,8 @@ +# mypy: follow-imports=skip + +# The canonical place to use typemap stuff from right now is +# typemap_extensions. The point of this is to split the internals +# from what the tests import, so that type_eval can look at the real +# definitions while tests don't see that, and could have mypy stubs +# injected instead. +from typemap.typing import * # noqa: F403 diff --git a/typemap_extensions/__init__.pyi b/typemap_extensions/__init__.pyi new file mode 100644 index 0000000..dee12b5 --- /dev/null +++ b/typemap_extensions/__init__.pyi @@ -0,0 +1 @@ +from _typeshed.typemap import * From 68acb001739b5baaee1519946ea9d3c9106c17b8 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 4 Feb 2026 17:36:21 -0800 Subject: [PATCH 2/2] comment --- tests/test_fastapilike_2.py | 1 + tests/test_qblike_2.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/test_fastapilike_2.py b/tests/test_fastapilike_2.py index 088d3f8..a3e9fb4 100644 --- a/tests/test_fastapilike_2.py +++ b/tests/test_fastapilike_2.py @@ -208,6 +208,7 @@ class Hero: secret_name: str = Field(hidden=True) +# Quick reveal_type test for running mypy against this if TYPE_CHECKING: pubhero: Public[Hero] reveal_type(pubhero) # noqa diff --git a/tests/test_qblike_2.py b/tests/test_qblike_2.py index a27ffbe..8bcc57d 100644 --- a/tests/test_qblike_2.py +++ b/tests/test_qblike_2.py @@ -153,6 +153,7 @@ class User: def test_qblike_typing_only_1() -> None: + # Quick reveal_type test for running mypy against this if TYPE_CHECKING: _test_select = select( Post,