-
-
Notifications
You must be signed in to change notification settings - Fork 278
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add typing to brain_dataclasses
#1292
Conversation
astroid/brain/brain_dataclasses.py
Outdated
result = _get_field_default(value) | ||
|
||
default_type, default_node = result | ||
default_type, default_node = _get_field_default(value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cdce8p For some reason I can't seem to restrict the return type of _get_field_default
and let mypy
know about this.
I tried Union[Tuple[None, None], Tuple[str, NodeNG]]
(and the necessary change to the method) as return type annotation, but then mypy
doesn't recognise that if default_type == "default"
then default_node
is a NodeNG
. I have tried other things such as working but a type alias but I couldn't get it to work. Searching for "mypy union return distinguish tuple constraints" etc also didn't return anything.
Do you have any good ideas or are these assert
's the best solution here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a neat way to do that:
result = _get_field_default(value)
if result != (None, None):
default_type, default_node = result
# Here mypy should know that default_type is a str, and default_node is a NodeNG
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could also modify the return type of _get_field_value
to return None
instead of None, None
. Might make it better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
result = _get_field_default(value)
if result != (None, None):
default_type, default_node = result
if default_type == "default":
param_str += f" = {default_node.as_string()}" # L202
elif default_type == "default_factory":
param_str += f" = {DEFAULT_FACTORY}"
assignment_str = (
f"self.{name} = {default_node.as_string()} " # L206
f"if {name} is {DEFAULT_FACTORY} else {name}"
)
Still gives me:
astroid/brain/brain_dataclasses.py:202: error: Item "None" of "Optional[NodeNG]" has no attribute "as_string" [union-attr]
astroid/brain/brain_dataclasses.py:206: error: Item "None" of "Optional[NodeNG]" has no attribute "as_string" [union-attr]
Am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you change the return type for that function to Union[Tuple[None, None], Tuple[str, NodeNG]]
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah.
Diff:
diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py
index b34f1aeff..d239806c5 100644
--- a/astroid/brain/brain_dataclasses.py
+++ b/astroid/brain/brain_dataclasses.py
@@ -7,7 +7,7 @@ Support both built-in dataclasses and pydantic.dataclasses. References:
- https://docs.python.org/3/library/dataclasses.html
- https://pydantic-docs.helpmanual.io/usage/dataclasses/
"""
-from typing import FrozenSet, Generator, List, Optional, Tuple
+from typing import FrozenSet, Generator, List, Optional, Tuple, Union
from astroid import context, inference_tip
from astroid.builder import parse
@@ -195,17 +195,18 @@ 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 != (None, None):
+ 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()}"
@@ -335,7 +336,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,
+) -> Union[Tuple[None, None], 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 +366,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, None
def _is_class_var(node: Optional[NodeNG]) -> bool:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The following diff works, but it feels weird to need to do an additional assignment to help mypy
infer the type:
It should be able to link the None
to the None
and str
to NodeNG
.
diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py
index b34f1aeff..648fdb827 100644
--- a/astroid/brain/brain_dataclasses.py
+++ b/astroid/brain/brain_dataclasses.py
@@ -7,7 +7,7 @@ Support both built-in dataclasses and pydantic.dataclasses. References:
- https://docs.python.org/3/library/dataclasses.html
- https://pydantic-docs.helpmanual.io/usage/dataclasses/
"""
-from typing import FrozenSet, Generator, List, Optional, Tuple
+from typing import FrozenSet, Generator, List, Optional, Tuple, Union
from astroid import context, inference_tip
from astroid.builder import parse
@@ -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()}"
@@ -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:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. Possibly mypy bug.
But I prefer this solution, as we were essentially returning two sentinels in the previous case.
+1 from me on return None
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Thank you @tushar-deepsource for the thorough review !
I still plan to get to it, sorry for the delay. |
No worry, there's still the issue with python 3.10 to fix before merging anyway. I don't know what could be happening, maybe a deprecation in setuptools/distutils ? |
@cdce8p No rush! We're all still volunteers. If I can be so bold, could I ask you to put pylint-dev/pylint#5221 on the top of your |
astroid/brain/brain_dataclasses.py
Outdated
# Since this is a dataclass it should have at least one decorator | ||
assert node.decorators |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you pointed out in the comment, this will always be true.
I haven't used it much so far, but something like this might work.
class _DataclassClassDef(ClassDef):
decorators: Decorators
def _check_generate_dataclass_init(node: _DataclassClassDef) -> bool:
...
It might even make sense to guard _DataclassClassDef
behind if TYPE_CHECKING
, just to be safe that it isn't used at runtime.
Usually I would recommend isinstance
calls but with the astroid brains those already happen in the predicate functions, is_decorated_with_dataclass
in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if we should make an instance for dataclass
like I did for NamedTuple
in #1306.
That would also allow setting decorators
. For now, I have removed the assert until we find a more permanent solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure yet if a separate instance would provide much benefit. Let's leave this as is then until I had time to look at #1306 and we decided how to proceed.
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
astroid/brain/brain_dataclasses.py
Outdated
# Since this is a dataclass it should have at least one decorator | ||
assert node.decorators |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure yet if a separate instance would provide much benefit. Let's leave this as is then until I had time to look at #1306 and we decided how to proceed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! One last comment, can be merged afterwards IMO.
Next up pylint-dev/pylint#5221
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Steps
Description
This does not fix everything as I have left some of the non-trivial cases for a separate PR.
I have one question that needs to be answered before this can be merged.
Type of Changes
Related Issue