Skip to content

Commit 041b0e3

Browse files
vthemelisagronholm
andauthored
Handle missing branches in BinOp (#392)
Co-authored-by: Alex Grönholm <[email protected]>
1 parent 0c118de commit 041b0e3

3 files changed

Lines changed: 46 additions & 0 deletions

File tree

docs/versionhistory.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ Version history
33

44
This library adheres to `Semantic Versioning 2.0 <https://semver.org/#semantic-versioning-200>`_.
55

6+
**UNRELEASED**
7+
8+
- Fixed ``AttributeError`` where the transformer removed elements from a PEP 604 union
9+
(`#384 <https://github.com/agronholm/typeguard/issues/384>`_)
10+
611
**4.1.3** (2023-08-27)
712

813
- Dropped Python 3.7 support

src/typeguard/_transformer.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,16 @@ def visit_BinOp(self, node: BinOp) -> Any:
375375
self.generic_visit(node)
376376

377377
if isinstance(node.op, BitOr):
378+
# If either branch of the BinOp has been transformed to `None`
379+
# then the `ast.generic_visit` will eliminate that branch completely.
380+
# If this happens, treat the BinOp as just the other branch.
381+
if not hasattr(node, "left") and not hasattr(node, "right"):
382+
return None
383+
elif not hasattr(node, "left"):
384+
return node.right
385+
elif not hasattr(node, "right"):
386+
return node.left
387+
378388
# Return Any if either side is Any
379389
if self._memo.name_matches(node.left, *anytype_names):
380390
return node.left

tests/test_transformer.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1570,6 +1570,37 @@ def foo(x: str) -> None:
15701570
)
15711571

15721572

1573+
def test_union_annotation_with_or_operator() -> None:
1574+
node = parse(
1575+
dedent(
1576+
"""
1577+
from __future__ import annotations
1578+
1579+
class A:
1580+
...
1581+
1582+
def foo(A: A | None) -> None:
1583+
pass
1584+
"""
1585+
)
1586+
)
1587+
TypeguardTransformer(["foo"]).visit(node)
1588+
assert (
1589+
unparse(node)
1590+
== dedent(
1591+
"""
1592+
from __future__ import annotations
1593+
1594+
def foo(A: A | None) -> None:
1595+
from typeguard import TypeCheckMemo
1596+
from typeguard._functions import check_argument_types
1597+
memo = TypeCheckMemo(globals(), locals())
1598+
check_argument_types('foo', {'A': (A, None)}, memo)
1599+
"""
1600+
).strip()
1601+
)
1602+
1603+
15731604
def test_dont_parse_annotated_2nd_arg() -> None:
15741605
# Regression test for #352
15751606
node = parse(

0 commit comments

Comments
 (0)