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

coverage html crashes with RecursionError in ast traversal #1774

Closed
oscarbenjamin opened this issue May 3, 2024 · 11 comments
Closed

coverage html crashes with RecursionError in ast traversal #1774

oscarbenjamin opened this issue May 3, 2024 · 11 comments
Labels
bug Something isn't working fixed

Comments

@oscarbenjamin
Copy link

Describe the bug

Running coverage html crashes with RecursionError in ast traversal when trying to measure coverage of SymPy's sympy.polys module.

To Reproduce

Tested in a fresh venv with Python 3.8, 3.11 and 3.12 using coverage 7.5.0. It seems to be a new problem in coverage 7.5.0 because I can't reproduce it with e.g. coverage 7.4.4.

To reproduce run:

pip install pytest coverage==7.5.0 pytest-cov hypothesis sympy==1.12
pytest --cov=sympy.polys --pyargs sympy.polys.tests.test_densetools
coverage html

Then coverage html crashes out with:

$ coverage html
Traceback (most recent call last):
  File "/home/oscar/current/active/sympy/tmp/tmp2/venv/bin/coverage", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/home/oscar/current/active/sympy/tmp/tmp2/venv/lib/python3.12/site-packages/coverage/cmdline.py", line 970, in main
    status = CoverageScript().command_line(argv)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/oscar/current/active/sympy/tmp/tmp2/venv/lib/python3.12/site-packages/coverage/cmdline.py", line 720, in command_line
    total = self.coverage.html_report(
            ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/oscar/current/active/sympy/tmp/tmp2/venv/lib/python3.12/site-packages/coverage/control.py", line 1171, in html_report
    ret = reporter.report(morfs)
          ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/oscar/current/active/sympy/tmp/tmp2/venv/lib/python3.12/site-packages/coverage/html.py", line 335, in report
    self.write_html_page(ftr)
  File "/home/oscar/current/active/sympy/tmp/tmp2/venv/lib/python3.12/site-packages/coverage/html.py", line 419, in write_html_page
    file_data = self.datagen.data_for_file(ftr.fr, ftr.analysis)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/oscar/current/active/sympy/tmp/tmp2/venv/lib/python3.12/site-packages/coverage/html.py", line 138, in data_for_file
    for lineno, tokens in enumerate(fr.source_token_lines(), start=1):
  File "/home/oscar/current/active/sympy/tmp/tmp2/venv/lib/python3.12/site-packages/coverage/phystokens.py", line 127, in source_token_lines
    soft_key_lines = SoftKeywordFinder(source).soft_key_lines
                     ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/oscar/current/active/sympy/tmp/tmp2/venv/lib/python3.12/site-packages/coverage/phystokens.py", line 86, in __init__
    self.visit(ast.parse(source))
  File "/home/oscar/.pyenv/versions/3.12.0/lib/python3.12/ast.py", line 407, in visit
    return visitor(node)
           ^^^^^^^^^^^^^
  File "/home/oscar/.pyenv/versions/3.12.0/lib/python3.12/ast.py", line 415, in generic_visit
    self.visit(item)
  File "/home/oscar/.pyenv/versions/3.12.0/lib/python3.12/ast.py", line 407, in visit
    return visitor(node)
           ^^^^^^^^^^^^^
  File "/home/oscar/.pyenv/versions/3.12.0/lib/python3.12/ast.py", line 417, in generic_visit
    self.visit(value)
  File "/home/oscar/.pyenv/versions/3.12.0/lib/python3.12/ast.py", line 407, in visit
    return visitor(node)
           ^^^^^^^^^^^^^
  File "/home/oscar/.pyenv/versions/3.12.0/lib/python3.12/ast.py", line 415, in generic_visit
    self.visit(item)
  File "/home/oscar/.pyenv/versions/3.12.0/lib/python3.12/ast.py", line 407, in visit
    return visitor(node)
           ^^^^^^^^^^^^^
...
  File "/home/oscar/.pyenv/versions/3.12.0/lib/python3.12/ast.py", line 417, in generic_visit
    self.visit(value)
  File "/home/oscar/.pyenv/versions/3.12.0/lib/python3.12/ast.py", line 407, in visit
    return visitor(node)
           ^^^^^^^^^^^^^
RecursionError: maximum recursion depth exceeded

Expected behavior

Should produce coverage html report.

Additional context

There are other ways to reproduce this e.g. in a sympy git checkout:

pytest --cov=sympy.polys --cov-report=html sympy/polys
@oscarbenjamin oscarbenjamin added bug Something isn't working needs triage labels May 3, 2024
@oscarbenjamin
Copy link
Author

It seems to be a new problem in coverage 7.5.0 because I can't reproduce it with e.g. coverage 7.4.4.

Actually I'm not sure about this. I just reproduced this in a different way with 7.4.4...

@nedbat
Copy link
Owner

nedbat commented May 3, 2024

Wow, interesting bug report. When I try it, it does happen with 7.4.4 as well as 7.5.0. In fact, it happens without coverage at all:

import ast
import inspect
import sympy.polys.numberfields.resolvent_lookup as mod
source = inspect.getsource(mod)
# Causes RecursionError: maximum recursion depth exceeded
ast.NodeVisitor().visit(ast.parse(source))

Has something else changed along with the 7.4.4->7.5.0 coverage update? The file is relatively new (18 months) but I would expect it to have failed like this before now.

@oscarbenjamin
Copy link
Author

Thanks Ned.

Has something else changed along with the 7.4.4->7.5.0 coverage update?

I think it must be a while since I last tried to make a coverage html report that would have included this file. I was getting confused before about which versions of coverage I was testing with because of mixed up virtual environments. I guess what is new is just this file in sympy...

@oscarbenjamin
Copy link
Author

Strangely though the code that you showed crashes if I run it as a direct script but not if run in ipython:

In [1]: import ast
   ...: import inspect
   ...: import sympy.polys.numberfields.resolvent_lookup as mod
   ...: source = inspect.getsource(mod)
   ...: # no error here
   ...: ast.NodeVisitor().visit(ast.parse(source))

In [2]: 

I'm not sure why that would be...

In any case I suppose that this is an ast bug then so I will go to cpython...

@nedbat
Copy link
Owner

nedbat commented May 3, 2024

I don't think it's an ast bug. If you use sys.setrecursionlimit() higher, it will succeed. You have crafted a source file with a truly deep AST.

@nedbat
Copy link
Owner

nedbat commented May 3, 2024

A recursion limit of 1100 fails, 1200 succeeds. The usual limit is 1000.

@oscarbenjamin
Copy link
Author

It looks like the ast.walk function is better suited to what coverage needs here rather than NodeVisitor:
https://github.com/python/cpython/blob/13245027526bf1b21fae6e7ca62ceec2e39fcfb7/Lib/ast.py#L390-L401
That is non-recursive and yields all nodes in the tree. It can be wrapped up like:

if sys.version_info >= (3, 10):
    from ast import MatchClass
else:
    class MatchClass: pass


if sys.version_info >= (3, 12):
    from ast import TypeAlias
else:
    class TypeAlias: pass


def get_soft_key_lines(source):
    """Helper for finding lines with soft keywords, like match/case lines."""
    tree = ast.parse(source)
    soft_key_lines = set()

    for node in ast.walk(tree):
        if isinstance(node, MatchClass):
            soft_key_lines.add(node.lineno)
        elif isinstance(node, TypeAlias):
            soft_key_lines.add(node.lineno)
            for case in node.cases:
                soft_key_lines.add(case.pattern.lineno)

    return soft_key_lines

Unfortunately after making that change I get basically the same error coming from RegionFinder instead.

@nedbat
Copy link
Owner

nedbat commented May 3, 2024

Yes, coverage uses the ast in three places (up from two in 7.4.4). I'm wondering about limiting the depth of exploration, since it would also save time, and maybe caching the parses.

@nedbat
Copy link
Owner

nedbat commented May 4, 2024

Fixed in commit 5fa9f67.

@nedbat nedbat closed this as completed May 4, 2024
@nedbat nedbat added the fixed label May 4, 2024
@nedbat
Copy link
Owner

nedbat commented May 4, 2024

This is now released as part of coverage 7.5.1.

@oscarbenjamin
Copy link
Author

Thanks Ned!

I was still thinking what could fix this. It seems to me that the ast module is missing some kind of generic primitive that would be usable for something like RegionFinder. There is walk but that does breadth first search. What is needed here is more like a preorder traversal except that you also need to remember the parents of each node while traversing.

It is problematic that NodeVisitor breaks with large expressions but there is not much else provided for traversal.

renovate bot added a commit to allenporter/flux-local that referenced this issue May 4, 2024
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [coverage](https://togithub.com/nedbat/coveragepy) | `==7.5.0` ->
`==7.5.1` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/coverage/7.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/coverage/7.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/coverage/7.5.0/7.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/coverage/7.5.0/7.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>nedbat/coveragepy (coverage)</summary>

###
[`v7.5.1`](https://togithub.com/nedbat/coveragepy/blob/HEAD/CHANGES.rst#Version-751--2024-05-04)

[Compare
Source](https://togithub.com/nedbat/coveragepy/compare/7.5.0...7.5.1)

- Fix: a pragma comment on the continuation lines of a multi-line
statement
now excludes the statement and its body, the same as if the pragma is
on the first line. This closes `issue 754`*. The fix was contributed by
    `Daniel Diniz <pull 1773_>`*.

- Fix: very complex source files like `this one <resolvent_lookup_>`\_
could
cause a maximum recursion error when creating an HTML report. This is
now
    fixed, closing `issue 1774`\_.

-   HTML report improvements:

- Support files (JavaScript and CSS) referenced by the HTML report now
have
hashes added to their names to ensure updated files are used instead of
        stale cached copies.

- Missing branch coverage explanations that said "the condition was
never
false" now read "the condition was always true" because it's easier to
        understand.

- Column sort order is remembered better as you move between the index
pages,
        fixing `issue 1766`*.  Thanks, `Daniel Diniz <pull 1768_>`*.

.. \_resolvent_lookup:
https://github.com/sympy/sympy/blob/130950f3e6b3f97fcc17f4599ac08f70fdd2e9d4/sympy/polys/numberfields/resolvent_lookup.py
.. \_issue
754[nedbat/coveragepy#754
.. \_issue
176[nedbat/coveragepy#1766
.. \_pull
17[nedbat/coveragepy#1768
.. \_pull
1[nedbat/coveragepy#1773
.. \_issue
[nedbat/coveragepy#1774

.. \_changes\_7-5-0:

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/allenporter/flux-local).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4zMzEuMCIsInVwZGF0ZWRJblZlciI6IjM3LjMzMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working fixed
Projects
None yet
Development

No branches or pull requests

2 participants