Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ This is the development repository for
which proposes TypeScript-inspired type-level introspection and construction
facilities for the Python type system.

This repository contains an implementation of the proposed additions
to ``typing`` ([typemap/typing.py](typemap/typing.py)), exported as
the module ``typemap_extensions``.

It also contains a **prototype** runtime evaluator
([typemap/type_eval](typemap/type_eval)).

Discussion of the PEP is at the
[PEP 827 discussion thread](https://discuss.python.org/t/pep-827-type-manipulation/106353).

This repository also contains an implementation of the proposed
additions to ``typing`` ([typemap/typing.py](typemap/typing.py)), as well as a
**prototype** runtime evaluator ([typemap/type_eval](typemap/type_eval)).
A prototype typechecker implementation lives at
https://github.com/msullivan/mypy-typemap and is a test dependency of
this repo.

## Development

Expand All @@ -21,9 +28,11 @@ additions to ``typing`` ([typemap/typing.py](typemap/typing.py)), as well as a

## 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`
The prototype mypy can be run from this repo with `uv run mypy`.
Stubs are set up so that importing ``typemap_extensions`` will do the
right thing.

`uv run pytest tests/test_mypy_proto.py` will run the mypy prototype
against a supported subset of test files.

Not all of them run cleanly yet though.
You can also run the prototype mypy directly on a file with `uv run mypy <file>`
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ include = ["typemap", "typemap_extensions"]
test = [
"pytest>=7.0",
"ruff",
"mypy==1.18.1",
"mypy @ git+https://github.com/msullivan/mypy-typemap@f49083e5cd7124df93ea1a0a844d60adf901c250",
]

[tool.uv]
Expand Down
19 changes: 19 additions & 0 deletions scripts/update-mypy-pin
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash
set -e

MYPY_DIR="${1:-../mypy}"

if [ ! -d "$MYPY_DIR/.git" ]; then
echo "error: $MYPY_DIR is not a git repository" >&2
exit 1
fi

REV=$(git -C "$MYPY_DIR" rev-parse HEAD)
echo "Pinning mypy to $REV (from $MYPY_DIR)"

sed -i "s|mypy @ git+https://github.com/msullivan/mypy-typemap@[a-f0-9]*|mypy @ git+https://github.com/msullivan/mypy-typemap@$REV|" pyproject.toml

uv sync

git add pyproject.toml uv.lock
git commit -m "Pin mypy to mypy-typemap@${REV:0:12}"
2 changes: 2 additions & 0 deletions tests/test_astlike_1.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# SKIP MYPY: runs forever! must debug

import pytest
import typing

Expand Down
3 changes: 2 additions & 1 deletion tests/test_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
def func[*T, K: BaseTypedDict](
*args: Unpack[T],
**kwargs: Unpack[K],
) -> NewProtocol[*[Member[c.name, int] for c in Iter[Attrs[K]]]]: ...
) -> NewProtocol[*[Member[c.name, int] for c in Iter[Attrs[K]]]]:
raise NotImplementedError


def test_call_1():
Expand Down
43 changes: 19 additions & 24 deletions tests/test_cqa.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ def project_root() -> pathlib.Path:

def test_cqa_ruff_check(project_root):
"""Test that code passes ruff linting checks."""
# Ruff respects pyproject.toml configuration and exclusions
result = subprocess.run(
[sys.executable, "-m", "ruff", "check", "."],
capture_output=True,
Expand All @@ -32,7 +31,6 @@ def test_cqa_ruff_check(project_root):

def test_cqa_ruff_format_check(project_root):
"""Test that code is properly formatted according to ruff."""
# Ruff format respects pyproject.toml exclusions
result = subprocess.run(
[sys.executable, "-m", "ruff", "format", "--check", "."],
capture_output=True,
Expand All @@ -48,26 +46,23 @@ def test_cqa_ruff_format_check(project_root):


def test_cqa_mypy(project_root):
"""Test that code passes mypy type checking."""
# Mypy uses configuration from pyproject.toml
# Run on typemap -- tests not ready yet
for subdir in ["typemap"]:
result = subprocess.run(
[
sys.executable,
"-m",
"mypy",
"--config-file",
project_root / "pyproject.toml",
subdir,
],
capture_output=True,
text=True,
cwd=project_root,
)
"""Test that typemap/ passes mypy type checking."""
result = subprocess.run(
[
sys.executable,
"-m",
"mypy",
"--config-file",
project_root / "pyproject.toml",
"typemap",
],
capture_output=True,
text=True,
cwd=project_root,
)

if result.returncode != 0:
output = result.stdout
if result.stderr:
output += "\n\n" + result.stderr
pytest.fail(f"mypy validation failed:\n{output}", pytrace=False)
if result.returncode != 0:
output = result.stdout
if result.stderr:
output += "\n\n" + result.stderr
pytest.fail(f"mypy validation failed:\n{output}", pytrace=False)
8 changes: 3 additions & 5 deletions tests/test_dataclass_like.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
ReadOnly,
TypedDict,
Never,
Self,
)

import typemap_extensions as typing
Expand Down Expand Up @@ -54,7 +53,7 @@ class Field[T: FieldArgs](typing.InitField[T]):
Literal["__init__"],
Callable[
typing.Params[
typing.Param[Literal["self"], Self],
typing.Param[Literal["self"], T],
*[
typing.Param[
p.name,
Expand All @@ -80,7 +79,6 @@ class Field[T: FieldArgs](typing.InitField[T]):
*[x for x in typing.Iter[typing.Members[T]]],
]


"""

``UpdateClass`` can then be used to create a class decorator (a la
Expand All @@ -95,7 +93,7 @@ def dataclass_ish[T](
# Add the computed __init__ function
InitFnType[T],
]:
pass
raise NotImplementedError


"""
Expand All @@ -112,7 +110,7 @@ def __init_subclass__[T](
# Add the computed __init__ function
InitFnType[T],
]:
super().__init_subclass__()
pass


# End PEP section
Expand Down
2 changes: 2 additions & 0 deletions tests/test_eval_call_with_types.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# SKIP MYPY

import pytest

from typing import Callable, Generic, Literal, Self, TypeVar
Expand Down
7 changes: 6 additions & 1 deletion tests/test_fastapilike_1.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# SKIP MYPY: Invalid use of Self
# TODO: resolve

# We should have at least *one* test with this...
from __future__ import annotations

Expand Down Expand Up @@ -71,7 +74,9 @@ class _Default:

type AddInit[T] = NewProtocol[
InitFnType[T],
*Members[T],
# TODO: mypy rejects this -- should it work?
# *Members[T],
*[t for t in Iter[Members[T]]],
]

# Strip `| None` from a type by iterating over its union components
Expand Down
3 changes: 3 additions & 0 deletions tests/test_fastapilike_2.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# SKIP MYPY: Invalid use of Self
# TODO: resolve

from typing import (
Callable,
Literal,
Expand Down
4 changes: 2 additions & 2 deletions tests/test_model_like.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pytest

from typing import _GenericAlias
from typing import _GenericAlias # type: ignore[attr-defined]

import typemap_extensions as typing

Expand Down Expand Up @@ -44,7 +44,7 @@ def __repr__(self):
return f'{type(self).__name__}(**{self.__dict__})'


class _BaseModelAlias(_GenericAlias, _root=True):
class _BaseModelAlias(_GenericAlias, _root=True): # type: ignore[call-arg]
def __call__(self, *args, **kwargs):
return self.__origin__(*args, **kwargs, _alias=self)

Expand Down
56 changes: 56 additions & 0 deletions tests/test_mypy_proto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Run mypy on each test file that doesn't have # SKIP MYPY."""

import os
import pathlib
import subprocess
import sys

import pytest

PROJECT_ROOT = pathlib.Path(__file__).parent.parent

# Set MYPY_SOURCE_DIR to use a local mypy source checkout.
_mypy_source = os.environ.get("MYPY_SOURCE_DIR")
MYPY_SOURCE_DIR = pathlib.Path(_mypy_source).resolve() if _mypy_source else None


def _collect_mypy_test_files():
"""Collect test files that don't have # SKIP MYPY."""
tests_dir = pathlib.Path(__file__).parent
for path in sorted(tests_dir.glob("test_*.py")):
if path.name in ("test_cqa.py", "test_mypy_proto.py"):
continue
text = path.read_text()
if "# SKIP MYPY" not in text:
yield pytest.param(path, id=path.stem)


@pytest.mark.parametrize("test_file", _collect_mypy_test_files())
def test_mypy(test_file):
"""Test that individual test files pass mypy."""
env = None
if MYPY_SOURCE_DIR:
env = {**os.environ, "PYTHONPATH": str(MYPY_SOURCE_DIR)}
cmd = [
sys.executable,
"-m",
"mypy",
"--config-file",
str(PROJECT_ROOT / "pyproject.toml"),
str(test_file),
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=PROJECT_ROOT,
env=env,
)

if result.returncode != 0:
output = result.stdout
if result.stderr:
output += "\n\n" + result.stderr
pytest.fail(
f"mypy failed on {test_file.name}:\n{output}", pytrace=False
)
3 changes: 2 additions & 1 deletion tests/test_qblike.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ def select[K: BaseTypedDict](
]
for c in Iter[Attrs[K]]
]
]: ...
]:
raise NotImplementedError


# Basic filtering
Expand Down
3 changes: 3 additions & 0 deletions tests/test_qblike_3.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# SKIP MYPY: a *lot* of errors
# TODO: investigate

import dataclasses
import enum
import textwrap
Expand Down
2 changes: 1 addition & 1 deletion tests/test_schemalike.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class Property:
Callable[
Params[
Param[Literal["self"], Schemaify[T]],
NamedParam[Literal["schema"], Schema, Literal["keyword"]],
NamedParam[Literal["schema"], Schema],
],
p.type,
],
Expand Down
3 changes: 3 additions & 0 deletions tests/test_type_dir.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# SKIP MYPY: lots of weird testing stuff
# but TODO: some failures seem bad

import textwrap
import typing
from typing import Literal, Never, TypeVar, TypedDict, Union, ReadOnly
Expand Down
2 changes: 2 additions & 0 deletions tests/test_type_eval.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# SKIP MYPY

from __future__ import annotations

import collections
Expand Down
Loading
Loading