Skip to content

Commit

Permalink
Merge pull request #176 from netromdk/union-types-feature
Browse files Browse the repository at this point in the history
  • Loading branch information
netromdk committed Apr 10, 2023
2 parents dc912c9 + 7cd3b5d commit c661ded
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 3 deletions.
6 changes: 6 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ distinguish ``f'{a=}'`` from ``f'a={a}'``, for instance, since it optimizes some
code as using fstring self-doc when only using general fstring. To enable (unstable) fstring
self-doc detection, use ``--feature fstring-self-doc``.

Detecting union types (``X | Y`` `PEP 604 <https://www.python.org/dev/peps/pep-0604/>`__) can be
tricky because Vermin doesn't know all underlying details of constants and types since it parses and
traverses the AST. For this reason, heuristics are employed and this can sometimes yield incorrect
results (`#103 <https://github.com/netromdk/vermin/issues/103>`__). To enable (unstable) union types
detection, use ``--feature union-types``.

Function and variable annotations aren't evaluated at definition time when ``from __future__ import
annotations`` is used (`PEP 563 <https://www.python.org/dev/peps/pep-0563/>`__). This is why
``--no-eval-annotations`` is on by default (since v1.1.1, `#66
Expand Down
79 changes: 78 additions & 1 deletion tests/lang.py
Original file line number Diff line number Diff line change
Expand Up @@ -1851,7 +1851,8 @@ def http_error(status):
""")
self.assertTrue(visitor.pattern_matching())

def test_union_types(self):
def test_union_types_enabled(self):
self.config.enable_feature("union-types")
visitor = self.visit("int | float")
self.assertTrue(visitor.union_types())
self.assertOnlyIn((3, 10), visitor.minimum_versions())
Expand Down Expand Up @@ -2097,6 +2098,82 @@ def foo(n: int | None):
self.assertFalse(visitor.maybe_annotations())
self.assertOnlyIn((3, 10), visitor.minimum_versions())

def test_union_types_disabled(self):
self.assertFalse(self.config.has_feature("union-types"))

# --- Normally true but false because the feature is disabled. ---

visitor = self.visit("int | float")
self.assertFalse(visitor.union_types())

visitor = self.visit('isinstance("", int | str)')
self.assertFalse(visitor.union_types())

visitor = self.visit("issubclass(bool, int | float)")
self.assertFalse(visitor.union_types())

visitor = self.visit("""
a = str
a |= int
""")
self.assertFalse(visitor.union_types())

visitor = self.visit("""
class Foo: pass
class Bar: pass
a = Foo | Bar
""")
self.assertFalse(visitor.union_types())

if current_version() >= (3, 10):
# Issue 159: Attributes can also be used for union types.
visitor = self.visit("""
def function(argument: ipaddress.IPv4Interface | ipaddress.IPv6Interface):
if isinstance(argument, ipaddress.IPv4Address):
print("We got an IPv4")
elif isinstance(argument, ipaddress.IPv6Address):
print("We got an IPv6")
else:
print(f"We got type {type(argument)}")
""")
self.assertFalse(visitor.union_types())

# --- False in all cases, disabled or not. ---

visitor = self.visit("a = 1 | 0")
self.assertFalse(visitor.union_types())

visitor = self.visit("""
a = {'x':1}
b = {'y':2}
a | b
""")
self.assertFalse(visitor.union_types())

visitor = self.visit("""
a = str
a |= "test"
""")
self.assertFalse(visitor.union_types())

visitor = self.visit("""
a = "test"
a |= int
""")
self.assertFalse(visitor.union_types())

# Issue 103: Don't judge `|` operator applied to values, and not types, as a union of types.
visitor = self.visit("""
index = 0
c_bitmap = [42]
exec_result = [42]
global_byte = c_bitmap[index]
local_byte = exec_result[index]
if (global_byte | local_byte) != global_byte:
pass
""")
self.assertFalse(visitor.union_types())

def test_super_no_args(self):
# Without arguments, it's v3.0.
visitor = self.visit("""
Expand Down
4 changes: 4 additions & 0 deletions vermin/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
"[Unstable] Detect self-documenting fstrings. Can in",
"some cases wrongly report fstrings as self-documenting."
]),
("union-types", [
"[Unstable] Detect union types `X | Y`. Can in some cases",
"wrongly report union types due to having to employ heuristics."
]),
)

class Features:
Expand Down
8 changes: 6 additions & 2 deletions vermin/source_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ def __init__(self, config, path=None, source=None):
# incorrectly marks some source code as using fstring self-doc when only using general fstring.
self.__fstring_self_doc_enabled = self.__config.has_feature("fstring-self-doc")

# Default to disabling union types detection because it sometimes fails to report it correctly
# due to using heuristics.
self.__union_types_enabled = self.__config.has_feature("union-types")

# Used for incompatible versions texts.
self.__info_versions = {}

Expand Down Expand Up @@ -1396,7 +1400,7 @@ def is_none_or_name(node):
return name is not None and \
((name not in self.__name_res and name not in self.__user_defs) or
(name in self.__name_res_type))
if is_none_or_name(node.left) and is_none_or_name(node.right):
if self.__union_types_enabled and is_none_or_name(node.left) and is_none_or_name(node.right):
self.__union_types = True
self.__vvprint("union types as `X | Y`", line=node.lineno, versions=[None, (3, 10)],
plural=True)
Expand Down Expand Up @@ -1710,7 +1714,7 @@ def is_union_type(node):
# AugAssign(target=Name(id='a', ctx=Store()),
# op=BitOr(),
# value=Name(id='int', ctx=Load()))
elif is_union_type(node.target) and is_union_type(node.value):
elif self.__union_types_enabled and is_union_type(node.target) and is_union_type(node.value):
self.__union_types = True
self.__vvprint("union types as `a = X; a |= Y`", line=node.lineno, versions=[None, (3, 10)],
plural=True)
Expand Down

0 comments on commit c661ded

Please sign in to comment.