nedbat / coveragepy

The code coverage tool for Python
https://coverage.readthedocs.io
Apache License 2.0
3.01k stars 433 forks source link

No Source for Code #1459

Closed adam-grant-hendry closed 2 years ago

adam-grant-hendry commented 2 years ago

Describe the bug I'm attempting to follow your recent blog post on Making a coverage badge and always run into the same error:

No source for code: '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py'.

I tried deleting *.pyc files after reading this SO post, but it didn't solve my problem. I also searched several issue reports here having the same problem, but couldn't find an answer. I tried ignoring the error with -i, which also didn't seem to help.

Here is a link to the Actions tab of my GitHub repo. It has almost no code (just some # pragma: no cover statements in the qtpygraph/__init__.py file). I tried using COVERAGE_DEBUG=trace but I can't seem to get it to print to GitHub Action's stdout.

To Reproduce How can we reproduce the problem? Please be specific. Don't link to a failing CI job. Answer the questions below:

  1. What version of Python are you using?

3.8, 3.9, 3.10 on Windows, MacOs, and Ubuntu. Coverage is combined using ubuntu-latest with Python 3.10 on GitHub Actions.

  1. What version of coverage.py shows the problem? The output of coverage debug sys is helpful.

6.5.0

  1. What versions of what packages do you have installed? The output of pip freeze is helpful.
pip list ``` Package Version Editable project location ----------------------------- ----------- ----------------------------------------- add-trailing-comma 2.3.0 alabaster 0.7.12 altgraph 0.17.3 appdirs 1.4.4 argcomplete 2.0.0 astroid 2.12.10 asttokens 2.0.8 attrs 22.1.0 Babel 2.10.3 backcall 0.2.0 bandit 1.7.4 beautifulsoup4 4.11.1 black 22.8.0 blackdoc 0.3.7 blacken-docs 1.12.1 bottle 0.12.23 certifi 2022.9.24 cfgv 3.3.1 charset-normalizer 2.1.1 check-jsonschema 0.18.3 click 8.1.3 codecov 2.1.12 codespell 2.2.1 colorama 0.4.5 commitizen 2.35.0 commonmark 0.9.1 contourpy 1.0.5 coverage 6.5.0 cycler 0.11.0 decli 0.5.2 decorator 5.1.1 dill 0.3.5.1 distlib 0.3.6 doc8 0.11.2 docformatter 1.5.0 docutils 0.17.1 emoji 2.1.0 esbonio 0.14.1 exceptiongroup 1.0.0rc9 execnet 1.9.0 executing 1.1.0 filelock 3.8.0 flake8 5.0.4 flake8-bugbear 22.9.23 flake8-quotes 3.3.1 fonttools 4.37.4 future 0.18.2 gitdb 4.0.9 GitPython 3.1.27 glfw 2.5.5 graphviz 0.20.1 h5py 3.7.0 html5lib 1.1 hypothesis 6.55.0 IceSpringPySideStubs-PySide6 1.3.1 identify 2.5.5 idna 3.4 imagesize 1.4.1 importlib-metadata 5.0.0 importlib-resources 5.9.0 iniconfig 1.1.1 instaviz 0.6.0 ipdb 0.13.9 ipython 8.5.0 isort 5.10.1 jedi 0.18.1 Jinja2 3.1.2 jsonschema 4.16.0 kiwisolver 1.4.4 lazy-object-proxy 1.7.1 libcst 0.4.7 lxml 4.9.1 markdown-it-py 2.1.0 MarkupSafe 2.1.1 matplotlib 3.6.0 matplotlib-inline 0.1.6 mccabe 0.7.0 mdit-py-plugins 0.3.1 mdurl 0.1.2 memory-profiler 0.60.0 merry 0.3.0 more-itertools 8.14.0 mypy 0.981 mypy-extensions 0.4.3 myst-parser 0.18.1 natsort 8.2.0 ninja 1.10.2.4 nodeenv 1.7.0 numpy 1.23.3 numpydoc 1.4.0 objgraph 3.5.0 opencv-python 4.6.0.66 packaging 21.3 parso 0.8.3 pathspec 0.9.0 pbr 5.10.0 pefile 2022.5.30 pep8-naming 0.13.2 pickleshare 0.7.5 Pillow 9.2.0 pip 22.2.2 pkgutil_resolve_name 1.3.10 platformdirs 2.5.2 pluggy 1.0.0 pockets 0.9.1 pre-commit 2.20.0 prompt-toolkit 3.0.31 psutil 5.9.2 pure-eval 0.2.2 py 1.11.0 pycln 2.1.1 pycodestyle 2.9.1 pydantic 1.9.2 pydocstringformatter 0.7.2 pydocstyle 6.1.1 pyflakes 2.5.0 pygls 0.12.2 Pygments 2.13.0 pyinstaller 5.4.1 pyinstaller-hooks-contrib 2022.10 pylint 2.15.3 pyparsing 3.0.9 pyright 1.1.273 pyrsistent 0.18.1 PySide6 6.3.2 PySide6-Addons 6.3.2 PySide6-Essentials 6.3.2 pyspellchecker 0.7.0 pytest 7.1.3 pytest-cov 4.0.0 pytest-doctestplus 0.12.1 pytest-env 0.6.2 pytest-forked 1.4.0 pytest-memprof 0.2.0 pytest-mock 3.9.0 pytest-qt 4.1.0 pytest-randomly 3.12.0 pytest-xdist 2.5.0 python-dateutil 2.8.2 pytz 2022.4 pyupgrade 2.38.2 pywin32-ctypes 0.2.0 PyYAML 6.0 QtPy 2.2.0 qtpygraph 0.1.0 C:\Users\hendra11\Code\external\qtpygraph questionary 1.10.0 requests 2.28.1 restructuredtext-lint 1.4.0 rich 12.6.0 rstcheck 6.1.0 rstcheck-core 1.0.2 ruamel.yaml 0.17.21 ruamel.yaml.clib 0.2.6 seedir 0.3.1 setuptools 65.4.1 setuptools-scm 7.0.5 shellingham 1.5.0 shiboken6 6.3.2 six 1.16.0 smmap 5.0.0 snowballstemmer 2.2.0 sortedcontainers 2.4.0 soupsieve 2.3.2.post1 Sphinx 5.2.3 sphinx-rtd-theme 1.0.0 sphinxcontrib-applehelp 1.0.2 sphinxcontrib-devhelp 1.0.2 sphinxcontrib-email 0.3.5 sphinxcontrib-htmlhelp 2.0.0 sphinxcontrib-jsmath 1.0.1 sphinxcontrib-napoleon 0.7 sphinxcontrib-qthelp 1.0.3 sphinxcontrib-serializinghtml 1.1.5 stack-data 0.5.1 stevedore 4.0.0 termcolor 2.0.1 tokenize-rt 4.2.1 toml 0.10.2 tomli 2.0.1 tomlkit 0.11.5 tox 3.26.0 tox-gh-actions 2.9.1 tqdm 4.64.1 tqdm-stubs 0.2.1 traitlets 5.4.0 typeguard 2.13.3 typer 0.6.1 types-beautifulsoup4 4.11.6 types-docutils 0.18.3 types-setuptools 65.4.0.0 types-toml 0.10.8 typing_extensions 4.3.0 typing-inspect 0.8.0 untokenize 0.1.1 urllib3 1.26.12 virtualenv 20.16.5 vulture 2.6 wcwidth 0.2.5 webencodings 0.5.1 wheel 0.37.1 wrapt 1.14.1 zipp 3.8.1 ```
  1. What code shows the problem? Give us a specific commit of a specific repo that we can check out. If you've already worked around the problem, please provide a commit before that fix.

Please see qtpygraph commit dc2807

  1. What commands did you run?
tox configuration in pyproject.toml ```toml [tool.tox] legacy_tox_ini = """ [tox] minversion = 3.25.0 envlist = py{38,39,310}-{pyqt5,pyside2,pyqt6,pyside6} isolated_build = true [gh-actions] python = 3.8: py38-{pyqt5,pyside2,pyqt6,pyside6} 3.9: py39-{pyqt5,pyside2,pyqt6,pyside6} 3.10: py310-{pyqt5,pyside2,pyqt6,pyside6} [testenv] allowlist_externals = poetry pytest setenv = pyqt5: PYTEST_QT_API=pyqt5 pyqt5: QT_API=pyqt5 pyside2: PYTEST_QT_API=pyside2 pyside2: QT_API=pyside2 pyqt6: PYTEST_QT_API=pyqt6 pyqt6: QT_API=pyqt6 pyside6: PYTEST_QT_API=pyside6 pyside6: QT_API=pyside6 QT_QPA_PLATFORM=offscreen # See: https://github.com/tox-dev/tox/issues/1550 PYTHONIOENCODING=utf-8 COVERAGE_FILE=tox-.coverage.{envname} commands = poetry install --no-root pyqt5: poetry run python -m pip install PyQt5 PyQt5-stubs pyside2: poetry run python -m pip install PySide2 PySide2-stubs pyqt6: poetry run python -m pip install PyQt6 IceSpringPySideStubs-PyQt6 pyside6: poetry run python -m pip install PySide6 IceSpringPySideStubs-PySide6 poetry run pytest # Not run by default. To run, use `tox -e coverage` [testenv:coverage] allowlist_externals = find deps = coverage tox basepython = python3.10 setenv = COVERAGE_FILE=tox-.coverage COVERAGE_DEBUG=trace COVERAGE_DEBUG_FILE=stdout commands = find . -name "*.pyc" -delete coverage combine coverage report -m --skip-covered --debug=trace coverage html coverage json sh -c 'mv -f tox-.coverage .coverage' sh -c 'rm -f tox-.coverage' parallel_show_output = true # Not run by default. To run, use `tox -e docs` [testenv:docs] allowlist_externals = poetry sphinx-build commands = poetry install --no-root poetry run sphinx-build -W --keep-going -b docs docs/build """ ```

Expected behavior A clear and concise description of what you expected to happen.

Coverage is able to combine data from separate runs and produce a badge, as in the scriv repo and demonstrated in the Making a coverage badge blog post.

Additional context Add any other context about the problem here.

N/A (If you need more details, please let me know.)

nedbat commented 2 years ago

Can you add some debugging? Before your coverage combine command, add coverage debug data, and change the combine step to coverage combine --debug=pathmap. That will give us some good data.

adam-grant-hendry commented 2 years ago

@nedbat Yes, I'll do that right now. Give me a couple minutes to report back.

adam-grant-hendry commented 2 years ago

The link to the latest run is here, but the relevant output is as follows:

commands[0] | coverage debug data ``` -- data ------------------------------------------------------ path: /home/runner/work/qtpygraph/qtpygraph/tox-.coverage No data collected: file doesn't exist ----- path: /home/runner/work/qtpygraph/qtpygraph/tox-.coverage.py38-pyqt6 has_arcs: False 1 file: /Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py: 0 lines ----- path: /home/runner/work/qtpygraph/qtpygraph/tox-.coverage.py39-pyqt6 has_arcs: False 1 file: D:\a\qtpygraph\qtpygraph\qtpygraph\__init__.py: 0 lines ----- path: /home/runner/work/qtpygraph/qtpygraph/tox-.coverage.py310-pyside6 has_arcs: False 1 file: D:\a\qtpygraph\qtpygraph\qtpygraph\__init__.py: 0 lines ----- path: /home/runner/work/qtpygraph/qtpygraph/tox-.coverage.py39-pyqt5 has_arcs: False 1 file: D:\a\qtpygraph\qtpygraph\qtpygraph\__init__.py: 0 lines ----- path: /home/runner/work/qtpygraph/qtpygraph/tox-.coverage.py310-pyside2 has_arcs: False 1 file: D:\a\qtpygraph\qtpygraph\qtpygraph\__init__.py: 0 lines ----- path: /home/runner/work/qtpygraph/qtpygraph/tox-.coverage.py39-pyside2 has_arcs: False 1 file: D:\a\qtpygraph\qtpygraph\qtpygraph\__init__.py: 0 lines ----- path: /home/runner/work/qtpygraph/qtpygraph/tox-.coverage.py38-pyqt5 has_arcs: False 1 file: /Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py: 0 lines ----- path: /home/runner/work/qtpygraph/qtpygraph/tox-.coverage.py38-pyside2 has_arcs: False 1 file: /Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py: 0 lines ----- path: /home/runner/work/qtpygraph/qtpygraph/tox-.coverage.py310-pyqt6 has_arcs: False 1 file: D:\a\qtpygraph\qtpygraph\qtpygraph\__init__.py: 0 lines ----- path: /home/runner/work/qtpygraph/qtpygraph/tox-.coverage.py310-pyqt5 has_arcs: False 1 file: D:\a\qtpygraph\qtpygraph\qtpygraph\__init__.py: 0 lines ----- path: /home/runner/work/qtpygraph/qtpygraph/tox-.coverage.py38-pyside6 has_arcs: False 1 file: /Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py: 0 lines ----- path: /home/runner/work/qtpygraph/qtpygraph/tox-.coverage.py39-pyside6 has_arcs: False 1 file: D:\a\qtpygraph\qtpygraph\qtpygraph\__init__.py: 0 lines ```
commands[1] | coverage combine --debug=pathmap ``` Aliases (relative=False): Rule: '.tox/py*/Lib/site-packages/' -> 'qtpygraph/' using regex '(?:(?s:[\\\\/]home[\\\\/]runner[\\\\/]work[\\\\/]qtpygraph[\\\\/]qtpygraph[\\\\/]\\.tox[\\\\/]py.*[\\\\/]Lib[\\\\/]site\\-packages[\\\\/]))' Rule: '.tox/linting/Lib/site-packages/' -> 'qtpygraph/' using regex '(?:(?s:[\\\\/]home[\\\\/]runner[\\\\/]work[\\\\/]qtpygraph[\\\\/]qtpygraph[\\\\/]\\.tox[\\\\/]linting[\\\\/]Lib[\\\\/]site\\-packages[\\\\/]))' No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged Combined data file tox-.coverage.py38-pyqt6 No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged Combined data file tox-.coverage.py39-pyqt6 No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged Combined data file tox-.coverage.py310-pyside6 No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged Combined data file tox-.coverage.py39-pyqt5 No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged Combined data file tox-.coverage.py310-pyside2 No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged Combined data file tox-.coverage.py39-pyside2 No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged Combined data file tox-.coverage.py38-pyqt5 No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged Combined data file tox-.coverage.py38-pyside2 No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged Combined data file tox-.coverage.py310-pyqt6 No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged Combined data file tox-.coverage.py310-pyqt5 No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged Combined data file tox-.coverage.py38-pyside6 No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged Combined data file tox-.coverage.py39-pyside6 ```
commands[2] | coverage report -m --skip-covered ``` No source for code: '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py'. ERROR: InvocationError for command /home/runner/work/qtpygraph/qtpygraph/.tox/coverage/bin/coverage report -m --skip-covered (exited with code 1) ___________________________________ summary ____________________________________ ERROR: coverage: commands failed Error: Process completed with exit code 1. ```

The __init__.py seems to be picked up...

adam-grant-hendry commented 2 years ago

@nedbat The regex's seem to start with /home/, but the data paths start with /Users/. Does that have any bearing on this?

adam-grant-hendry commented 2 years ago

@nedbat Actually, if I'm reading the --debug=pathmap output correctly, it looks like my coverage.run.paths might not be set up correctly.

Here are the relevant portions of my pyproject.toml [tool.coverage.*] configuration entries (if you see anything, please let me know):

[tool.coverage.run]
branch = true
# Append machine name, process id, and random number to data file name so coverage can be
# run in parallel environments, e.g. in tox.
parallel = true
# `shiboken6`, which creates the python bindings for `Qt` C++ source, imports from a
# `zip` file into the top-level directory at runtime. These files are deleted after
# running, but `coverage` attempts to look at their source after they're gone, causing
# warnings to appear. Namely, it looks for these modules/files:
#
#    project_dir/pysrcript
#    project_dir/shibokensupport
#    project_dir/signature_bootstrap.py
#
# To avoid this error, `source` is specified to the package subdirectory. However, this
# can also be avoided by explicitly omitting these folders in the `omit` section.
#
# See https://github.com/nedbat/coveragepy/issues/1392
source = [
    'qtpygraph/'
]
omit = [
    'qtpygraph/__main__.py',
    '.vscode/',
    '.venv/',
    'tests/',
    'stubs/'
]
disable_warnings = ['no-data-collected']

[tool.coverage.html]
directory = 'logs/coverage/html'

[tool.coverage.json]
output = 'logs/coverage/coverage.json'

[tool.coverage.xml]
output = 'logs/coverage/coverage.xml'

[tool.coverage.report]
exclude_lines = [
    'pragma: no cover',
    'def __repr__',
    'raise AssertionError',
    'raise NotImplementedError',
    'if __name__ == .__main__.:',
    '@(abc\.)?abstractmethod'
]

[tool.coverage.paths]
source = [
    "qtpygraph/",
    ".tox/py*/Lib/site-packages/",
    ".tox/linting/Lib/site-packages/"
]
adam-grant-hendry commented 2 years ago

@nedbat Indeed, switching my tool.coverage.paths fixed the problem:

[tool.coverage.paths]
source = [
    "qtpygraph/",
    "*/site-packages"
]
others = [
    "qtpygraph/",
    "*/qtpygraph",
]

but:

  1. I don't understand why. I took a page from scriv's tox.ini. Can you explain a little more of what is going on here?

  2. I've now run into another problem: the schneegans/dynamic-badges-action isn't letting me write to my gist:

Failed to create gist, response status code: 401, status message: Unauthorized

Below is the relevant section of my workflow file (test.yml):

- name: Make Coverage Badge
  uses: schneegans/dynamic-badges-action@v1.4.0
  with:
    # `GIST_TOKEN` is simply a personal access token with a scope of "gist".
    auth: ${{ secrets.GIST_TOKEN }}
    gistID: 93a225b73dfa5ee041e6786dfbeff9ad
    filename: coverage_badge.json
    label: Coverage
    message: ${{ env.total }}%
    minColorRange: 50
    maxColorRange: 90
    valColorRAnge: ${{ env.total }}

and the link to my gist is here.

Should I open a separate issue for this?

adam-grant-hendry commented 2 years ago

@nedbat I opened issue #15 on dynamic-bages-action for my second problem, so if you can explain why changing my [tool.coverage.paths] fixed the problem, we can close this issue.

Preocts commented 2 years ago

Starting disclaimer: I am not an authority on the subject, just another dev working through a very similar issue (now resolved thanks to this thread).

The key difference appears to be the isolated_build = true flag that the scriv project does not use. If I understand this explanation; the flag changes the source of the files being tested from the project directory to an isolated */site-packages/ install. It's also a required flag if the project uses pyproject.toml over setup.cfg and/or setup.py.

This is good, imo, but it also requires us to give coverage some extra information on how to transform the paths back to the project structure. That's my theory, at least, after three hours of poking around at my own template project to add the same groovy coverage badge.

adam-grant-hendry commented 2 years ago

just another dev working through a very similar issue (now resolved thanks to this thread).

Glad I could help!

The key difference appears to be the isolated_build = true flag that the scriv project does not use.

That's a good thought, but the scriv/setup.cfg settings for coverage:paths use the same pattern of reporting same-named files in */site-packages to those in the original source:

[coverage:paths]
source =
    src
    */site-packages

others =
    .
    */scriv

which is how I discovered that I should write my pyproject.toml configuration like so in the first place:

[tool.coverage.paths]
source = [
    "qtpygraph/",
    "*/site-packages"
]
others = [
    "qtpygraph/",
    "*/qtpygraph",
]

So if scriv doesn't use isolated_build = true, which you correctly pointed out, it seems the reason for setting paths like this is deeper than isolated_build = True, or unrelated.

@nedbat Why are the paths listed like this?

adam-grant-hendry commented 2 years ago

@nedbat Following up on this before it gets too far away from us. Can you please explain why paths are listed like this?

nedbat commented 2 years ago

I see this line in your debug output:

No rules match, path '/Users/runner/work/qtpygraph/qtpygraph/qtpygraph/__init__.py' is unchanged

This was under your old rules:

[tool.coverage.paths]
source = [
    "qtpygraph/",
    ".tox/py*/Lib/site-packages/",
    ".tox/linting/Lib/site-packages/"
]

where indeed, there is no match, since the path has no .tox in it, not to mention the rest of the pattern.

You changed your rules to:

[tool.coverage.paths]
source = [
    "qtpygraph/",
    "*/site-packages"
]
others = [
    "qtpygraph/",
    "*/qtpygraph",
]

(which btw, could be simpler, since the first item, the map-to, is the same in both:)

[tool.coverage.paths]
source = [
    "qtpygraph/",
    "*/site-packages"
    "*/qtpygraph",
]

Now the path is matched by */qtpygraph, so it is remapped to be qtpygraph/qtpygraph/qtpygraph/__init__.py.

adam-grant-hendry commented 2 years ago

Thank you @nedbat! Thank you also for coverage.py, your wonderful PyCon talks, and all your contributions to the Python community! I hope all is going well.