Skip to content

Commit

Permalink
Merge pull request #1567 from google/google_sync
Browse files Browse the repository at this point in the history
Google sync
  • Loading branch information
rchen152 authored Jan 11, 2024
2 parents 8f05bff + 24a62a7 commit 2e30cee
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 107 deletions.
12 changes: 8 additions & 4 deletions pytype/abstract/_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions pytype/tests/test_abc2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 4 additions & 6 deletions pytype/tests/test_attr1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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])

Expand Down
12 changes: 4 additions & 8 deletions pytype/tests/test_attr2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
""")

Expand Down Expand Up @@ -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
""")
Expand Down
112 changes: 65 additions & 47 deletions pytype/tests/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
""")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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__":
Expand Down
12 changes: 6 additions & 6 deletions pytype/tests/test_decorators2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
""")
Expand All @@ -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()
""")
Expand Down
4 changes: 2 additions & 2 deletions pytype/tests/test_final.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]})
Expand Down
4 changes: 2 additions & 2 deletions pytype/tests/test_methods1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, """
Expand Down
4 changes: 2 additions & 2 deletions pytype/tests/test_paramspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, """
Expand Down
28 changes: 0 additions & 28 deletions pytype/vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 2e30cee

Please sign in to comment.