Skip to content

Commit

Permalink
Stop doing suboptimal line number adjustment for 3.10/3.11 consistency.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 596415523
  • Loading branch information
rchen152 committed Jan 7, 2024
1 parent a7053db commit b17845a
Show file tree
Hide file tree
Showing 15 changed files with 69 additions and 102 deletions.
11 changes: 6 additions & 5 deletions pytype/imports/module_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class _PathFinder:
def __init__(self, options: config.Options):
self.options = options

def find_import(self, module_name: str) -> Tuple[Optional[str], bool]:
def find_import(self, module_name: str) -> Optional[Tuple[str, bool]]:
"""Search through pythonpath for a module.
Loops over self.options.pythonpath, taking care of the semantics for
Expand All @@ -34,7 +34,7 @@ def find_import(self, module_name: str) -> Tuple[Optional[str], bool]:
Returns:
- (path, file_exists) if we find a path (file_exists will be false if we
have found a directory where we need to create an __init__.pyi)
- (None, None) if we cannot find a full path
- None if we cannot find a full path
"""
module_name_split = module_name.split(".")
for searchdir in self.options.pythonpath:
Expand All @@ -58,7 +58,7 @@ def find_import(self, module_name: str) -> Tuple[Optional[str], bool]:
if full_path is not None:
log.debug("Found module %r in path %r", module_name, path)
return full_path, True
return None, None
return None

def get_pyi_path(self, path: str) -> Optional[str]:
"""Get a pyi file from path if it exists."""
Expand Down Expand Up @@ -87,9 +87,10 @@ def __init__(self, options: config.Options):

def find_import(self, module_name: str) -> Optional[base.ModuleInfo]:
"""See if the loader can find a file to import for the module."""
full_path, file_exists = self._path_finder.find_import(module_name)
if full_path is None:
found_import = self._path_finder.find_import(module_name)
if found_import is None:
return None
full_path, file_exists = found_import
return base.ModuleInfo(module_name, full_path, file_exists)

def _load_pyi(self, mod_info: base.ModuleInfo):
Expand Down
2 changes: 1 addition & 1 deletion pytype/stubs/builtins/typing.pytd
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ class IO(Iterator[AnyStr]):
closed = ... # type: bool
newlines = ... # type: Union[None, str, Tuple[str,...]]
def __enter__(self: _T_IO) -> _T_IO: ...
def __exit__(self, t, value, traceback) -> bool: ...
def __exit__(self, t, value, traceback) -> None: ...
def __iter__(self) -> Iterator[AnyStr]: ...
def __next__(self) -> AnyStr: ...
def close(self) -> None: ...
Expand Down
1 change: 1 addition & 0 deletions pytype/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ py_library(
pytype.libvm
pytype.utils
pytype.platform_utils.platform_utils
pytype.pytd.pytd
)

py_test(
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
13 changes: 3 additions & 10 deletions pytype/tests/test_coroutine.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,14 +370,7 @@ def log(s: _T0) -> Coroutine[Any, Any, _T0]: ...
""")

def test_async_with_error(self):
# pylint: disable=anomalous-backslash-in-string
if self.python_version >= (3, 10):
e4 = " # bad-return-type[e4]"
e4_pre310 = ""
else:
e4 = ""
e4_pre310 = " # bad-return-type[e4]"
errors = self.CheckWithErrors(f"""
errors = self.CheckWithErrors("""
class AsyncCtx1:
pass
Expand All @@ -392,8 +385,8 @@ async def caller():
ctx1 = AsyncCtx1()
ctx2 = AsyncCtx2()
async with ctx1 as var1: # attribute-error[e1] # attribute-error[e2]
async with ctx2 as var2: # bad-return-type[e3]{e4}
pass{e4_pre310}
async with ctx2 as var2: # bad-return-type[e3] # bad-return-type[e4]>=3.10
pass # bad-return-type[e4]<3.10
""")
self.assertErrorRegexes(errors, {
"e1": r"No attribute.*__aexit__", "e2": r"No attribute.*__aenter__",
Expand Down
10 changes: 4 additions & 6 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
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
14 changes: 3 additions & 11 deletions pytype/tests/test_type_comments1.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Tests for type comments."""

import sys

from pytype.tests import test_base
from pytype.tests import test_utils

Expand Down Expand Up @@ -716,15 +714,9 @@ def test_type_comment_on_class(self):
# What error is reported differs depending on whether directors.py is using
# libcst (host 3.8-) or ast (host 3.9+) and the target version. All we care
# about is that the type comment is not ignored.
if sys.version_info[:2] >= (3, 9):
line1_error = ""
line2_error = " # ignored-type-comment"
else:
line1_error = " # annotation-type-mismatch"
line2_error = ""
self.CheckWithErrors(f"""
class Foo({line1_error}
int): # type: str{line2_error}
self.CheckWithErrors("""
class Foo( # annotation-type-mismatch<3.9
int): # type: str # ignored-type-comment>=3.9
pass
""")

Expand Down
42 changes: 29 additions & 13 deletions pytype/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
from pytype import file_utils
from pytype import load_pytd
from pytype import state as frame_state
from pytype import utils
from pytype.file_utils import makedirs
from pytype.platform_utils import path_utils
from pytype.platform_utils import tempfile as compatible_tempfile
from pytype.pytd import slots

import unittest

Expand Down Expand Up @@ -296,7 +298,8 @@ class ErrorMatcher:
See tests/test_base_test.py for usage examples.
"""

ERROR_RE = re.compile(r"^(?P<code>(\w+-)+\w+)(\[(?P<mark>.+)\])?$")
ERROR_RE = re.compile(r"^(?P<code>(\w+-)+\w+)(\[(?P<mark>.+)\])?"
r"((?P<cmp>([!=]=|[<>]=?))(?P<version>\d+\.\d+))?$")

def __init__(self, src):
# errorlog and marks are set by assert_errors_match_expected()
Expand Down Expand Up @@ -372,24 +375,37 @@ def assert_error_sequences(self, expected_sequences):
matchers = {k: SequenceMatcher(v) for k, v in expected_sequences.items()}
self._assert_error_messages(matchers)

def _parse_comment(self, comment):
comment = comment.strip()
error_match = self.ERROR_RE.fullmatch(comment)
if not error_match:
return None
version_cmp = error_match.group("cmp")
if version_cmp:
version = utils.version_from_string(error_match.group("version"))
actual_version = sys.version_info[:2]
if not slots.COMPARES[version_cmp](actual_version, version):
return None
return error_match.group("code"), error_match.group("mark")

def _parse_comments(self, src):
"""Parse comments."""
src = io.StringIO(src)
expected = collections.defaultdict(list)
used_marks = set()
for tok, s, (line, _), _, _ in tokenize.generate_tokens(src.readline):
if tok == tokenize.COMMENT:
for comment in s.split("#"):
comment = comment.strip()
match = self.ERROR_RE.match(comment)
if not match:
continue
mark = match.group("mark")
if mark:
if mark in used_marks:
self._fail(f"Mark {mark} already used")
used_marks.add(mark)
expected[line].append((match.group("code"), mark))
if tok != tokenize.COMMENT:
continue
for comment in s.split("#"):
parsed_comment = self._parse_comment(comment)
if parsed_comment is None:
continue
code, mark = parsed_comment
if mark:
if mark in used_marks:
self._fail(f"Mark {mark} already used")
used_marks.add(mark)
expected[line].append((code, mark))
return expected


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 b17845a

Please sign in to comment.