Skip to content

Commit 40f0ddd

Browse files
Merge branch 'master' into any-all-for-helper
2 parents a04345f + 374fefb commit 40f0ddd

File tree

9 files changed

+454
-16
lines changed

9 files changed

+454
-16
lines changed

docs/source/error_code_list2.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,3 +676,26 @@ Example:
676676
print("red")
677677
case _:
678678
print("other")
679+
680+
.. _code-untyped-decorator:
681+
682+
Error if an untyped decorator makes a typed function effectively untyped [untyped-decorator]
683+
--------------------------------------------------------------------------------------------
684+
685+
If enabled with :option:`--disallow-untyped-decorators <mypy --disallow-untyped-decorators>`
686+
mypy generates an error if a typed function is wrapped by an untyped decorator
687+
(as this would effectively remove the benefits of typing the function).
688+
689+
Example:
690+
691+
.. code-block:: python
692+
693+
def printing_decorator(func):
694+
def wrapper(*args, **kwds):
695+
print("Calling", func)
696+
return func(*args, **kwds)
697+
return wrapper
698+
# A decorated function.
699+
@printing_decorator # E: Untyped decorator makes function "add_forty_two" untyped [untyped-decorator]
700+
def add_forty_two(value: int) -> int:
701+
return value + 42

docs/source/stubtest.rst

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,75 @@ to mypy build errors". In this case, you will need to mitigate those errors
9999
before stubtest will run. Despite potential overlap in errors here, stubtest is
100100
not intended as a substitute for running mypy directly.
101101

102+
Allowlist
103+
*********
104+
102105
If you wish to ignore some of stubtest's complaints, stubtest supports a
103-
pretty handy allowlist system.
106+
pretty handy :option:`--allowlist` system.
107+
108+
Let's say that you have this python module called ``ex``:
109+
110+
.. code-block:: python
111+
112+
try:
113+
import optional_expensive_dep
114+
except ImportError:
115+
optional_expensive_dep = None
116+
117+
first = 1
118+
if optional_expensive_dep:
119+
second = 2
120+
121+
Let's say that you can't install ``optional_expensive_dep`` in CI for some reason,
122+
but you still want to include ``second: int`` in the stub file:
123+
124+
.. code-block:: python
125+
126+
first: int
127+
second: int
128+
129+
In this case stubtest will correctly complain:
130+
131+
.. code-block:: shell
132+
133+
error: ex.second is not present at runtime
134+
Stub: in file /.../ex.pyi:2
135+
builtins.int
136+
Runtime:
137+
MISSING
138+
139+
Found 1 error (checked 1 module)
140+
141+
To fix this, you can add an ``allowlist`` entry:
142+
143+
.. code-block:: ini
144+
145+
# Allowlist entries in `allowlist.txt` file:
146+
147+
# Does not exist if `optional_expensive_dep` is not installed:
148+
ex.second
149+
150+
And now when running stubtest with ``--allowlist=allowlist.txt``,
151+
no errors will be generated anymore.
152+
153+
Allowlists also support regular expressions,
154+
which can be useful to ignore many similar errors at once.
155+
They can also be useful for suppressing stubtest errors that occur sometimes,
156+
but not on every CI run. For example, if some CI workers have
157+
``optional_expensive_dep`` installed, stubtest might complain with this message
158+
on those workers if you had the ``ex.second`` allowlist entry:
159+
160+
.. code-block:: ini
161+
162+
note: unused allowlist entry ex.second
163+
Found 1 error (checked 1 module)
164+
165+
Changing ``ex.second`` to be ``(ex\.second)?`` will make this error optional,
166+
meaning that stubtest will pass whether or not a CI runner
167+
has``optional_expensive_dep`` installed.
168+
169+
CLI
170+
***
104171

105172
The rest of this section documents the command line interface of stubtest.
106173

@@ -119,15 +186,15 @@ The rest of this section documents the command line interface of stubtest.
119186
.. option:: --allowlist FILE
120187

121188
Use file as an allowlist. Can be passed multiple times to combine multiple
122-
allowlists. Allowlists can be created with --generate-allowlist. Allowlists
123-
support regular expressions.
189+
allowlists. Allowlists can be created with :option:`--generate-allowlist`.
190+
Allowlists support regular expressions.
124191

125192
The presence of an entry in the allowlist means stubtest will not generate
126193
any errors for the corresponding definition.
127194

128195
.. option:: --generate-allowlist
129196

130-
Print an allowlist (to stdout) to be used with --allowlist
197+
Print an allowlist (to stdout) to be used with :option:`--allowlist`.
131198

132199
When introducing stubtest to an existing project, this is an easy way to
133200
silence all existing errors.
@@ -141,17 +208,17 @@ The rest of this section documents the command line interface of stubtest.
141208

142209
Note if an allowlist entry is a regex that matches the empty string,
143210
stubtest will never consider it unused. For example, to get
144-
`--ignore-unused-allowlist` behaviour for a single allowlist entry like
211+
``--ignore-unused-allowlist`` behaviour for a single allowlist entry like
145212
``foo.bar`` you could add an allowlist entry ``(foo\.bar)?``.
146213
This can be useful when an error only occurs on a specific platform.
147214

148215
.. option:: --mypy-config-file FILE
149216

150-
Use specified mypy config file to determine mypy plugins and mypy path
217+
Use specified mypy config *file* to determine mypy plugins and mypy path
151218

152219
.. option:: --custom-typeshed-dir DIR
153220

154-
Use the custom typeshed in DIR
221+
Use the custom typeshed in *DIR*
155222

156223
.. option:: --check-typeshed
157224

mypy/errorcodes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,10 @@ def __hash__(self) -> int:
302302
sub_code_of=MISC,
303303
)
304304

305+
UNTYPED_DECORATOR: Final = ErrorCode(
306+
"untyped-decorator", "Error if an untyped decorator makes a typed function untyped", "General"
307+
)
308+
305309
NARROWED_TYPE_NOT_SUBTYPE: Final = ErrorCode(
306310
"narrowed-type-not-subtype",
307311
"Warn if a TypeIs function's narrowed type is not a subtype of the original type",

mypy/messages.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2008,7 +2008,11 @@ def untyped_decorated_function(self, typ: Type, context: Context) -> None:
20082008
)
20092009

20102010
def typed_function_untyped_decorator(self, func_name: str, context: Context) -> None:
2011-
self.fail(f'Untyped decorator makes function "{func_name}" untyped', context)
2011+
self.fail(
2012+
f'Untyped decorator makes function "{func_name}" untyped',
2013+
context,
2014+
code=codes.UNTYPED_DECORATOR,
2015+
)
20122016

20132017
def bad_proto_variance(
20142018
self, actual: int, tvar_name: str, expected: int, context: Context

mypyc/irbuild/function.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
dict_new_op,
8484
exact_dict_set_item_op,
8585
)
86-
from mypyc.primitives.generic_ops import generic_getattr, py_setattr_op
86+
from mypyc.primitives.generic_ops import generic_getattr, generic_setattr, py_setattr_op
8787
from mypyc.primitives.misc_ops import register_function
8888
from mypyc.primitives.registry import builtin_names
8989
from mypyc.sametype import is_same_method_signature, is_same_type
@@ -423,8 +423,10 @@ def generate_setattr_wrapper(builder: IRBuilder, cdef: ClassDef, setattr: FuncDe
423423
Returns 0 on success and -1 on failure. Restrictions are similar to the __getattr__
424424
wrapper above.
425425
426-
This one is simpler because to match interpreted python semantics it's enough to always
427-
call the user-provided function, including for names matching regular attributes.
426+
The wrapper calls the user-defined __setattr__ when the value to set is not NULL.
427+
When it's NULL, this means that the call to tp_setattro comes from a del statement,
428+
so it calls __delattr__ instead. If __delattr__ is not overridden in the native class,
429+
this will call the base implementation in object which doesn't work without __dict__.
428430
"""
429431
name = setattr.name + "__wrapper"
430432
ir = builder.mapper.type_to_ir[cdef.info]
@@ -440,6 +442,27 @@ def generate_setattr_wrapper(builder: IRBuilder, cdef: ClassDef, setattr: FuncDe
440442
attr_arg = builder.add_argument("attr", object_rprimitive)
441443
value_arg = builder.add_argument("value", object_rprimitive)
442444

445+
call_delattr, call_setattr = BasicBlock(), BasicBlock()
446+
null = Integer(0, object_rprimitive, line)
447+
is_delattr = builder.add(ComparisonOp(value_arg, null, ComparisonOp.EQ, line))
448+
builder.add_bool_branch(is_delattr, call_delattr, call_setattr)
449+
450+
builder.activate_block(call_delattr)
451+
delattr_symbol = cdef.info.get("__delattr__")
452+
delattr = delattr_symbol.node if delattr_symbol else None
453+
delattr_override = delattr is not None and not delattr.fullname.startswith("builtins.")
454+
if delattr_override:
455+
builder.gen_method_call(builder.self(), "__delattr__", [attr_arg], None, line)
456+
else:
457+
# Call internal function that cpython normally calls when deleting an attribute.
458+
# Cannot call object.__delattr__ here because it calls PyObject_SetAttr internally
459+
# which in turn calls our wrapper and recurses infinitely.
460+
# Note that since native classes don't have __dict__, this will raise AttributeError
461+
# for dynamic attributes.
462+
builder.call_c(generic_setattr, [builder.self(), attr_arg, null], line)
463+
builder.add(Return(Integer(0, c_int_rprimitive), line))
464+
465+
builder.activate_block(call_setattr)
443466
builder.gen_method_call(builder.self(), setattr.name, [attr_arg, value_arg], None, line)
444467
builder.add(Return(Integer(0, c_int_rprimitive), line))
445468

@@ -514,6 +537,14 @@ def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None
514537
generate_getattr_wrapper(builder, cdef, fdef)
515538
elif fdef.name == "__setattr__":
516539
generate_setattr_wrapper(builder, cdef, fdef)
540+
elif fdef.name == "__delattr__":
541+
setattr = cdef.info.get("__setattr__")
542+
if not setattr or not setattr.node or setattr.node.fullname.startswith("builtins."):
543+
builder.error(
544+
'"__delattr__" supported only in classes that also override "__setattr__", '
545+
+ "or inherit from a native class that overrides it.",
546+
fdef.line,
547+
)
517548

518549

519550
def handle_non_ext_method(

mypyc/test-data/fixtures/ir.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def __eq__(self, x: object) -> bool: pass
4646
def __ne__(self, x: object) -> bool: pass
4747
def __str__(self) -> str: pass
4848
def __setattr__(self, k: str, v: object) -> None: pass
49+
def __delattr__(self, k: str) -> None: pass
4950

5051
class type:
5152
def __init__(self, o: object) -> None: ...

0 commit comments

Comments
 (0)