Skip to content

Commit 585191c

Browse files
authored
Support creating methods nicely in generated classes (#28)
To do this we convert our Callable+Param types into methods. The main reason for this is because it seems to be the expected thing, though maybe we'd be better off keeping them as methods? This lets us implement flatten_type fully in terms of `NewProtocol`+`Members`, no special work needed, though! I also added GenericCallable, since pulling that off required being able to represent type params. I am suuuuper unsure about that one, though.
1 parent c4b57a0 commit 585191c

7 files changed

Lines changed: 396 additions & 29 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ extend-ignore = [
5656
"E402", # module-import-not-at-top-of-file
5757
"E252", # missing-whitespace-around-parameter-equals
5858
"F541", # f-string-missing-placeholders
59+
"E731", # don't assign lambdas
5960
]
6061

6162
[tool.ruff.lint.per-file-ignores]

tests/test_schemalike.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import textwrap
2+
3+
from typing import Callable, Literal
4+
5+
from typemap.type_eval import eval_typing
6+
from typemap.typing import (
7+
NewProtocol,
8+
Iter,
9+
Attrs,
10+
GetType,
11+
GetName,
12+
Member,
13+
Param,
14+
StrConcat,
15+
)
16+
17+
from . import format_helper
18+
19+
20+
class Schema:
21+
pass
22+
23+
24+
class Type:
25+
pass
26+
27+
28+
class Expression:
29+
pass
30+
31+
32+
# hmmmm... recursion with this sort of thing will be funny...
33+
# how will we handle the decorators or __init_subclass__ or what have you
34+
35+
36+
class Property:
37+
name: str
38+
required: bool
39+
multi: bool
40+
typ: Type
41+
expr: Expression | None
42+
43+
44+
type Schemaify[T] = NewProtocol[
45+
*[p for p in Iter[Attrs[T]]],
46+
*[
47+
Member[
48+
StrConcat[Literal["get_"], GetName[p]],
49+
Callable[
50+
[
51+
Param[Literal["self"], Schemaify[T]],
52+
Param[Literal["schema"], Schema, Literal["keyword"]],
53+
],
54+
GetType[p],
55+
],
56+
Literal["ClassVar"],
57+
]
58+
for p in Iter[Attrs[T]]
59+
],
60+
]
61+
62+
63+
def test_schema_like_1():
64+
tgt = eval_typing(Schemaify[Property])
65+
fmt = format_helper.format_class(tgt)
66+
67+
assert fmt == textwrap.dedent("""\
68+
class Schemaify[tests.test_schemalike.Property]:
69+
name: str
70+
required: bool
71+
multi: bool
72+
typ: tests.test_schemalike.Type
73+
expr: tests.test_schemalike.Expression | None
74+
def get_name(self: Schemaify[tests.test_schemalike.Property], *, schema: tests.test_schemalike.Schema) -> str: ...
75+
def get_required(self: Schemaify[tests.test_schemalike.Property], *, schema: tests.test_schemalike.Schema) -> bool: ...
76+
def get_multi(self: Schemaify[tests.test_schemalike.Property], *, schema: tests.test_schemalike.Schema) -> bool: ...
77+
def get_typ(self: Schemaify[tests.test_schemalike.Property], *, schema: tests.test_schemalike.Schema) -> tests.test_schemalike.Type: ...
78+
def get_expr(self: Schemaify[tests.test_schemalike.Property], *, schema: tests.test_schemalike.Schema) -> tests.test_schemalike.Expression | None: ...
79+
""")

tests/test_type_dir.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ def sbase[Z](a: OrGotcha[T] | Z | None, b: K) -> dict[str, T | Z]:
5858
pass
5959

6060

61+
class CMethod:
62+
@classmethod
63+
def cbase2(cls, lol: int, /, a: bool | None) -> int:
64+
pass
65+
66+
6167
class Wrapper[X](Base[X], AnotherBase[X]):
6268
x: "Wrapper[X | None]"
6369

@@ -185,7 +191,7 @@ def test_type_dir_link_2():
185191
assert loop is Foo
186192

187193

188-
def test_type_dir_1():
194+
def test_type_dir_1a():
189195
d = eval_typing(Final)
190196

191197
assert format_helper.format_class(d) == textwrap.dedent("""\
@@ -197,15 +203,25 @@ class Final:
197203
fin: typing.Final[int]
198204
x: tests.test_type_dir.Wrapper[int | None]
199205
ordinary: str
200-
def foo(self, a: int | None, *, b: int = 0) -> dict[str, int]: ...
201-
def base[Z](self, a: int | Z | None, b: ~K) -> dict[str, int | Z]: ...
206+
def foo(self: tests.test_type_dir.Base[int], a: int | None, *, b: int = ...) -> dict[str, int]: ...
207+
def base[Z](self: tests.test_type_dir.Base[int], a: int | Z | None, b: ~K) -> dict[str, int | Z]: ...
202208
@classmethod
203-
def cbase(cls, a: int | None, b: ~K) -> dict[str, int]: ...
209+
def cbase(cls: type[tests.test_type_dir.Base[int]], a: int | None, b: ~K) -> dict[str, int]: ...
204210
@staticmethod
205211
def sbase[Z](a: int | Literal['gotcha!'] | Z | None, b: ~K) -> dict[str, int | Z]: ...
206212
""")
207213

208214

215+
def test_type_dir_1b():
216+
d = eval_typing(CMethod)
217+
218+
assert format_helper.format_class(d) == textwrap.dedent("""\
219+
class CMethod:
220+
@classmethod
221+
def cbase2(_arg0: type[tests.test_type_dir.CMethod], _arg1: int, /, a: bool | None) -> int: ...
222+
""")
223+
224+
209225
def test_type_dir_2():
210226
d = eval_typing(OptionalFinal)
211227

@@ -394,6 +410,8 @@ def test_type_members_func_3():
394410

395411
assert (
396412
str(typ)
413+
# == "\
414+
# staticmethod[tuple[typemap.typing.Param[typing.Literal['a'], int | typing.Literal['gotcha!'] | Z | None, typing.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Never]], dict[str, int | Z]]"
397415
== "\
398-
staticmethod[tuple[typemap.typing.Param[typing.Literal['a'], int | typing.Literal['gotcha!'] | Z | None, typing.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Never]], dict[str, int | Z]]"
416+
typemap.typing.GenericCallable[tuple[Z], staticmethod[tuple[typemap.typing.Param[typing.Literal['a'], int | typing.Literal['gotcha!'] | Z | None, typing.Never], typemap.typing.Param[typing.Literal['b'], ~K, typing.Never]], dict[str, int | Z]]]"
399417
)

tests/test_type_eval.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import pytest
2-
31
import collections
42
import textwrap
53
import unittest
@@ -15,6 +13,8 @@
1513
Union,
1614
)
1715

16+
import pytest
17+
1818
from typemap.type_eval import eval_typing
1919
from typemap.typing import (
2020
Attrs,
@@ -730,3 +730,42 @@ def test_eval_literal_idempotent_01():
730730
nt = eval_typing(t)
731731
assert t == nt
732732
t = nt
733+
734+
735+
def test_callable_to_signature():
736+
from typemap.type_eval._eval_operators import _callable_type_to_signature
737+
from typemap.typing import Param
738+
739+
# Test the example from the docstring:
740+
# def func(
741+
# a: int,
742+
# /,
743+
# b: int,
744+
# c: int = 0,
745+
# *args: int,
746+
# d: int,
747+
# e: int = 0,
748+
# **kwargs: int
749+
# ) -> int:
750+
callable_type = Callable[
751+
[
752+
Param[None, int],
753+
Param[Literal["b"], int],
754+
Param[Literal["c"], int, Literal["default"]],
755+
Param[None, int, Literal["*"]],
756+
Param[Literal["d"], int, Literal["keyword"]],
757+
Param[Literal["e"], int, Literal["default", "keyword"]],
758+
Param[None, int, Literal["**"]],
759+
],
760+
int,
761+
]
762+
763+
sig = _callable_type_to_signature(callable_type)
764+
765+
params = list(sig.parameters.values())
766+
assert len(params) == 7
767+
768+
assert str(sig) == (
769+
'(_arg0: int, /, b: int, c: int = ..., *args: int, '
770+
'd: int, e: int = ..., **kwargs: int) -> int'
771+
)

typemap/type_eval/_apply_generic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,4 +396,4 @@ def flatten_class_explicit(obj: typing.Any):
396396
return _flatten_class_explicit(obj, ctx)
397397

398398

399-
flatten_class = flatten_class_explicit
399+
flatten_class = flatten_class_new_proto

0 commit comments

Comments
 (0)