Skip to content

Commit 907a0d9

Browse files
Евгений БлиновЕвгений Блинов
authored andcommitted
Add typing tests for describe_call, repred, and superrepr
1 parent 1049b98 commit 907a0d9

4 files changed

Lines changed: 404 additions & 0 deletions

File tree

tests/typing/__init__.py

Whitespace-only changes.

tests/typing/test_describe_call.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import pytest
2+
from full_match import match
3+
4+
from printo import describe_call, not_none
5+
6+
7+
@pytest.mark.mypy_testing
8+
def test_describe_call_basic():
9+
"""mypy accepts the basic call signature and infers str return type."""
10+
_: str = describe_call('MyClass', (1, 2, 'text'), {'key': 1})
11+
12+
13+
@pytest.mark.mypy_testing
14+
def test_describe_call_with_list_args():
15+
"""mypy accepts a list for the args parameter, not only tuple."""
16+
_: str = describe_call('MyClass', [1, 2, 'text'], {'key': 1})
17+
18+
19+
@pytest.mark.mypy_testing
20+
def test_describe_call_with_filters():
21+
"""mypy accepts a filters dict with int and str keys and callable values."""
22+
_: str = describe_call('MyClass', (1, 2), {'key': 1}, filters={1: lambda x: x != 2, 'key': lambda _: True})
23+
24+
25+
@pytest.mark.mypy_testing
26+
def test_describe_call_with_not_none_filter():
27+
"""mypy accepts not_none as a valid filter value."""
28+
_: str = describe_call('MyClass', (1, None), {}, filters={1: not_none})
29+
30+
31+
@pytest.mark.mypy_testing
32+
def test_describe_call_with_serializer():
33+
"""mypy accepts a Callable[[Any], str] for the serializer parameter."""
34+
_: str = describe_call('MyClass', (1, 2), {'key': 'val'}, serializer=lambda x: repr(x).upper())
35+
36+
37+
@pytest.mark.mypy_testing
38+
def test_describe_call_with_placeholders():
39+
"""mypy accepts a placeholders dict with str and int keys and str values."""
40+
_: str = describe_call('MyClass', (1, 2), {'password': 'secret'}, placeholders={0: '***', 'password': '***'})
41+
42+
43+
@pytest.mark.mypy_testing
44+
def test_describe_call_with_item_limit():
45+
"""mypy accepts an int for the item_limit parameter."""
46+
_: str = describe_call('MyClass', (123456789,), {'name': 'a very long string'}, item_limit=5)
47+
48+
49+
@pytest.mark.mypy_testing
50+
def test_describe_call_with_total_limit():
51+
"""mypy accepts an int for the total_limit parameter."""
52+
_: str = describe_call('MyClass', (), {'a': 1, 'b': 2, 'c': 3}, total_limit=20)
53+
54+
55+
@pytest.mark.mypy_testing
56+
def test_describe_call_invalid_class_name_type():
57+
"""mypy rejects int as class_name — expected str."""
58+
describe_call(42, [], {}) # E: [arg-type]
59+
60+
61+
@pytest.mark.mypy_testing
62+
def test_describe_call_invalid_args_type():
63+
"""mypy rejects dict as args — expected Tuple or List."""
64+
describe_call('MyClass', {}, {}) # E: [arg-type]
65+
66+
67+
@pytest.mark.mypy_testing
68+
def test_describe_call_invalid_kwargs_key_type():
69+
"""mypy rejects int as a kwargs key — expected str."""
70+
describe_call('MyClass', (), {1: 'val'}) # E: [dict-item]
71+
72+
73+
@pytest.mark.mypy_testing
74+
def test_describe_call_invalid_kwargs_type():
75+
"""mypy rejects list as kwargs — expected Dict[str, Any]."""
76+
with pytest.raises(AttributeError, match=match("'list' object has no attribute 'items'")):
77+
describe_call('MyClass', [], []) # E: [arg-type]
78+
79+
80+
@pytest.mark.mypy_testing
81+
def test_describe_call_invalid_serializer_type():
82+
"""mypy rejects int as serializer — expected Callable[[Any], str]."""
83+
with pytest.raises(ValueError, match=match('It is impossible to determine the signature of an object that is not being callable.')):
84+
describe_call('MyClass', [], {}, serializer=42) # E: [arg-type]
85+
86+
87+
@pytest.mark.mypy_testing
88+
def test_describe_call_invalid_filters_key_type():
89+
"""mypy rejects float as a filters key — expected str or int."""
90+
describe_call('MyClass', [], {}, filters={1.5: not_none}) # E: [dict-item]
91+
92+
93+
@pytest.mark.mypy_testing
94+
def test_describe_call_invalid_filters_value_type():
95+
"""mypy rejects int as a filters value — expected Callable[[Any], bool]."""
96+
describe_call('MyClass', [], {}, filters={'x': 42}) # E: [dict-item]
97+
98+
99+
@pytest.mark.mypy_testing
100+
def test_describe_call_invalid_placeholders_key_type():
101+
"""mypy rejects float as a placeholders key — expected str or int."""
102+
describe_call('MyClass', [], {}, placeholders={1.5: '***'}) # E: [dict-item]
103+
104+
105+
@pytest.mark.mypy_testing
106+
def test_describe_call_invalid_placeholders_value_type():
107+
"""mypy rejects int as a placeholders value — expected str."""
108+
describe_call('MyClass', [], {}, placeholders={'x': 42}) # E: [dict-item]
109+
110+
111+
@pytest.mark.mypy_testing
112+
def test_describe_call_invalid_item_limit_type():
113+
"""mypy rejects str as item_limit — expected int."""
114+
with pytest.raises(TypeError, match=match("'<' not supported between instances of 'str' and 'int'")):
115+
describe_call('MyClass', [], {}, item_limit='5') # E: [arg-type]
116+
117+
118+
@pytest.mark.mypy_testing
119+
def test_describe_call_invalid_total_limit_type():
120+
"""mypy rejects str as total_limit — expected int."""
121+
with pytest.raises(TypeError, match=match("'<' not supported between instances of 'str' and 'int'")):
122+
describe_call('MyClass', [], {}, total_limit='15') # E: [arg-type]

tests/typing/test_repred.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import pytest
2+
from full_match import match
3+
from sigmatch import SignatureMismatchError
4+
5+
from printo import repred
6+
from printo.filters import not_none
7+
8+
9+
@pytest.mark.mypy_testing
10+
def test_repred_bare_decorator():
11+
"""mypy infers the exact decorated class type, not Any or object."""
12+
@repred
13+
class Foo:
14+
def __init__(self, x: int) -> None:
15+
self.x = x
16+
17+
_: Foo = Foo(1)
18+
19+
20+
@pytest.mark.mypy_testing
21+
def test_repred_empty_parens():
22+
"""mypy infers the exact decorated class type when @repred() is called with empty parens."""
23+
@repred()
24+
class Foo:
25+
def __init__(self, x: int) -> None:
26+
self.x = x
27+
28+
_: Foo = Foo(1)
29+
30+
31+
@pytest.mark.mypy_testing
32+
def test_repred_with_prefer_positional():
33+
"""mypy infers the exact decorated class type when prefer_positional=True is passed."""
34+
@repred(prefer_positional=True)
35+
class Foo:
36+
def __init__(self, x: int) -> None:
37+
self.x = x
38+
39+
_: Foo = Foo(1)
40+
41+
42+
@pytest.mark.mypy_testing
43+
def test_repred_with_qualname():
44+
"""mypy infers the exact decorated class type when qualname=True is passed."""
45+
@repred(qualname=True)
46+
class Foo:
47+
def __init__(self, x: int) -> None:
48+
self.x = x
49+
50+
_: Foo = Foo(1)
51+
52+
53+
@pytest.mark.mypy_testing
54+
def test_repred_with_ignore():
55+
"""mypy infers the exact decorated class type when ignore=[...] is passed."""
56+
@repred(ignore=['x'])
57+
class Foo:
58+
def __init__(self, x: int, y: int) -> None:
59+
self.x = x
60+
self.y = y
61+
62+
_: Foo = Foo(1, 2)
63+
64+
65+
@pytest.mark.mypy_testing
66+
def test_repred_with_getters():
67+
"""mypy infers the exact decorated class type when getters={...} is passed."""
68+
@repred(getters={'x': lambda self: self.x})
69+
class Foo:
70+
def __init__(self, x: int) -> None:
71+
self.x = x
72+
73+
_: Foo = Foo(1)
74+
75+
76+
@pytest.mark.mypy_testing
77+
def test_repred_with_filters():
78+
"""mypy infers the exact decorated class type when filters={...} is passed."""
79+
@repred(filters={'x': not_none})
80+
class Foo:
81+
def __init__(self, x: int) -> None:
82+
self.x = x
83+
84+
_: Foo = Foo(1)
85+
86+
87+
@pytest.mark.mypy_testing
88+
def test_repred_with_positionals():
89+
"""mypy infers the exact decorated class type when positionals=[...] is passed."""
90+
@repred(positionals=['x'])
91+
class Foo:
92+
def __init__(self, x: int) -> None:
93+
self.x = x
94+
95+
_: Foo = Foo(1)
96+
97+
98+
@pytest.mark.mypy_testing
99+
def test_repred_with_multiple_kwargs():
100+
"""mypy infers the exact decorated class type when multiple keyword args are passed."""
101+
@repred(prefer_positional=True, qualname=True)
102+
class Foo:
103+
def __init__(self, x: int) -> None:
104+
self.x = x
105+
106+
_: Foo = Foo(1)
107+
108+
109+
@pytest.mark.mypy_testing
110+
def test_repred_with_varargs():
111+
"""mypy infers the exact decorated class type on a class with *args and **kwargs."""
112+
@repred
113+
class Foo:
114+
def __init__(self, a: int, b: int, *args: int, **kwargs: str) -> None:
115+
self.a = a
116+
self.b = b
117+
self.args = args
118+
self.kwargs = kwargs
119+
120+
_: Foo = Foo(1, 2)
121+
122+
123+
@pytest.mark.mypy_testing
124+
def test_repred_instance_type():
125+
"""mypy infers Foo instance type, not object or Any, after bare @repred."""
126+
@repred
127+
class Foo:
128+
def __init__(self, x: int) -> None:
129+
self.x = x
130+
131+
_: Foo = Foo(1)
132+
133+
134+
@pytest.mark.mypy_testing
135+
def test_repred_instance_wrong_type():
136+
"""mypy rejects assigning a @repred-decorated class instance to an incompatible type."""
137+
@repred
138+
class Foo:
139+
def __init__(self, x: int) -> None:
140+
self.x = x
141+
142+
_: str = Foo(1) # E: [assignment]
143+
144+
145+
@pytest.mark.mypy_testing
146+
def test_repred_instance_wrong_type_with_kwargs():
147+
"""mypy rejects wrong-type assignment even when @repred is called with keyword args."""
148+
@repred(prefer_positional=True)
149+
class Foo:
150+
def __init__(self, x: int) -> None:
151+
self.x = x
152+
153+
_: str = Foo(1) # E: [assignment]
154+
155+
156+
@pytest.mark.mypy_testing
157+
def test_repred_invalid_filter_value_type():
158+
"""
159+
mypy rejects a non-callable filters value as dict-item error.
160+
161+
The pytest.raises wrapper is needed because repred also validates types at runtime.
162+
"""
163+
with pytest.raises(SignatureMismatchError, match=match('You have defined a getter for parameter "x" that cannot be called with a single argument.')):
164+
@repred(filters={'x': 42}) # E: [dict-item]
165+
class Foo:
166+
def __init__(self, x: int) -> None:
167+
self.x = x
168+
169+
170+
@pytest.mark.mypy_testing
171+
def test_repred_invalid_getter_value_type():
172+
"""
173+
mypy rejects a non-callable getters value as dict-item error.
174+
175+
The pytest.raises wrapper is needed because repred also validates types at runtime.
176+
"""
177+
with pytest.raises(SignatureMismatchError, match=match('You have defined a getter for parameter "x" that cannot be called with a single argument (an object of class Foo).')):
178+
@repred(getters={'x': 'not_a_function'}) # E: [dict-item]
179+
class Foo:
180+
def __init__(self, x: int) -> None:
181+
self.x = x
182+
183+
184+
@pytest.mark.mypy_testing
185+
def test_repred_invalid_filter_key_type():
186+
"""
187+
mypy rejects a float filters key as dict-item error.
188+
189+
The pytest.raises wrapper is needed because repred also validates types at runtime.
190+
"""
191+
with pytest.raises(ValueError, match=match('Keys for a filtered dictionary can be either integers starting from 0 or strings (parameter names).')):
192+
@repred(filters={1.5: not_none}) # E: [dict-item]
193+
class Foo:
194+
def __init__(self, x: int) -> None:
195+
self.x = x
196+
197+
198+
@pytest.mark.mypy_testing
199+
def test_repred_invalid_ignore_element_type():
200+
"""
201+
mypy rejects a non-str element in the ignore list as list-item error.
202+
203+
The pytest.raises wrapper is needed because repred also validates types at runtime.
204+
"""
205+
with pytest.raises(AttributeError, match=match("'int' object has no attribute 'isidentifier'")):
206+
@repred(ignore=[1]) # E: [list-item]
207+
class Foo:
208+
def __init__(self, x: int, y: int) -> None:
209+
self.x = x
210+
self.y = y
211+
212+
213+
@pytest.mark.mypy_testing
214+
def test_repred_invalid_positionals_element_type():
215+
"""
216+
mypy rejects a non-str element in the positionals list as list-item error.
217+
218+
The pytest.raises wrapper is needed because repred also validates types at runtime.
219+
"""
220+
with pytest.raises(AttributeError, match=match("'int' object has no attribute 'isidentifier'")):
221+
@repred(positionals=[1]) # E: [list-item]
222+
class Foo:
223+
def __init__(self, x: int) -> None:
224+
self.x = x

0 commit comments

Comments
 (0)