From 0dbb0d3caca07cdc5701891d4e49ff54ec3040fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 15 Dec 2021 00:10:05 +0100 Subject: [PATCH 1/9] Add typing to ``brain_dataclasses`` --- astroid/brain/brain_dataclasses.py | 40 ++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index 6ea145117a..cefcb9f117 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -119,7 +119,9 @@ def _get_dataclass_attributes(node: ClassDef, init: bool = False) -> Generator: isinstance(value, Call) and _looks_like_dataclass_field_call(value, check_scope=False) and any( - keyword.arg == "init" and not keyword.value.bool_value() + keyword.arg == "init" + and keyword.value + and not keyword.value.bool_value() for keyword in value.keywords ) ): @@ -142,6 +144,9 @@ def _check_generate_dataclass_init(node: ClassDef) -> bool: found = None + # Since this is a dataclass it should have at least one decorator + assert node.decorators + for decorator_attribute in node.decorators.nodes: if not isinstance(decorator_attribute, Call): continue @@ -154,7 +159,7 @@ def _check_generate_dataclass_init(node: ClassDef) -> bool: # Check for keyword arguments of the form init=False return all( - keyword.arg != "init" or keyword.value.bool_value() + keyword.arg != "init" or keyword.value and keyword.value.bool_value() for keyword in found.keywords ) @@ -190,12 +195,12 @@ def _generate_dataclass_init(assigns: List[AnnAssign]) -> str: if isinstance(value, Call) and _looks_like_dataclass_field_call( value, check_scope=False ): - result = _get_field_default(value) - - default_type, default_node = result + default_type, default_node = _get_field_default(value) if default_type == "default": + assert default_node param_str += f" = {default_node.as_string()}" elif default_type == "default_factory": + assert default_node param_str += f" = {DEFAULT_FACTORY}" assignment_str = ( f"self.{name} = {default_node.as_string()} " @@ -214,7 +219,7 @@ def _generate_dataclass_init(assigns: List[AnnAssign]) -> str: def infer_dataclass_attribute( - node: Unknown, ctx: context.InferenceContext = None + node: Unknown, ctx: Optional[context.InferenceContext] = None ) -> Generator: """Inference tip for an Unknown node that was dynamically generated to represent a dataclass attribute. @@ -247,15 +252,17 @@ def infer_dataclass_field_call( if not default_type: yield Uninferable elif default_type == "default": + assert default yield from default.infer(context=ctx) else: + assert default new_call = parse(default.as_string()).body[0].value new_call.parent = field_call.parent yield from new_call.infer(context=ctx) def _looks_like_dataclass_decorator( - node: NodeNG, decorator_names: FrozenSet[str] = DATACLASSES_DECORATORS + node: Optional[NodeNG], decorator_names: FrozenSet[str] = DATACLASSES_DECORATORS ) -> bool: """Return True if node looks like a dataclass decorator. @@ -264,6 +271,10 @@ def _looks_like_dataclass_decorator( """ if isinstance(node, Call): # decorator with arguments node = node.func + + if not node: + return False + try: inferred = next(node.infer()) except (InferenceError, StopIteration): @@ -288,6 +299,9 @@ def _looks_like_dataclass_attribute(node: Unknown) -> bool: """Return True if node was dynamically generated as the child of an AnnAssign statement. """ + if not node.parent: + return False + parent = node.parent scope = parent.scope() return ( @@ -356,8 +370,11 @@ def _get_field_default(field_call: Call) -> Tuple[str, Optional[NodeNG]]: return "", None -def _is_class_var(node: NodeNG) -> bool: +def _is_class_var(node: Optional[NodeNG]) -> bool: """Return True if node is a ClassVar, with or without subscripting.""" + if not node: + return False + if PY39_PLUS: try: inferred = next(node.infer()) @@ -376,8 +393,11 @@ def _is_class_var(node: NodeNG) -> bool: ) -def _is_init_var(node: NodeNG) -> bool: +def _is_init_var(node: Optional[NodeNG]) -> bool: """Return True if node is an InitVar, with or without subscripting.""" + if not node: + return False + try: inferred = next(node.infer()) except (InferenceError, StopIteration): @@ -399,7 +419,7 @@ def _is_init_var(node: NodeNG) -> bool: def _infer_instance_from_annotation( - node: NodeNG, ctx: context.InferenceContext = None + node: NodeNG, ctx: Optional[context.InferenceContext] = None ) -> Generator: """Infer an instance corresponding to the type annotation represented by node. From a62338344a50df0c510b1c8b5b0283fd31ac5cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 15 Dec 2021 10:51:12 +0100 Subject: [PATCH 2/9] Fix mistakes --- astroid/brain/brain_dataclasses.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index cefcb9f117..ab661694ba 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -262,7 +262,7 @@ def infer_dataclass_field_call( def _looks_like_dataclass_decorator( - node: Optional[NodeNG], decorator_names: FrozenSet[str] = DATACLASSES_DECORATORS + node: NodeNG, decorator_names: FrozenSet[str] = DATACLASSES_DECORATORS ) -> bool: """Return True if node looks like a dataclass decorator. @@ -272,9 +272,6 @@ def _looks_like_dataclass_decorator( if isinstance(node, Call): # decorator with arguments node = node.func - if not node: - return False - try: inferred = next(node.infer()) except (InferenceError, StopIteration): @@ -299,10 +296,10 @@ def _looks_like_dataclass_attribute(node: Unknown) -> bool: """Return True if node was dynamically generated as the child of an AnnAssign statement. """ - if not node.parent: + parent = node.parent + if not parent: return False - parent = node.parent scope = parent.scope() return ( isinstance(parent, AnnAssign) From 7e76f842a26266f1d4f7ad86937fa171591c0700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 15 Dec 2021 10:52:20 +0100 Subject: [PATCH 3/9] Update astroid/brain/brain_dataclasses.py --- astroid/brain/brain_dataclasses.py | 1 - 1 file changed, 1 deletion(-) diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index ab661694ba..b34f1aeff1 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -271,7 +271,6 @@ def _looks_like_dataclass_decorator( """ if isinstance(node, Call): # decorator with arguments node = node.func - try: inferred = next(node.infer()) except (InferenceError, StopIteration): From 80f074ed66662ab4acbc2f2d690ded1edeae21f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Mon, 20 Dec 2021 15:37:01 +0100 Subject: [PATCH 4/9] Code suggestion --- astroid/brain/brain_dataclasses.py | 46 ++++++++++++++++-------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index b34f1aeff1..0f12d245e8 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -195,17 +195,17 @@ def _generate_dataclass_init(assigns: List[AnnAssign]) -> str: if isinstance(value, Call) and _looks_like_dataclass_field_call( value, check_scope=False ): - default_type, default_node = _get_field_default(value) - if default_type == "default": - assert default_node - param_str += f" = {default_node.as_string()}" - elif default_type == "default_factory": - assert default_node - param_str += f" = {DEFAULT_FACTORY}" - assignment_str = ( - f"self.{name} = {default_node.as_string()} " - f"if {name} is {DEFAULT_FACTORY} else {name}" - ) + result = _get_field_default(value) + if result: + default_type, default_node = result + if default_type == "default": + param_str += f" = {default_node.as_string()}" + elif default_type == "default_factory": + param_str += f" = {DEFAULT_FACTORY}" + assignment_str = ( + f"self.{name} = {default_node.as_string()} " + f"if {name} is {DEFAULT_FACTORY} else {name}" + ) else: param_str += f" = {value.as_string()}" @@ -248,17 +248,17 @@ def infer_dataclass_field_call( if not isinstance(node.parent, (AnnAssign, Assign)): raise UseInferenceDefault field_call = node.parent.value - default_type, default = _get_field_default(field_call) - if not default_type: + result = _get_field_default(field_call) + if not result: yield Uninferable - elif default_type == "default": - assert default - yield from default.infer(context=ctx) else: - assert default - new_call = parse(default.as_string()).body[0].value - new_call.parent = field_call.parent - yield from new_call.infer(context=ctx) + default_type, default = result + if default_type == "default": + yield from default.infer(context=ctx) + else: + new_call = parse(default.as_string()).body[0].value + new_call.parent = field_call.parent + yield from new_call.infer(context=ctx) def _looks_like_dataclass_decorator( @@ -335,7 +335,9 @@ def _looks_like_dataclass_field_call(node: Call, check_scope: bool = True) -> bo return inferred.name == FIELD_NAME and inferred.root().name in DATACLASS_MODULES -def _get_field_default(field_call: Call) -> Tuple[str, Optional[NodeNG]]: +def _get_field_default( + field_call: Call, +) -> Optional[Tuple[str, NodeNG]]: """Return a the default value of a field call, and the corresponding keyword argument name. field(default=...) results in the ... node @@ -363,7 +365,7 @@ def _get_field_default(field_call: Call) -> Tuple[str, Optional[NodeNG]]: new_call.postinit(func=default_factory) return "default_factory", new_call - return "", None + return None def _is_class_var(node: Optional[NodeNG]) -> bool: From fe8ed31d47819b66511716d499a610f6a5c27e13 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Thu, 23 Dec 2021 19:27:35 +0100 Subject: [PATCH 5/9] Update astroid/brain/brain_dataclasses.py --- astroid/brain/brain_dataclasses.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index 0f12d245e8..dcfabdb83f 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -335,9 +335,7 @@ def _looks_like_dataclass_field_call(node: Call, check_scope: bool = True) -> bo return inferred.name == FIELD_NAME and inferred.root().name in DATACLASS_MODULES -def _get_field_default( - field_call: Call, -) -> Optional[Tuple[str, NodeNG]]: +def _get_field_default(field_call: Call) -> Optional[Tuple[str, NodeNG]]: """Return a the default value of a field call, and the corresponding keyword argument name. field(default=...) results in the ... node From 244ce328d1e8e978956380455ccd8d085f5931c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 29 Dec 2021 11:10:07 +0100 Subject: [PATCH 6/9] Code review --- astroid/brain/brain_dataclasses.py | 44 +++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index 548054aac1..385212d19d 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -10,7 +10,8 @@ - https://lovasoa.github.io/marshmallow_dataclass/ """ -from typing import FrozenSet, Generator, List, Optional, Tuple +import sys +from typing import FrozenSet, Generator, List, Optional, Tuple, Union from astroid import context, inference_tip from astroid.builder import parse @@ -36,6 +37,11 @@ from astroid.nodes.scoped_nodes import ClassDef, FunctionDef from astroid.util import Uninferable +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal + DATACLASSES_DECORATORS = frozenset(("dataclass",)) FIELD_NAME = "field" DATACLASS_MODULES = frozenset( @@ -115,7 +121,7 @@ def _get_dataclass_attributes(node: ClassDef, init: bool = False) -> Generator: ): continue - if _is_class_var(assign_node.annotation): + if _is_class_var(assign_node.annotation): # type: ignore[arg-type] # annotation is never None continue if init: @@ -125,13 +131,12 @@ def _get_dataclass_attributes(node: ClassDef, init: bool = False) -> Generator: and _looks_like_dataclass_field_call(value, check_scope=False) and any( keyword.arg == "init" - and keyword.value - and not keyword.value.bool_value() + and not keyword.value.bool_value() # type: ignore[union-attr] # value is never None for keyword in value.keywords ) ): continue - elif _is_init_var(assign_node.annotation): + elif _is_init_var(assign_node.annotation): # type: ignore[arg-type] # annotation is never None continue yield assign_node @@ -149,9 +154,6 @@ def _check_generate_dataclass_init(node: ClassDef) -> bool: found = None - # Since this is a dataclass it should have at least one decorator - assert node.decorators - for decorator_attribute in node.decorators.nodes: if not isinstance(decorator_attribute, Call): continue @@ -164,7 +166,8 @@ def _check_generate_dataclass_init(node: ClassDef) -> bool: # Check for keyword arguments of the form init=False return all( - keyword.arg != "init" or keyword.value and keyword.value.bool_value() + keyword.arg != "init" + and keyword.value.bool_value() # type: ignore[untion-attr] # value is never None for keyword in found.keywords ) @@ -179,7 +182,7 @@ def _generate_dataclass_init(assigns: List[AnnAssign]) -> str: name, annotation, value = assign.target.name, assign.annotation, assign.value target_names.append(name) - if _is_init_var(annotation): + if _is_init_var(annotation): # type: ignore[arg-type] # annotation is never None init_var = True if isinstance(annotation, Subscript): annotation = annotation.slice @@ -252,8 +255,7 @@ def infer_dataclass_field_call( """Inference tip for dataclass field calls.""" if not isinstance(node.parent, (AnnAssign, Assign)): raise UseInferenceDefault - field_call = node.parent.value - result = _get_field_default(field_call) + result = _get_field_default(node) if not result: yield Uninferable else: @@ -262,7 +264,7 @@ def infer_dataclass_field_call( yield from default.infer(context=ctx) else: new_call = parse(default.as_string()).body[0].value - new_call.parent = field_call.parent + new_call.parent = node.parent yield from new_call.infer(context=ctx) @@ -340,7 +342,11 @@ def _looks_like_dataclass_field_call(node: Call, check_scope: bool = True) -> bo return inferred.name == FIELD_NAME and inferred.root().name in DATACLASS_MODULES -def _get_field_default(field_call: Call) -> Optional[Tuple[str, NodeNG]]: +def _get_field_default( + field_call: Call, +) -> Union[ + None, Tuple[Literal["default"], NodeNG], Tuple[Literal["default_factory"], Call] +]: """Return a the default value of a field call, and the corresponding keyword argument name. field(default=...) results in the ... node @@ -371,11 +377,8 @@ def _get_field_default(field_call: Call) -> Optional[Tuple[str, NodeNG]]: return None -def _is_class_var(node: Optional[NodeNG]) -> bool: +def _is_class_var(node: NodeNG) -> bool: """Return True if node is a ClassVar, with or without subscripting.""" - if not node: - return False - if PY39_PLUS: try: inferred = next(node.infer()) @@ -394,11 +397,8 @@ def _is_class_var(node: Optional[NodeNG]) -> bool: ) -def _is_init_var(node: Optional[NodeNG]) -> bool: +def _is_init_var(node: NodeNG) -> bool: """Return True if node is an InitVar, with or without subscripting.""" - if not node: - return False - try: inferred = next(node.infer()) except (InferenceError, StopIteration): From 0db75b17f524ff9ad05e4c322139b24b13406fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 29 Dec 2021 13:10:52 +0100 Subject: [PATCH 7/9] Update astroid/brain/brain_dataclasses.py Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- astroid/brain/brain_dataclasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index 385212d19d..440cf86633 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -167,7 +167,7 @@ def _check_generate_dataclass_init(node: ClassDef) -> bool: # Check for keyword arguments of the form init=False return all( keyword.arg != "init" - and keyword.value.bool_value() # type: ignore[untion-attr] # value is never None + and keyword.value.bool_value() # type: ignore[union-attr] # value is never None for keyword in found.keywords ) From 46b81c9c1690e60fac2298f6dc36608e69a2225f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 29 Dec 2021 16:22:11 +0100 Subject: [PATCH 8/9] Create alias --- astroid/brain/brain_dataclasses.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index 440cf86633..ad89e9d6ef 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -42,6 +42,10 @@ else: from typing_extensions import Literal +FieldDefaultReturn = Union[ + None, Tuple[Literal["default"], NodeNG], Tuple[Literal["default_factory"], Call] +] + DATACLASSES_DECORATORS = frozenset(("dataclass",)) FIELD_NAME = "field" DATACLASS_MODULES = frozenset( @@ -342,11 +346,7 @@ def _looks_like_dataclass_field_call(node: Call, check_scope: bool = True) -> bo return inferred.name == FIELD_NAME and inferred.root().name in DATACLASS_MODULES -def _get_field_default( - field_call: Call, -) -> Union[ - None, Tuple[Literal["default"], NodeNG], Tuple[Literal["default_factory"], Call] -]: +def _get_field_default(field_call: Call) -> FieldDefaultReturn: """Return a the default value of a field call, and the corresponding keyword argument name. field(default=...) results in the ... node From a55abbd4dfeb987ed5849b092ed12a703b44245d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 29 Dec 2021 19:37:37 +0100 Subject: [PATCH 9/9] Apply suggestions from code review Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- astroid/brain/brain_dataclasses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index ad89e9d6ef..bfdbbe09e5 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -42,7 +42,7 @@ else: from typing_extensions import Literal -FieldDefaultReturn = Union[ +_FieldDefaultReturn = Union[ None, Tuple[Literal["default"], NodeNG], Tuple[Literal["default_factory"], Call] ] @@ -346,7 +346,7 @@ def _looks_like_dataclass_field_call(node: Call, check_scope: bool = True) -> bo return inferred.name == FIELD_NAME and inferred.root().name in DATACLASS_MODULES -def _get_field_default(field_call: Call) -> FieldDefaultReturn: +def _get_field_default(field_call: Call) -> _FieldDefaultReturn: """Return a the default value of a field call, and the corresponding keyword argument name. field(default=...) results in the ... node