From d2e8b60f8e61634f7bc92f16e89283260ea98620 Mon Sep 17 00:00:00 2001 From: Morten Kristensen Date: Mon, 26 Jun 2023 22:43:16 +0200 Subject: [PATCH] [3.12] Detect type alias statement ``` type X = SomeType ``` Ref: https://peps.python.org/pep-0695/ --- README.rst | 6 +++--- tests/comment_exclusions.py | 6 ++++++ tests/lang.py | 35 +++++++++++++++++++++++++++++++++++ vermin/parser.py | 12 ++++++++++++ vermin/source_state.py | 3 +++ vermin/source_visitor.py | 14 ++++++++++++++ 6 files changed, 73 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 386f45ad..2c438d11 100644 --- a/README.rst +++ b/README.rst @@ -147,9 +147,9 @@ bytearray, ``with`` statement, asynchronous ``with`` statement, multiple context unpacking assignment, generalized unpacking, ellipsis literal (``...``) out of slices, dictionary union (``{..} | {..}``), dictionary union merge (``a = {..}; a |= {..}``), builtin generic type annotations (``list[str]``), function decorators, class decorators, relaxed decorators, -``metaclass`` class keyword, pattern matching with ``match``, and union types written as ``X | Y``. -It tries to detect and ignore user-defined functions, classes, arguments, and variables with names -that clash with library-defined symbols. +``metaclass`` class keyword, pattern matching with ``match``, union types written as ``X | Y``, and +type alias statements (``type X = SomeType``). It tries to detect and ignore user-defined functions, +classes, arguments, and variables with names that clash with library-defined symbols. Caveats ======= diff --git a/tests/comment_exclusions.py b/tests/comment_exclusions.py index e52b651d..df615f03 100644 --- a/tests/comment_exclusions.py +++ b/tests/comment_exclusions.py @@ -501,3 +501,9 @@ def bar(): """) self.assertIn(5, visitor.no_lines()) self.assertEqual([(0, 0), (0, 0)], visitor.minimum_versions()) + + @VerminTest.skipUnlessVersion(3, 12) + def test_type_alias_statement(self): + visitor = self.visit("type X = int #novm") + self.assertIn(1, visitor.no_lines()) + self.assertEqual([(0, 0), (0, 0)], visitor.minimum_versions()) diff --git a/tests/lang.py b/tests/lang.py index a37bf2c0..dc5b76c4 100644 --- a/tests/lang.py +++ b/tests/lang.py @@ -1043,6 +1043,41 @@ def test_ellipsis_out_of_slices(self): self.assertTrue(visitor.ellipsis_out_of_slices()) self.assertOnlyIn((3, 0), visitor.minimum_versions()) + @VerminTest.skipUnlessVersion(3, 12) + def test_type_alias_statement(self): + visitor = self.visit('type X = int') + self.assertTrue(visitor.type_alias_statement()) + self.assertOnlyIn((3, 12), visitor.minimum_versions()) + + visitor = self.visit('type Point = tuple[float, float]') + self.assertTrue(visitor.type_alias_statement()) + self.assertOnlyIn((3, 12), visitor.minimum_versions()) + + visitor = self.visit('type(X)') + self.assertFalse(visitor.type_alias_statement()) + + # Plain syntax errors. + visitor = self.visit('type "X = int"') + self.assertEqual([(0, 0), (0, 0)], visitor) + + visitor = self.visit('type "Point = tuple[float, float]"') + self.assertEqual([(0, 0), (0, 0)], visitor) + + visitor = self.visit('type "Po_int = tuple[float, float]"') + self.assertEqual([(0, 0), (0, 0)], visitor) + + def test_type_alias_statement_invalid_syntax(self): + """Prior to 3.12 it would yield SyntaxError: invalid syntax for type alias statements.""" + cv = current_version() + if cv.major == 3 and cv.minor <= 11: + self.assertOnlyIn((3, 12), self.detect("type X = int")) + self.assertOnlyIn((3, 12), self.detect("type Point = tuple[float, float]")) + + # Plain syntax errors. + self.assertEqual([(0, 0), (0, 0)], self.detect("type 'X = int'")) + self.assertEqual([(0, 0), (0, 0)], self.detect("type 'Point = tuple[float, float]'")) + self.assertEqual([(0, 0), (0, 0)], self.detect("type 'Po_int = tuple[float, float]'")) + @VerminTest.skipUnlessVersion(3, 5) def test_bytes_format(self): visitor = self.visit("b'%x' % 10") diff --git a/vermin/parser.py b/vermin/parser.py index bc64602c..2c61b690 100644 --- a/vermin/parser.py +++ b/vermin/parser.py @@ -1,11 +1,15 @@ import ast import io import sys +import re from tokenize import generate_tokens, COMMENT, NEWLINE, NL, STRING from .printing import vvprint from .utility import version_strings +# 'type' identifier [type_params] "=" expression +TYPE_ALIAS_STMT = re.compile(r"type\s+(\w+)\s+(\[.+?\]\s+)?=\s+(.+)") + class Parser: def __init__(self, source, path=None): self.__source = source @@ -85,6 +89,14 @@ def detect(self, config): format(err.filename, err.lineno, err.offset, versions, text), config) return (None, [(2, 0), None], set()) + # Type alias statements. + # NOTE: This is only triggered with Python 3.11 or older. + if lmsg == "invalid syntax" and TYPE_ALIAS_STMT.match(text) is not None: + versions = "!2:3.12:" if parsable else "" + vvprint("{}:{}:{}:{}info: type alias statement `{}` requires !2, 3.12". + format(err.filename, err.lineno, err.offset, versions, text), config) + return (None, [None, (3, 12)], set()) + min_versions = [(0, 0), (0, 0)] if config.pessimistic(): min_versions[sys.version_info.major - 2] = None diff --git a/vermin/source_state.py b/vermin/source_state.py index f0343b0a..419c4927 100644 --- a/vermin/source_state.py +++ b/vermin/source_state.py @@ -105,6 +105,9 @@ def __init__(self, config, path=None, source=None): self.except_star = False self.metaclass_class_keyword = False + # `type X = SomeType`. + self.type_alias_statement = False + # Imported members of modules, like "exc_clear" of "sys". self.import_mem_mod = {} diff --git a/vermin/source_visitor.py b/vermin/source_visitor.py index 69cced4d..20ef2ef0 100644 --- a/vermin/source_visitor.py +++ b/vermin/source_visitor.py @@ -210,6 +210,9 @@ def unpacking_assignment(self): def ellipsis_out_of_slices(self): return self.__s.ellipsis_out_of_slices + def type_alias_statement(self): + return self.__s.type_alias_statement + def bytes_format(self): return self.__s.bytes_format @@ -443,6 +446,10 @@ def minimum_versions(self): mins = self.__add_versions_entity(mins, (None, (3, 0)), "ellipsis literal (`...`) out of slices", plural=False) + if self.type_alias_statement(): + mins = self.__add_versions_entity(mins, (None, (3, 12)), + "type alias statement (`type X = SomeType`)", plural=False) + if self.bytes_format(): # Since byte strings are a `str` synonym as of 2.6+, and thus also supports `%` formatting, # (2, 6) is returned instead of None. @@ -2174,6 +2181,13 @@ def visit_Ellipsis(self, node): self.__vvprint("ellipsis literal (`...`) out of slices", versions=[None, (3, 0)], plural=False) + def visit_TypeAlias(self, node): + if not self.__is_no_line(node.lineno): + self.__s.type_alias_statement = True + self.__vvprint("type alias statement (`type X = SomeType`)", versions=[None, (3, 12)], + plural=False) + self.generic_visit(node) + # Ignore unused nodes as a speed optimization. def visit_alias(self, node):