Skip to content
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

Move inference methods to nodes module(s) #2171

Merged

Conversation

jacobtylerwalls
Copy link
Member

@jacobtylerwalls jacobtylerwalls commented May 8, 2023

Type of Changes

Type
βœ“ πŸ”¨ Refactoring

Description

Move _infer_() methods from inference.py to the nodes themselves (alternative to #2167).

Benefits

  • Avoid assigning methods to classes in inference.py and protocols.py (yuk!)
  • Move LookupMixIn to _base_nodes as promised in the long-existing TODO comment
  • We can now clearly see messages like:
astroid/nodes/node_classes.py:2349:4: W0221: Variadics removed in overriding 'Dict._infer' method (arguments-differ)

Drawbacks

  • There are some function-level imports created here that are symptoms of circular imports, but many of them are with the helpers module, and many of those are just for safe_infer, which could be extracted into its own helper module (it only needs UninferableBase). I don't think this jeopardizes the design.

Fixes #679

@@ -111,10 +107,7 @@
)
from astroid.nodes.utils import Position

_BaseContainer = BaseContainer # TODO Remove for astroid 3.0
Copy link
Member Author

Choose a reason for hiding this comment

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

removed!

Comment on lines -513 to -515
nodes.Assign.assigned_stmts = assign_assigned_stmts
nodes.AnnAssign.assigned_stmts = assign_annassigned_stmts
nodes.AugAssign.assigned_stmts = assign_assigned_stmts
Copy link
Member Author

Choose a reason for hiding this comment

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

yuk!


# TODO: Move into _base_nodes. Blocked by import of _infer_stmts from bases.
Copy link
Member Author

Choose a reason for hiding this comment

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

done!

Comment on lines -406 to -398
infer_lhs: ClassVar[InferLHS[AssignName]]

Copy link
Member Author

Choose a reason for hiding this comment

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

yuk!

@@ -1470,6 +1727,88 @@ def last_child(self):
return self.ops[-1][1]
# return self.left

# TODO: move to util?
Copy link
Member Author

Choose a reason for hiding this comment

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

decision point

def _to_literal(node: SuccessfulInferenceResult) -> Any:
# Can raise SyntaxError or ValueError from ast.literal_eval
# Can raise AttributeError from node.as_string() as not all nodes have a visitor
# Is this the stupidest idea or the simplest idea?
Copy link
Member Author

Choose a reason for hiding this comment

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

lol

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.

maxresdefault

I really heavily dislike the nodes.Const.infer_binary_op = const_infer_binary_op this is the most unreadable pattern (not to even mention cyclic import creator) I ever saw in the wild. I hope this approach can work !

Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

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

Left some early review comments.

Benefits

...

πŸš€ πŸ‘

Drawbacks

  • There are some function-level imports created here that are symptoms of circular imports, but many of them are with the helpers module, and many of those are just for safe_infer, which could be extracted into its own helper module (it only needs UninferableBase). I don't think this jeopardizes the design.

Agreed!

However, I think we are missing one major drawback (although not necessarily a drawback of this design).
We still have the issue of these classes being behemoths that (imo) try to do too much. With this change, and possibly the inclusion of protocols.py on the nodes themselves as well, there is not that much left besides the astroid.nodes package. I still don't know how I feel about this as it feels we are putting more logic on the nodes than is completely necessary.
There are also still reference to AstroidManager() or MANAGER which makes it impossible to decouple the global state used by astroid without starting the pass an actual manager object in these signatures.

Todos:

  • Get benchmarks

If we agree on this design I think we shouldn't be held back by performance degradation too much. We can more easily improve performance when we can actually work with the code.



class AttributeNode(NodeNG):
expr: NodeNG
Copy link
Collaborator

Choose a reason for hiding this comment

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

This feels like this class should also define a postinit to couple this annotation to something actually setting it.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's more of a mixin. The actual concrete classes do have postinit methods. Should I document this or raise some sort of TypeError when attempting to instantiate?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps make them ABC? However, I don't really like mixins in this way. We actually spend quite some time removing them from pylint because the way this mixins work don't allow us to guarantee that they function at runtime. We can't enforce that expr is being set.. Would it be feasible to make them have a postinit that needs to be called?

Copy link
Member Author

Choose a reason for hiding this comment

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

Happy to do it, but just for my own understanding, why would a postinit help? When would it ever be called, and how does it help a static type checker? I'm sure there is a reason, I just haven't worked with postinit in astroid yet.

Copy link
Collaborator

Choose a reason for hiding this comment

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

In this case it should probably be an init. Sorry for the confusion!

Copy link
Member Author

Choose a reason for hiding this comment

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

If the mixins need to be robust against unintended instantiation, then they're more trouble then they're worth just for the DRY benefit. I'll just extract helper functions that can be shared.

Copy link
Member

Choose a reason for hiding this comment

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

I also greatly dislike mixin for the sake of being dry because having to initialise instance in a particular order and not being able to use typecheck (becauseof course you can't guarantee the instanciation particular order and a type checker will rightfully warn you) is order if magnitude worse than whatever design problem mixin are trying to solve.

context.lookupname = self.name
context.constraints[self.name] = get_constraints(self, frame)

return bases._infer_stmts(stmts, context, frame)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I really like these new base classes, but don't really like how long this file is going to be and how many different types of bases nodes are in here. Not sure if this is fixable though.

Copy link
Member

Choose a reason for hiding this comment

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

We could burst some files later on but we have other issues to deal with imo. (Especially cyclic imports that make this harder)

Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

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

Left some early review comments.

Benefits

...

πŸš€ πŸ‘

Drawbacks

  • There are some function-level imports created here that are symptoms of circular imports, but many of them are with the helpers module, and many of those are just for safe_infer, which could be extracted into its own helper module (it only needs UninferableBase). I don't think this jeopardizes the design.

Agreed!

However, I think we are missing one major drawback (although not necessarily a drawback of this design).
We still have the issue of these classes being behemoths that (imo) try to do too much. With this change, and possibly the inclusion of protocols.py on the nodes themselves as well, there is not that much left besides the astroid.nodes package. I still don't know how I feel about this as it feels we are putting more logic on the nodes than is completely necessary.
There are also still reference to AstroidManager() or MANAGER which makes it impossible to decouple the global state used by astroid without starting the pass an actual manager object in these signatures.

Todos:

  • Get benchmarks

If we agree on this design I think we shouldn't be held back by performance degradation too much. We can more easily improve performance when we can actually work with the code.

@jacobtylerwalls
Copy link
Member Author

Thanks for the early feedback. I'll try to make some time over the next week or so to iron out the kinks. In the meantime, let's avoid merging changes to inference.py, because I've already deleted the file and could easily miss all the merge conflicts unless I check every commit on main.

@codecov
Copy link

codecov bot commented May 11, 2023

Codecov Report

Merging #2171 (80c0391) into main (8d57ce2) will decrease coverage by 0.02%.
The diff coverage is 94.82%.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2171      +/-   ##
==========================================
- Coverage   92.93%   92.91%   -0.02%     
==========================================
  Files          95       94       -1     
  Lines       10921    10926       +5     
==========================================
+ Hits        10149    10152       +3     
- Misses        772      774       +2     
Flag Coverage Ξ”
linux 92.71% <94.82%> (-0.03%) ⬇️
pypy 91.01% <93.66%> (-0.06%) ⬇️
windows 92.48% <94.82%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Ξ”
astroid/nodes/__init__.py 100.00% <ΓΈ> (ΓΈ)
astroid/nodes/node_classes.py 94.90% <93.22%> (-0.67%) ⬇️
astroid/nodes/_base_nodes.py 97.06% <96.60%> (-1.03%) ⬇️
astroid/__init__.py 100.00% <100.00%> (ΓΈ)
astroid/bases.py 87.96% <100.00%> (+0.06%) ⬆️
astroid/constraint.py 100.00% <100.00%> (ΓΈ)
astroid/filter_statements.py 98.91% <100.00%> (ΓΈ)
astroid/helpers.py 94.54% <100.00%> (+0.24%) ⬆️
astroid/manager.py 89.12% <100.00%> (ΓΈ)
astroid/nodes/scoped_nodes/mixin.py 100.00% <100.00%> (ΓΈ)
... and 2 more

... and 1 file with indirect coverage changes

@jacobtylerwalls jacobtylerwalls changed the title [discussion] Move inference methods to nodes module(s) Move inference methods to nodes module(s) May 12, 2023
@jacobtylerwalls jacobtylerwalls marked this pull request as ready for review May 12, 2023 00:11
@jacobtylerwalls jacobtylerwalls added this to the 3.0.0a3 milestone May 12, 2023
This was referenced May 13, 2023
Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

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

Still not sure how I feel about this. I think the use of Mixins and the import issues are not very nice, but you guys seem to agree that this is better so I'm also not sure how productive my opposition to this is πŸ˜„

def _infer(
self, context: InferenceContext | None = None, **kwargs: Any
) -> Generator[objects.Property | FunctionDef, None, InferenceErrorInfo]:
from astroid import objects # pylint: disable=import-outside-toplevel
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this is a case of pretty bad cyclic imports. With this approach we can't avoid this I think which makes me doubtful about whether this is the right approach.

Copy link
Member Author

@jacobtylerwalls jacobtylerwalls May 14, 2023

Choose a reason for hiding this comment

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

Challenge accepted!

Here is a branch that removes these cyclic imports by moving PartialFunction and Property to scoped_nodes, which makes sense, since they inherit from FunctionDef anyway (what's the use of a Property being an object, anyway?)

The ease with which I could do this gives me hope that this is a good direction that can enable easier refactors in iterative PRs. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

If we go with the infer_object approach, we're freezing the inference/objects/protocols ontologies in place, instead of actually questioning if everything is where it should be, which we should feel free to do during breaking changes season β˜”

Copy link
Collaborator

Choose a reason for hiding this comment

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

See my comment on the other pull request. I don't necessarily think we would be freezing anything in place.

I do think that we should merge your PartialFunction branch to master irrespective of the approach. Seems like a very sensible change!

Copy link
Member

Choose a reason for hiding this comment

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

Maybe bursting the big node files is actually (one of) the way to solve circular imports?

DanielNoord
DanielNoord previously approved these changes Jun 27, 2023
Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

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

I have one nit, but I don't really mind merging this as is and seeing if there are smaller improvements later on. There is too much going on to make it perfect anyway.

While I still don't prefer this method it seems the discussion doesn't really take off and I don't want to block this any longer. I trust you fixed this and tested that pylint test suite still passes.

If Pierre still approves of this PR (the code, not the idea) then let's ship it πŸš€

astroid now better infers self.root() as
[Uninferable, Instance of Global] instead of
[Uninferable]. But due to control flow, only
a Module will be returned, never a Global.
astroid has ignore-on-opaque-inference set to No,
unlike pylint.
@jacobtylerwalls jacobtylerwalls force-pushed the inference-polymorphism branch from b522ee9 to 76c10af Compare June 28, 2023 03:46
@jacobtylerwalls
Copy link
Member Author

jacobtylerwalls commented Jun 28, 2023

Sounds good @DanielNoord I really appreciate the care it took to reach this point, and of course I'm open to infer_object() down the road if we start showing tangible results with it. πŸ‘

I rebased and cleaned up the branch a bit.

Future PRs:

I trust you fixed this and tested that pylint test suite still passes.

I don't know what I fixed other than the monkey-patching πŸ˜‰ but yes the pylint suite passes. There's plenty to do in pylint, we should cut an astroid alpha ASAP and add 3.12 support and remove future=True everywhere.

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.

LGTM, amazing refactor there's a lot of work here, and it certainely makes astroid more undrstandable

for result in infer_callable(context):
if isinstance(result, error):
# For the sake of .infer(), we don't care about operation
# errors, which is the job of pylint. So return something
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# errors, which is the job of pylint. So return something
# errors, which is the job of a linter. So return something

Astroid Can be used without pylint :D

Copy link
Member Author

Choose a reason for hiding this comment

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

Hah, good point! I'll catch this in the next PR.



class AttributeNode(NodeNG):
expr: NodeNG
Copy link
Member

Choose a reason for hiding this comment

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

I also greatly dislike mixin for the sake of being dry because having to initialise instance in a particular order and not being able to use typecheck (becauseof course you can't guarantee the instanciation particular order and a type checker will rightfully warn you) is order if magnitude worse than whatever design problem mixin are trying to solve.

context.lookupname = self.name
context.constraints[self.name] = get_constraints(self, frame)

return bases._infer_stmts(stmts, context, frame)
Copy link
Member

Choose a reason for hiding this comment

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

We could burst some files later on but we have other issues to deal with imo. (Especially cyclic imports that make this harder)

def _infer(
self, context: InferenceContext | None = None, **kwargs: Any
) -> Generator[objects.Property | FunctionDef, None, InferenceErrorInfo]:
from astroid import objects # pylint: disable=import-outside-toplevel
Copy link
Member

Choose a reason for hiding this comment

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

Maybe bursting the big node files is actually (one of) the way to solve circular imports?

@jacobtylerwalls
Copy link
Member Author

Thanks so much for the reviews. I'll merge this and then get started on the followup PRs for removing the mixins and some of the circular imports.

@jacobtylerwalls jacobtylerwalls merged commit 7c90b58 into pylint-dev:main Jul 2, 2023
@jacobtylerwalls jacobtylerwalls deleted the inference-polymorphism branch July 2, 2023 11:47
jacobtylerwalls added a commit to jacobtylerwalls/astroid that referenced this pull request Jul 2, 2023
jacobtylerwalls added a commit that referenced this pull request Jul 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Remove monkey-patching of methods onto classes
3 participants