From 0e97e3c770e11aeee8ffb6a72e74aa64b82f2df6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 27 Jan 2023 18:39:05 +0100 Subject: [PATCH 1/5] Add support for binary union types - Python 3.10 --- ChangeLog | 3 ++ astroid/bases.py | 38 +++++++++++++- astroid/inference.py | 25 +++++++++ astroid/raw_building.py | 18 +++++++ tests/unittest_inference.py | 100 +++++++++++++++++++++++++++++++++++- 5 files changed, 180 insertions(+), 4 deletions(-) diff --git a/ChangeLog b/ChangeLog index 9e80772bd3..7c51da1ca5 100644 --- a/ChangeLog +++ b/ChangeLog @@ -14,6 +14,9 @@ Release date: TBA * Fix issues with ``typing_extensions.TypeVar``. +* Add support for inferring binary union types added in Python 3.10. + + Refs PyCQA/pylint#8119 What's New in astroid 2.13.3? diff --git a/astroid/bases.py b/astroid/bases.py index d6c830c7ff..c947a20440 100644 --- a/astroid/bases.py +++ b/astroid/bases.py @@ -121,11 +121,12 @@ def __init__( if proxied is None: # This is a hack to allow calling this __init__ during bootstrapping of # builtin classes and their docstrings. - # For Const and Generator nodes the _proxied attribute is set during bootstrapping + # For Const, Generator, and UnionType nodes the _proxied attribute + # is set during bootstrapping # as we first need to build the ClassDef that they can proxy. # Thus, if proxied is None self should be a Const or Generator # as that is the only way _proxied will be correctly set as a ClassDef. - assert isinstance(self, (nodes.Const, Generator)) + assert isinstance(self, (nodes.Const, Generator, UnionType)) else: self._proxied = proxied @@ -669,3 +670,36 @@ def __repr__(self) -> str: def __str__(self) -> str: return f"AsyncGenerator({self._proxied.name})" + + +class UnionType(BaseInstance): + """Special node representing new style typing unions. + + Proxied class is set once for all in raw_building. + """ + + _proxied: nodes.ClassDef + + def __init__(self, left, right, parent=None): + super().__init__() + self.parent = parent + self.left = left + self.right = right + + def callable(self) -> Literal[False]: + return False + + def pytype(self) -> Literal["types.UnionType"]: + return "types.UnionType" + + def display_type(self) -> str: + return "UnionType" + + def bool_value(self, context: InferenceContext | None = None) -> Literal[True]: + return True + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return f"UnionType({self._proxied.name})" diff --git a/astroid/inference.py b/astroid/inference.py index e8fec289fa..dd0a48720f 100644 --- a/astroid/inference.py +++ b/astroid/inference.py @@ -15,6 +15,7 @@ from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union from astroid import bases, constraint, decorators, helpers, nodes, protocols, util +from astroid.const import PY310_PLUS from astroid.context import ( CallContext, InferenceContext, @@ -755,6 +756,14 @@ def _bin_op( ) +def _bin_op_or_union_type( + left: bases.UnionType | nodes.ClassDef | nodes.Const, + right: bases.UnionType | nodes.ClassDef | nodes.Const, +) -> Generator[InferenceResult, None, None]: + """Create a new UnionType instance for binary or, e.g. int | str.""" + yield bases.UnionType(left, right) + + def _get_binop_contexts(context, left, right): """Get contexts for binary operations. @@ -814,6 +823,22 @@ def _get_binop_flow( _bin_op(left, binary_opnode, op, right, context), _bin_op(right, binary_opnode, op, left, reverse_context, reverse=True), ] + + if ( + PY310_PLUS + and op == "|" + and ( + isinstance(left, (bases.UnionType, nodes.ClassDef)) + or isinstance(left, nodes.Const) + and left.value is None + ) + and ( + isinstance(right, (bases.UnionType, nodes.ClassDef)) + or isinstance(right, nodes.Const) + and right.value is None + ) + ): + methods.extend([functools.partial(_bin_op_or_union_type, left, right)]) return methods diff --git a/astroid/raw_building.py b/astroid/raw_building.py index cc3aa01525..8475519c4c 100644 --- a/astroid/raw_building.py +++ b/astroid/raw_building.py @@ -550,6 +550,24 @@ def _astroid_bootstrapping() -> None: ) bases.AsyncGenerator._proxied = _AsyncGeneratorType builder.object_build(bases.AsyncGenerator._proxied, types.AsyncGeneratorType) + + if hasattr(types, "UnionType"): + _UnionTypeType = nodes.ClassDef(types.UnionType.__name__) + _UnionTypeType.parent = astroid_builtin + union_type_doc_node = ( + nodes.Const(value=types.UnionType.__doc__) + if types.UnionType.__doc__ + else None + ) + _UnionTypeType.postinit( + bases=[], + body=[], + decorators=None, + doc_node=union_type_doc_node, + ) + bases.UnionType._proxied = _UnionTypeType + builder.object_build(bases.UnionType._proxied, types.UnionType) + builtin_types = ( types.GetSetDescriptorType, types.GeneratorType, diff --git a/tests/unittest_inference.py b/tests/unittest_inference.py index 1351457077..6d743a66f7 100644 --- a/tests/unittest_inference.py +++ b/tests/unittest_inference.py @@ -21,9 +21,9 @@ from astroid import decorators as decoratorsmod from astroid import helpers, nodes, objects, test_utils, util from astroid.arguments import CallSite -from astroid.bases import BoundMethod, Instance, UnboundMethod +from astroid.bases import BoundMethod, Instance, UnboundMethod, UnionType from astroid.builder import AstroidBuilder, _extract_single_node, extract_node, parse -from astroid.const import PY38_PLUS, PY39_PLUS +from astroid.const import PY38_PLUS, PY39_PLUS, PY310_PLUS from astroid.context import InferenceContext from astroid.exceptions import ( AstroidTypeError, @@ -1209,6 +1209,102 @@ def randint(maximum): ], ) + def test_binary_op_or_union_type(self) -> None: + """Binary or union is only defined for Python 3.10+.""" + code = """ + class A: ... + + int | 2 #@ + int | "Hello" #@ + int | ... #@ + int | A() #@ + int | None | 2 #@ + """ + ast_nodes = extract_node(code) + for n in ast_nodes: + assert n.inferred() == [util.Uninferable] + + code = """ + class A: ... + class B: ... + + int | None #@ + int | str #@ + int | str | None #@ + A | B #@ + A | None #@ + list[int] | int #@ + """ + ast_nodes = extract_node(code) + if not PY310_PLUS: + for n in ast_nodes: + assert n.inferred() == [util.Uninferable] + else: + i0 = ast_nodes[0].inferred()[0] + assert isinstance(i0, UnionType) + assert isinstance(i0.left, nodes.ClassDef) + assert i0.left.name == "int" + assert isinstance(i0.right, nodes.Const) + assert i0.right.value is None + + i1 = ast_nodes[1].inferred()[0] + assert isinstance(i1, UnionType) + + i2 = ast_nodes[2].inferred()[0] + assert isinstance(i2, UnionType) + assert isinstance(i2.left, UnionType) + assert isinstance(i2.left.left, nodes.ClassDef) + assert i2.left.left.name == "int" + assert isinstance(i2.left.right, nodes.ClassDef) + assert i2.left.right.name == "str" + assert isinstance(i2.right, nodes.Const) + assert i2.right.value is None + + i3 = ast_nodes[3].inferred()[0] + assert isinstance(i3, UnionType) + assert isinstance(i3.left, nodes.ClassDef) + assert i3.left.name == "A" + assert isinstance(i3.right, nodes.ClassDef) + assert i3.right.name == "B" + + i4 = ast_nodes[4].inferred()[0] + assert isinstance(i4, UnionType) + + i5 = ast_nodes[5].inferred()[0] + assert isinstance(i5, UnionType) + assert isinstance(i5.left, nodes.ClassDef) + assert i5.left.name == "list" + + code = """ + Alias1 = list[int] + Alias2 = str | int + + Alias1 | int #@ + Alias2 | int #@ + Alias1 | Alias2 #@ + """ + ast_nodes = extract_node(code) + if not PY310_PLUS: + for n in ast_nodes: + assert n.inferred() == [util.Uninferable] + else: + i0 = ast_nodes[0].inferred()[0] + assert isinstance(i0, UnionType) + assert isinstance(i0.left, nodes.ClassDef) + assert i0.left.name == "list" + + i1 = ast_nodes[1].inferred()[0] + assert isinstance(i1, UnionType) + assert isinstance(i1.left, UnionType) + assert isinstance(i1.left.left, nodes.ClassDef) + assert i1.left.left.name == "str" + + i2 = ast_nodes[2].inferred()[0] + assert isinstance(i2, UnionType) + assert isinstance(i2.left, nodes.ClassDef) + assert i2.left.name == "list" + assert isinstance(i2.right, UnionType) + def test_nonregr_lambda_arg(self) -> None: code = """ def f(g = lambda: None): From a30fe85b246fad841febdf452a3ea94ac0bc9c35 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 27 Jan 2023 19:26:46 +0100 Subject: [PATCH 2/5] Fix tests --- tests/unittest_inference.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/unittest_inference.py b/tests/unittest_inference.py index 6d743a66f7..0d8c6898c9 100644 --- a/tests/unittest_inference.py +++ b/tests/unittest_inference.py @@ -1225,6 +1225,8 @@ class A: ... assert n.inferred() == [util.Uninferable] code = """ + from typing import List + class A: ... class B: ... @@ -1233,7 +1235,8 @@ class B: ... int | str | None #@ A | B #@ A | None #@ - list[int] | int #@ + List[int] | int #@ + tuple | int #@ """ ast_nodes = extract_node(code) if not PY310_PLUS: @@ -1273,10 +1276,17 @@ class B: ... i5 = ast_nodes[5].inferred()[0] assert isinstance(i5, UnionType) assert isinstance(i5.left, nodes.ClassDef) - assert i5.left.name == "list" + assert i5.left.name == "List" + + i6 = ast_nodes[6].inferred()[0] + assert isinstance(i6, UnionType) + assert isinstance(i6.left, nodes.ClassDef) + assert i6.left.name == "tuple" code = """ - Alias1 = list[int] + from typing import List + + Alias1 = List[int] Alias2 = str | int Alias1 | int #@ @@ -1291,7 +1301,7 @@ class B: ... i0 = ast_nodes[0].inferred()[0] assert isinstance(i0, UnionType) assert isinstance(i0.left, nodes.ClassDef) - assert i0.left.name == "list" + assert i0.left.name == "List" i1 = ast_nodes[1].inferred()[0] assert isinstance(i1, UnionType) @@ -1302,7 +1312,7 @@ class B: ... i2 = ast_nodes[2].inferred()[0] assert isinstance(i2, UnionType) assert isinstance(i2.left, nodes.ClassDef) - assert i2.left.name == "list" + assert i2.left.name == "List" assert isinstance(i2.right, UnionType) def test_nonregr_lambda_arg(self) -> None: From 6a1abda354186f205874da0d6c9e51ddfb00d4ea Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 27 Jan 2023 22:03:41 +0100 Subject: [PATCH 3/5] Move changelog entry --- ChangeLog | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 7c51da1ca5..e7dbcfb279 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,9 @@ What's New in astroid 2.14.0? ============================= Release date: TBA +* Add support for inferring binary union types added in Python 3.10. + + Refs PyCQA/pylint#8119 What's New in astroid 2.13.4? @@ -14,9 +17,6 @@ Release date: TBA * Fix issues with ``typing_extensions.TypeVar``. -* Add support for inferring binary union types added in Python 3.10. - - Refs PyCQA/pylint#8119 What's New in astroid 2.13.3? From eb11fd99d9212e607d5078e3bccf1ee2799222c7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 29 Jan 2023 15:31:34 +0100 Subject: [PATCH 4/5] Add some more type annotations --- astroid/bases.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/astroid/bases.py b/astroid/bases.py index c947a20440..451d462ff8 100644 --- a/astroid/bases.py +++ b/astroid/bases.py @@ -680,7 +680,12 @@ class UnionType(BaseInstance): _proxied: nodes.ClassDef - def __init__(self, left, right, parent=None): + def __init__( + self, + left: UnionType | nodes.ClassDef | nodes.Const, + right: UnionType | nodes.ClassDef | nodes.Const, + parent: nodes.NodeNG | None = None, + ) -> None: super().__init__() self.parent = parent self.left = left From 6e575dab21b5cb50155d325326966e010dc55c64 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 30 Jan 2023 01:58:48 +0100 Subject: [PATCH 5/5] Add additional test coverage --- astroid/bases.py | 6 +++--- tests/unittest_inference.py | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/astroid/bases.py b/astroid/bases.py index 451d462ff8..e930328eda 100644 --- a/astroid/bases.py +++ b/astroid/bases.py @@ -694,15 +694,15 @@ def __init__( def callable(self) -> Literal[False]: return False + def bool_value(self, context: InferenceContext | None = None) -> Literal[True]: + return True + def pytype(self) -> Literal["types.UnionType"]: return "types.UnionType" def display_type(self) -> str: return "UnionType" - def bool_value(self, context: InferenceContext | None = None) -> Literal[True]: - return True - def __repr__(self) -> str: return f"" diff --git a/tests/unittest_inference.py b/tests/unittest_inference.py index d64b6d1fb1..eda8b5f37b 100644 --- a/tests/unittest_inference.py +++ b/tests/unittest_inference.py @@ -1250,6 +1250,14 @@ class B: ... assert isinstance(i0.right, nodes.Const) assert i0.right.value is None + # Assert basic UnionType properties and methods + assert i0.callable() is False + assert i0.bool_value() is True + assert i0.pytype() == "types.UnionType" + assert i0.display_type() == "UnionType" + assert str(i0) == "UnionType(UnionType)" + assert repr(i0) == f"" + i1 = ast_nodes[1].inferred()[0] assert isinstance(i1, UnionType)