diff --git a/pytype/abstract/_classes.py b/pytype/abstract/_classes.py index 5e7a68ff4..5b518a611 100644 --- a/pytype/abstract/_classes.py +++ b/pytype/abstract/_classes.py @@ -464,11 +464,15 @@ def _init_attr_metadata_from_pytd(self, decorator): # InitVar attributes to generate __init__, so the fields we want to add to # the subclass __init__ are the init params rather than the full list of # class attributes. - # We also need to use the list of class constants to restore names of the - # form `_foo`, which get replaced by `foo` in __init__. init = next(x for x in self.pytd_cls.methods if x.name == "__init__") - protected = {x.name[1:]: x.name for x in self.pytd_cls.constants - if x.name.startswith("_")} + # attr strips the leading underscores off of fields when generating the + # __init__ argument for fields. This behavior may not be shared by other + # libraries, such as dataclasses. + if decorator.startswith("attr."): + protected = {x.name[1:]: x.name for x in self.pytd_cls.constants + if x.name.startswith("_")} + else: + protected = {} params = [] for p in init.signatures[0].params[1:]: if p.name in protected: diff --git a/pytype/tests/test_abc2.py b/pytype/tests/test_abc2.py index 959ce7396..ed96c464f 100644 --- a/pytype/tests/test_abc2.py +++ b/pytype/tests/test_abc2.py @@ -183,9 +183,9 @@ def test_abstract_property(self): errors = self.CheckWithErrors(""" import abc class Foo(abc.ABC): - @abc.abstractmethod + @abc.abstractmethod # wrong-arg-types[e]>=3.11 @property - def f(self) -> str: # wrong-arg-types[e] + def f(self) -> str: # wrong-arg-types[e]<3.11 return 'a' @property diff --git a/pytype/tests/test_attr1.py b/pytype/tests/test_attr1.py index 8dc776e4a..b49beedb7 100644 --- a/pytype/tests/test_attr1.py +++ b/pytype/tests/test_attr1.py @@ -152,12 +152,10 @@ def __init__(self, x: int, y: str) -> None: ... """) def test_type_clash(self): - # Note: explicitly inheriting from object keeps the line number of the error - # stable between Python versions. self.CheckWithErrors(""" import attr - @attr.s - class Foo(object): # invalid-annotation + @attr.s # invalid-annotation>=3.11 + class Foo: # invalid-annotation<3.11 x = attr.ib(type=str) # type: int y = attr.ib(type=str, default="") # type: int Foo(x="") # should not report an error @@ -917,8 +915,8 @@ def int_attrib(): self.CheckWithErrors(""" import attr import foo - @attr.s() - class Foo(object): # invalid-annotation + @attr.s() # invalid-annotation>=3.11 + class Foo: # invalid-annotation<3.11 x: int = foo.int_attrib() """, pythonpath=[d.path]) diff --git a/pytype/tests/test_attr2.py b/pytype/tests/test_attr2.py index fecdcb37b..f6f9c13c0 100644 --- a/pytype/tests/test_attr2.py +++ b/pytype/tests/test_attr2.py @@ -289,12 +289,10 @@ def __init__(self, x: int, y: str) -> None: ... """) def test_type_clash(self): - # Note: explicitly inheriting from object keeps the line number of the error - # stable between Python versions. self.CheckWithErrors(""" import attr - @attr.s - class Foo(object): # invalid-annotation + @attr.s # invalid-annotation>=3.11 + class Foo: # invalid-annotation<3.11 x : int = attr.ib(type=str) """) @@ -633,12 +631,10 @@ def __attrs_init__(self, x, y: int, z: str = "bar") -> None: ... """) def test_bad_default_param_order(self): - # Note: explicitly inheriting from object keeps the line number of the error - # stable between Python versions. self.CheckWithErrors(""" import attr - @attr.s(auto_attribs=True) - class Foo(object): # invalid-function-definition + @attr.s(auto_attribs=True) # invalid-function-definition>=3.11 + class Foo: # invalid-function-definition<3.11 x: int = 10 y: str """) diff --git a/pytype/tests/test_dataclasses.py b/pytype/tests/test_dataclasses.py index 4c2624909..7a09cfbc1 100644 --- a/pytype/tests/test_dataclasses.py +++ b/pytype/tests/test_dataclasses.py @@ -231,12 +231,10 @@ def __init__(self, x: bool, y: int) -> None: ... """) def test_bad_default_param_order(self): - # Note: explicitly inheriting from object keeps the line number of the error - # stable between Python versions. self.CheckWithErrors(""" import dataclasses - @dataclasses.dataclass() - class Foo(object): # invalid-function-definition + @dataclasses.dataclass() # invalid-function-definition>=3.11 + class Foo: # invalid-function-definition<3.11 x: int = 10 y: str """) @@ -730,8 +728,8 @@ def test_sticky_kwonly_error(self): self.CheckWithErrors(""" import dataclasses - @dataclasses.dataclass - class A(): # dataclass-error + @dataclasses.dataclass # dataclass-error>=3.11 + class A: # dataclass-error<3.11 a1: int _a: dataclasses.KW_ONLY a2: int = dataclasses.field(default_factory=lambda: 0) @@ -802,6 +800,51 @@ class Test: dataclasses.replace(Test(), y=1, z=2) # wrong-arg-types """) + def test_replace_late_annotation(self): + # Regression test: LateAnnotations (like `z: Z`) should behave + # like their underlying types once resolved. The dataclass overlay + # relies on this behavior. + self.Check(""" + from __future__ import annotations + import dataclasses + @dataclasses.dataclass + class A: + z: Z + def do(self): + return dataclasses.replace(self.z, name="A") + @dataclasses.dataclass + class Z: + name: str + """) + + def test_replace_as_method_with_kwargs(self): + # This is a weird case where replace is added as a method, then called + # with kwargs. This makes pytype unable to see that `self` is the object + # being modified, and also caused a crash when the dataclass overlay tries + # to unpack the object being modified from the args. + self.Check(""" + import dataclasses + @dataclasses.dataclass + class WithKwargs: + replace = dataclasses.replace + def do(self, **kwargs): + return self.replace(**kwargs) + """) + + def test_replace_subclass(self): + self.CheckWithErrors(""" + import dataclasses + @dataclasses.dataclass + class Base: + name: str + @dataclasses.dataclass + class Sub(Base): + index: int + a = Sub(name="a", index=0) + dataclasses.replace(a, name="b", index=2) + dataclasses.replace(a, name="c", idx=3) # wrong-keyword-args + """) + class TestPyiDataclass(test_base.BaseTest): """Tests for @dataclasses in pyi files.""" @@ -1316,50 +1359,25 @@ def __init__(self, x: int) -> None: ... dataclasses.replace(x, y=1, z=2) # wrong-keyword-args """) - def test_replace_late_annotation(self): - # Regression test: LateAnnotations (like `z: Z`) should behave - # like their underlying types once resolved. The dataclass overlay - # relies on this behavior. - self.Check(""" - from __future__ import annotations + def test_no_name_mangling(self): + # attrs turns _x into x in `__init__`. We account for this in + # PytdClass._init_attr_metadata_from_pytd by replacing "x" with "_x" when + # reconstructing the class's attr metadata. + # However, dataclasses does *not* do this name mangling. + with self.DepTree([("foo.pyi", """ import dataclasses + from typing import Annotated @dataclasses.dataclass class A: - z: Z - def do(self): - return dataclasses.replace(self.z, name="A") - @dataclasses.dataclass - class Z: - name: str - """) - - def test_replace_as_method_with_kwargs(self): - # This is a weird case where replace is added as a method, then called - # with kwargs. This makes pytype unable to see that `self` is the object - # being modified, and also caused a crash when the dataclass overlay tries - # to unpack the object being modified from the args. - self.Check(""" - import dataclasses - @dataclasses.dataclass - class WithKwargs: - replace = dataclasses.replace - def do(self, **kwargs): - return self.replace(**kwargs) - """) - - def test_replace_subclass(self): - self.CheckWithErrors(""" - import dataclasses - @dataclasses.dataclass - class Base: - name: str - @dataclasses.dataclass - class Sub(Base): - index: int - a = Sub(name="a", index=0) - dataclasses.replace(a, name="b", index=2) - dataclasses.replace(a, name="c", idx=3) # wrong-keyword-args - """) + x: int + _x: Annotated[int, 'property'] + def __init__(self, x: int) -> None: ... + """)]): + self.Check(""" + import dataclasses + import foo + dataclasses.replace(foo.A(1), x=2) + """) if __name__ == "__main__": diff --git a/pytype/tests/test_decorators2.py b/pytype/tests/test_decorators2.py index 52c909c72..d06b9708f 100644 --- a/pytype/tests/test_decorators2.py +++ b/pytype/tests/test_decorators2.py @@ -235,9 +235,9 @@ class Decorate: def __call__(self, func): return func class Foo: - @classmethod - @Decorate # forgot to instantiate Decorate - def bar(cls): # wrong-arg-count[e] # not-callable + @classmethod # not-callable>=3.11 + @Decorate # forgot to instantiate Decorate # wrong-arg-count[e]>=3.11 + def bar(cls): # wrong-arg-count[e]<3.11 # not-callable<3.11 pass Foo.bar() """) @@ -248,9 +248,9 @@ def test_uncallable_instance_as_decorator(self): class Decorate: pass # forgot to define __call__ class Foo: - @classmethod - @Decorate # forgot to instantiate Decorate - def bar(cls): # wrong-arg-count[e1] # not-callable + @classmethod # not-callable>=3.11 + @Decorate # forgot to instantiate Decorate # wrong-arg-count[e1]>=3.11 + def bar(cls): # wrong-arg-count[e1]<3.11 # not-callable<3.11 pass Foo.bar() """) diff --git a/pytype/tests/test_final.py b/pytype/tests/test_final.py index cdf1ca1a6..99b04be94 100644 --- a/pytype/tests/test_final.py +++ b/pytype/tests/test_final.py @@ -160,8 +160,8 @@ def f(self): def test_invalid(self): err = self.CheckWithErrors(""" from typing_extensions import final - @final - def f(x): # final-error[e] + @final # final-error[e]>=3.11 + def f(x): # final-error[e]<3.11 pass """) self.assertErrorSequences(err, {"e": ["Cannot apply @final", "f"]}) diff --git a/pytype/tests/test_methods1.py b/pytype/tests/test_methods1.py index b47c3a1ad..b33753445 100644 --- a/pytype/tests/test_methods1.py +++ b/pytype/tests/test_methods1.py @@ -730,9 +730,9 @@ def test_invalid_classmethod(self): def f(x): return 42 class A: - @classmethod + @classmethod # not-callable[e]>=3.11 @f - def myclassmethod(*args): # not-callable[e] + def myclassmethod(*args): # not-callable[e]<3.11 return 3 """) self.assertTypesMatchPytd(ty, """ diff --git a/pytype/tests/test_paramspec.py b/pytype/tests/test_paramspec.py index f7abd5c79..03dcaf0c3 100644 --- a/pytype/tests/test_paramspec.py +++ b/pytype/tests/test_paramspec.py @@ -345,8 +345,8 @@ def g(a: int, b: str) -> int: def h(a: int, b: str) -> int: return 10 - @foo.mismatched - def k(a: int, b: str) -> int: # wrong-arg-types[e] + @foo.mismatched # wrong-arg-types[e]>=3.11 + def k(a: int, b: str) -> int: # wrong-arg-types[e]<3.11 return 10 """) self.assertTypesMatchPytd(ty, """ diff --git a/pytype/vm.py b/pytype/vm.py index 20b7044fc..2040ac49b 100644 --- a/pytype/vm.py +++ b/pytype/vm.py @@ -433,39 +433,11 @@ def simple_stack(self, opcode=None): else: return () - def _in_3_11_decoration(self): - """Are we in a Python 3.11 decorator call?""" - if not (self.ctx.python_version == (3, 11) and - isinstance(self.current_opcode, opcodes.CALL) and - self.current_line in self._director.decorated_functions): - return False - prev = self.current_opcode - # Skip past the PRECALL opcode. - for _ in range(2): - prev = prev.prev - if not prev: - return False - # `prev` is the last loaded argument. For this call to be a decorator call, - # the last argument must be the decorated object or another decorator. - return (prev.line != self.current_line and - any(prev.line in d for d in (self._director.decorators, - self._director.decorated_functions))) - def stack(self, func=None): """Get a frame stack for the given function for error reporting.""" if (isinstance(func, abstract.INTERPRETER_FUNCTION_TYPES) and not self.current_opcode): return self.simple_stack(func.get_first_opcode()) - elif self._in_3_11_decoration(): - # TODO(b/241431224): In Python 3.10, the line number of the CALL opcode - # for a decorator is at the function definition line, while in 3.11, the - # opcode is at the decorator line. For a smoother transition from 3.10 to - # 3.11, we adjust the error line number for a bad decorator call to match - # 3.10. The 3.11 line number is more sensible, so we should stop making - # this adjustment once we're out of the transition period. - adjusted_opcode = self.current_opcode.at_line( - self._director.decorated_functions[self.current_line]) - return self.simple_stack(adjusted_opcode) else: return self.frames