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

Log RecursionErrors out as warnings during node transformation #2385

Merged
merged 14 commits into from
Feb 23, 2024

Conversation

jacobtylerwalls
Copy link
Member

@jacobtylerwalls jacobtylerwalls commented Feb 18, 2024

Type of Changes

Type
βœ“ πŸ› Bug fix

Description

Closes pylint-dev/pylint#8842

This only addresses recursion errors during node transformation. We should do something similar for the inference system, but suggesting to do that in pylint, not here.

@jacobtylerwalls jacobtylerwalls added this to the 3.1.0 milestone Feb 18, 2024
Copy link

codecov bot commented Feb 18, 2024

Codecov Report

Attention: Patch coverage is 91.66667% with 1 lines in your changes are missing coverage. Please review.

Project coverage is 92.75%. Comparing base (07419f0) to head (79e046a).
Report is 4 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2385      +/-   ##
==========================================
- Coverage   92.75%   92.75%   -0.01%     
==========================================
  Files          94       94              
  Lines       11070    11080      +10     
==========================================
+ Hits        10268    10277       +9     
- Misses        802      803       +1     
Flag Coverage Ξ”
linux 92.56% <91.66%> (-0.01%) ⬇️
pypy 90.75% <50.00%> (-0.13%) ⬇️
windows 92.34% <91.66%> (-0.01%) ⬇️

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

Files Coverage Ξ”
astroid/transforms.py 100.00% <100.00%> (ΓΈ)
astroid/nodes/as_string.py 96.80% <83.33%> (-0.23%) ⬇️

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.

Nice idea! One optional comment

tests/test_transforms.py Outdated Show resolved Hide resolved
tests/test_transforms.py Outdated Show resolved Hide resolved
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, maybe there's a middle ground between 1000 and 10 for the tests ? πŸ˜„ I'll let you decide

@jacobtylerwalls
Copy link
Member Author

LGTM, maybe there's a middle ground between 1000 and 10 for the tests ? πŸ˜„ I'll let you decide

It has to be pretty high so that it doesn't accidentally cause other RecursionErrors we're not interested in testing. 1000 is the default, but lower than the pytest default of 2000, so that's why I had to do it at all.

cdce8p
cdce8p previously approved these changes Feb 23, 2024
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.

The warning level make so much sense that I wonder why we didn't do that sooner. πŸŽ‰

@Pierre-Sassoulas Pierre-Sassoulas enabled auto-merge (squash) February 23, 2024 08:37
@cdce8p cdce8p disabled auto-merge February 23, 2024 09:25
Comment on lines 368 to 369
except RecursionError:
left = f"({node.expr.accept(self)})"
Copy link
Member

Choose a reason for hiding this comment

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

Seems like these lines aren't covered by the test case. Is it actually necessary to catch RecursionError here? Doing so in _visit_generic seems to be enough.

Copy link
Member Author

Choose a reason for hiding this comment

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

No worries, I'll add the test coverage. It's not related to node transformation, but I wanted this file to be lintable without further patches needed in astroid so we could close the ticket.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, I tried, but the test crashes pytest. Any ideas?

diff --git a/tests/test_nodes.py b/tests/test_nodes.py
index 6ea25fd8..713591e4 100644
--- a/tests/test_nodes.py
+++ b/tests/test_nodes.py
@@ -29,7 +29,7 @@ from astroid import (
     transforms,
     util,
 )
-from astroid.const import PY310_PLUS, PY312_PLUS, Context
+from astroid.const import IS_PYPY, PY310_PLUS, PY312_PLUS, Context
 from astroid.context import InferenceContext
 from astroid.exceptions import (
     AstroidBuildingError,
@@ -49,6 +49,7 @@ from astroid.nodes.node_classes import (
 from astroid.nodes.scoped_nodes import ClassDef, FunctionDef, GeneratorExp, Module
 
 from . import resources
+from tests.testdata.python3.recursion_error import LONG_CHAINED_METHOD_CALL
 
 abuilder = builder.AstroidBuilder()
 
@@ -279,6 +280,21 @@ everything = f""" " \' \r \t \\ {{ }} {'x' + x!r:a} {["'"]!s:{a}}"""
         assert nodes.Unknown().as_string() == "Unknown.Unknown()"
         assert nodes.Unknown(lineno=1, col_offset=0).as_string() == "Unknown.Unknown()"
 
+    @staticmethod
+    def test_recursion_error_trapped() -> None:
+        original_limit = sys.getrecursionlimit()
+        sys.setrecursionlimit(500 if IS_PYPY else 1000)
+        
+        try:
+            with pytest.warns(UserWarning, match="unable to transform"):
+                ast = abuilder.string_build(LONG_CHAINED_METHOD_CALL)
+
+            attribute = ast.body[1].value.func
+            assert attribute.as_string()
+
+        finally:
+            sys.setrecursionlimit(original_limit)
+
 
 @pytest.mark.skipif(not PY312_PLUS, reason="Uses 3.12 type param nodes")
 class AsStringTypeParamNodes(unittest.TestCase):

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll try logging a warning and catching that in the test

Copy link
Member

Choose a reason for hiding this comment

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

I guess that the except clause itself also triggers a new RecursionError.

f"({node.expr.accept(self)})"

Just as POC, try:

f"({node.expr})"

Copy link
Member Author

Choose a reason for hiding this comment

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

I wondered about that, but pylint lints the file just fine.

Copy link
Member Author

Choose a reason for hiding this comment

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

With your change, the output begins:

(Call(func=<Attribute.add l.5 at 0x1075eaf30>,
args=[<Const.str l.10 at 0x1075eb680>],
keywords=[<Keyword l.10 at 0x1075eb6e0>])).add('name', value='value').add('name', value='value')...

instead of

b.builder('name').add('name', value='value').add('name', value='value')...

Comment on lines +289 to +290
with pytest.raises(UserWarning):
attribute.as_string()
Copy link
Member

Choose a reason for hiding this comment

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

Just know that this is more a workaround. pytest would still hang if the method didn't raise a UserWarning. I believe internally pytest sets the warning to error to be able to catch it here, but that also means the fallback isn't executed.

Copy link
Member Author

Choose a reason for hiding this comment

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

absolutely, but it seems pytest is doing something magical if the using same recursion limit is not a problem in the interactive interpreter but is a problem inside pytest?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, pytest and warnings capture is just magic sometimes πŸ˜…
Since the test pass an it works in pylint I would be fine with merging this as is (even with the one uncovered line).

Comment on lines +289 to +290
with pytest.raises(UserWarning):
attribute.as_string()
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, pytest and warnings capture is just magic sometimes πŸ˜…
Since the test pass an it works in pylint I would be fine with merging this as is (even with the one uncovered line).

@jacobtylerwalls jacobtylerwalls merged commit 8c7106e into main Feb 23, 2024
19 of 20 checks passed
@jacobtylerwalls jacobtylerwalls deleted the trap-recursion branch February 23, 2024 15:17
@jacobtylerwalls
Copy link
Member Author

Thanks for the reviews!

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.

RecursionError: maximum recursion depth exceeded while linting a large chained method calls
4 participants