Skip to content

Ensure the *_fields attributes of __init__/postinit are cohesive #1218

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

Closed
wants to merge 15 commits into from

Conversation

thejcannon
Copy link
Contributor

@thejcannon thejcannon commented Oct 22, 2021

Steps

  • For new features or bug fixes, add a ChangeLog entry describing what your PR does.
  • Write a good description on what the PR does.

Description

It was discovered (and the fix is needed by) #1194 that there isn't strict cohesion between the *_fields attributes and the parameters in __init__ and postinit, so I've added tests which enforces this.

Through testing a handful of violators have been found, so a deprecation warning decorator has been added, and parameters have been updated accordingly.

Type of Changes

Type
🐛 Bug fix
✨ New feature
🔨 Refactoring
📜 Docs

Related Issue

Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for taking the time to review this part of the code and for undergoing the huge work of making it coherent. I have some small suggestions.

@@ -2105,8 +2108,7 @@ def postinit(
:param keywords: The keywords given to the class definition.
:type keywords: list(Keyword) or None
"""
if keywords is not None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a very strange way to do it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is strange about it? The default for self.keywords is an empty list. If I remember correctly, this change was added recently to fix a crash in pylint.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well usually you use if keyword is None: to apply the default value when the default value is a mutable like [] in order to avoid the problem of mutable default argument being function attribute, so doing the opposite where you initialize only if it's not None "feel" wrong.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the relevant PR for this change here:
#1181

thejcannon and others added 4 commits October 23, 2021 15:53
Co-authored-by: Pierre Sassoulas <[email protected]>
Co-authored-by: Pierre Sassoulas <[email protected]>
Co-authored-by: Pierre Sassoulas <[email protected]>
Copy link
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should not do these changes. For most, if not all, there is a good reason why it was designed that way. Some (limited) areas can certainly be improved, but these are changes that are more likely to introduce bugs than fix anything.

As for why some attributes are added with postinit and other in __init__, I don't know the full answer to that. I believe the idea was to pass most with __init__ but that isn't possible for children nodes. Thus they are added in postinit. Besides that, whatever makes most sense.

Moving some, just for the point of doing it, will add more potential for errors that is IMO just unnecessary.

Comment on lines +755 to +756
lineno: Optional[int] = None,
col_offset: Optional[int] = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ast node arguments doesn't have lineno or col_offset information. IMO it doesn't make sense to add them. https://docs.python.org/3.10/library/ast.html

@@ -1328,7 +1332,7 @@ def postinit(
self.target = target
self.annotation = annotation
self.value = value
self.simple = simple
self.simple = simple or self.simple
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can easily break. simple is a boolean integer so either 0 or 1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this was a forehead-slapper.

Comment on lines +1769 to +1770
lineno: Optional[int] = None,
col_offset: Optional[int] = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, the ast Comprehension node doesn't have lineno or col_offset information.
https://docs.python.org/3.10/library/ast.html

@@ -1800,7 +1811,7 @@ def postinit(
self.iter = iter
if ifs is not None:
self.ifs = ifs
self.is_async = is_async
self.is_async = is_async or self.is_async
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can break too!

Comment on lines +2669 to +2670
self.fromname: Optional[str] = fromname # can be None
self.modname = self.fromname # For backwards compatibility
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be part of this PR. IMO it's not worth it to do this change in isolation, there is no really need for it.

@@ -4076,6 +4088,7 @@ class FormattedValue(NodeNG):
"""

_astroid_fields = ("value", "format_spec")
_other_other_fields = ("conversion",)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should this be in _other_other_fields and not _other_fields? It's just an optional integer.

@@ -4252,7 +4265,7 @@ class EvaluatedObject(NodeNG):

name = "EvaluatedObject"
_astroid_fields = ("original",)
_other_fields = ("value",)
_other_other_fields = ("value",)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why here?

Comment on lines +4352 to +4353
lineno: Optional[int] = None,
col_offset: Optional[int] = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MatchCase doesn't have lineno or col_offset information.

Comment on lines -4534 to +4557
kwd_attrs: typing.List[str],
kwd_patterns: typing.List[Pattern],
kwd_attrs: typing.Optional[typing.List[str]] = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it does make more sense to keep kwd_attrs and kwd_patterns together in postinit. No need to move one to __init__.

@@ -2105,8 +2108,7 @@ def postinit(
:param keywords: The keywords given to the class definition.
:type keywords: list(Keyword) or None
"""
if keywords is not None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is strange about it? The default for self.keywords is an empty list. If I remember correctly, this change was added recently to fix a crash in pylint.

@thejcannon
Copy link
Contributor Author

It says in https://github.com/PyCQA/astroid/blob/3f98fa0be2279ff422a0e157705f4e3ea1e4d48f/doc/extending.rst that

When instantiating a node, the non-AST parameters are usually passed via the constructor, while the AST parameters are provided via the postinit() method. The only exception is that the parent is also passed via the constructor. Instantiating a new node might look as in:

So in addition to having these follow a convention for developer's sake conventions also helps allow code to make assumptions.

So I wouldn't say it's "just for the point of doing it". It's to address some tech debt / code uncleanliness.

@cdce8p
Copy link
Member

cdce8p commented Nov 1, 2021

It says in https://github.com/PyCQA/astroid/blob/3f98fa0be2279ff422a0e157705f4e3ea1e4d48f/doc/extending.rst that

When instantiating a node, the non-AST parameters are usually passed via the constructor, while the AST parameters are provided via the postinit() method. The only exception is that the parent is also passed via the constructor. Instantiating a new node might look as in:

So in addition to having these follow a convention for developer's sake conventions also helps allow code to make assumptions.

So I wouldn't say it's "just for the point of doing it". It's to address some tech debt / code uncleanliness.

We might need to update the documentation then 😉
I still don't think we should change it. Even if it addresses some tech debt, there are also good arguments to keep it the way it's now. The most important is the risk of regressions: Just from the current changes, we would have introduced three new regressions that could each crash astroid and by extension pylint. It's just not worth it!

@Pierre-Sassoulas
Copy link
Member

We've done some pretty heavy refactor and breaking change in 3.0 for consistency's sake, Marc, what is different here ? It think having a clear rule like:

  • non-AST parameters are passed via the constructor # notice I removed "usually"
  • AST parameters are provided via the postinit()
  • except for "parents" which are passed to constructor

Would make astroid development easier and reduce mistakes then the need to retro-engineer classes to see how they were implemented if they do not follow the general "rule". Is a breaking change in 3.0 really that much more impacting than what we already did ?

@cdce8p
Copy link
Member

cdce8p commented Nov 3, 2021

We've done some pretty heavy refactor and breaking change in 3.0 for consistency's sake, Marc, what is different here ?

The breaking changes so far have been because we moved code around. So changing the import is usually enough. Then there are the cases which shouldn't happen anyway, like passing None as argument in cases where it should never be None. Those are bugs that we previously handled gracefully but to improve typing, we deprecate it now.

This change in contrast will break code just because it "might" improve consistency. IMO this is dangerous and not worth it. As I've pointed out already, just from a few lines, there would have been multiple regressions. True, we can fix them but who is to say those don't introduce new bugs in plugins. Usually I think it's better to limit breaking changes to an absolute minimum and only do them when there is a clear benefit.

To highlight something different. Although it might fix inconsistencies, it also introduces new ones. All node types have lineno, col_offset, and parent as their last arguments for __init__. This PR creates an exceptions just for a few. Should we mention that in the docs, then?

A last point, the ast changes with every Python release. These inconsistencies are a direct result of that. We can easily add new arguments to postinit whereas with __init__ we would create breaking changes or change the order, i.e. parent isn't the last anymore. One of the major things astroid does so well, is hide the ast behind a somewhat stable interface pylint and plugins can work with. As for the need to "retro-engineer" classes, I would be surprised if anyone is able to write plugins without at least looking at the documentation, sometimes even that isn't enough. This PR won't change that.

@Pierre-Sassoulas
Copy link
Member

Ok, I'm convinced, it seemed really nice on paper @thejcannon thank you for the proposal. But the inconsistencies coming from the AST that will keep on coming and the bigger changes required for lib that are not us (pylint) are pretty convincing reasons to not fix this.

@thejcannon
Copy link
Contributor Author

Bummer. Cheers anyways.

@thejcannon thejcannon closed this Nov 4, 2021
cdce8p added a commit to cdce8p/astroid that referenced this pull request Nov 6, 2021
* Based on suggestions from pylint-dev#1218
cdce8p added a commit to cdce8p/astroid that referenced this pull request Nov 6, 2021
* Based on suggestions from pylint-dev#1218
@cdce8p cdce8p mentioned this pull request Nov 6, 2021
cdce8p added a commit that referenced this pull request Nov 6, 2021
* Based on suggestions from #1218
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants