Skip to content

Commit

Permalink
[3.12] Detect type alias statement
Browse files Browse the repository at this point in the history
```
type X = SomeType
```

Ref: https://peps.python.org/pep-0695/
  • Loading branch information
netromdk committed Jul 9, 2023
1 parent 8cd3d5f commit d2e8b60
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 3 deletions.
6 changes: 3 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
=======
Expand Down
6 changes: 6 additions & 0 deletions tests/comment_exclusions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
35 changes: 35 additions & 0 deletions tests/lang.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
12 changes: 12 additions & 0 deletions vermin/parser.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions vermin/source_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}

Expand Down
14 changes: 14 additions & 0 deletions vermin/source_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit d2e8b60

Please sign in to comment.