diff --git a/.github/workflows/cancel.yml b/.github/workflows/cancel.yml index 97b4d88d3..11b385b17 100644 --- a/.github/workflows/cancel.yml +++ b/.github/workflows/cancel.yml @@ -2,7 +2,7 @@ # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # This action finds in-progress Action jobs for the same branch, and cancels -# them. There's little point in continuing to run superceded jobs. +# them. There's little point in continuing to run superseded jobs. name: "Cancel" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ee798ada1..5686493e2 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -9,12 +9,16 @@ on: push: branches: - master + - "**/*metacov*" workflow_dispatch: defaults: run: shell: bash +env: + PIP_DISABLE_PIP_VERSION_CHECK: 1 + jobs: coverage: name: "Python ${{ matrix.python-version }} on ${{ matrix.os }}" @@ -29,10 +33,11 @@ jobs: python-version: # When changing this list, be sure to check the [gh-actions] list in # tox.ini so that tox will run properly. - - "2.7" - - "3.5" + - "3.6" + - "3.7" + - "3.8" - "3.9" - - "3.10.0-alpha.5" + - "3.10.0-rc.2" - "pypy3" exclude: # Windows PyPy doesn't seem to work? @@ -52,11 +57,6 @@ jobs: with: python-version: "${{ matrix.python-version }}" - - name: "Install Visual C++ if needed" - if: runner.os == 'Windows' && matrix.python-version == '2.7' - run: | - choco install vcpython27 -f -y - - name: "Install dependencies" run: | set -xe @@ -70,19 +70,12 @@ jobs: - name: "Run tox coverage for ${{ matrix.python-version }}" env: COVERAGE_COVERAGE: "yes" + COVERAGE_CONTEXT: "${{ matrix.python-version }}.${{ matrix.os }}" run: | set -xe - python -m tox - - - name: "Combine" - env: - COVERAGE_COVERAGE: "yes" - COVERAGE_RCFILE: "metacov.ini" - COVERAGE_METAFILE: ".metacov" - run: | - set -xe - COVERAGE_DEBUG=dataio python -m igor combine_html - mv .metacov .metacov.${{ matrix.python-version }}.${{ matrix.os }} + # Something about pytest 6.x with xdist keeps data from collecting. + # Use -n0 for now. + python -m tox -- -n 0 - name: "Upload coverage data" uses: actions/upload-artifact@v2 @@ -112,7 +105,7 @@ jobs: python -VV python -m site python setup.py --quiet clean develop - python igor.py zip_mods install_egg + python igor.py zip_mods - name: "Download coverage data" uses: actions/download-artifact@v2 @@ -124,11 +117,12 @@ jobs: env: COVERAGE_RCFILE: "metacov.ini" COVERAGE_METAFILE: ".metacov" + COVERAGE_CONTEXT: "yes" run: | set -xe python -m igor combine_html python -m coverage json - echo "::set-output name=total::$(python -c "import json;print(format(json.load(open('coverage.json'))['totals']['percent_covered'],'.2f'))")" + echo "::set-output name=total::$(python -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])")" - name: "Upload to codecov" uses: codecov/codecov-action@v1 diff --git a/.github/workflows/kit.yml b/.github/workflows/kit.yml index 854b4f299..726cefaca 100644 --- a/.github/workflows/kit.yml +++ b/.github/workflows/kit.yml @@ -7,25 +7,41 @@ name: "Kits" on: + push: + branches: + # Don't build kits all the time, but do if the branch is about kits. + - "**/*kit*" workflow_dispatch: defaults: run: shell: bash +env: + PIP_DISABLE_PIP_VERSION_CHECK: 1 + jobs: - build_wheels: + wheels: name: "Build wheels on ${{ matrix.os }}" runs-on: ${{ matrix.os }} strategy: matrix: - os: - - ubuntu-latest - - windows-latest - - macos-latest + include: + - os: ubuntu-latest + cibw_arch: x86_64 i686 aarch64 + - os: windows-latest + cibw_arch: x86 AMD64 + - os: macos-latest + cibw_arch: x86_64 fail-fast: false steps: + - name: Setup QEMU + if: matrix.os == 'ubuntu-latest' + uses: docker/setup-qemu-action@v1 + with: + platforms: arm64 + - name: "Check out the repo" uses: actions/checkout@v2 @@ -38,25 +54,22 @@ jobs: run: | python -m pip install -c requirements/pins.pip cibuildwheel - - name: "Install Visual C++ for Python 2.7" - if: runner.os == 'Windows' - run: | - choco install vcpython27 -f -y - - name: "Build wheels" env: # Don't build wheels for PyPy. CIBW_SKIP: pp* + CIBW_ARCHS: ${{ matrix.cibw_arch }} run: | python -m cibuildwheel --output-dir wheelhouse + ls -al wheelhouse/ - name: "Upload wheels" uses: actions/upload-artifact@v2 with: name: dist - path: ./wheelhouse/*.whl + path: wheelhouse/*.whl - build_sdist: + sdist: name: "Build source distribution" runs-on: ubuntu-latest steps: @@ -71,6 +84,7 @@ jobs: - name: "Build sdist" run: | python setup.py sdist + ls -al dist/ - name: "Upload sdist" uses: actions/upload-artifact@v2 @@ -78,7 +92,7 @@ jobs: name: dist path: dist/*.tar.gz - build_pypy: + pypy: name: "Build PyPy wheels" runs-on: ubuntu-latest steps: @@ -98,6 +112,55 @@ jobs: run: | pypy3 setup.py bdist_wheel --python-tag pp36 pypy3 setup.py bdist_wheel --python-tag pp37 + ls -al dist/ + + - name: "Upload wheels" + uses: actions/upload-artifact@v2 + with: + name: dist + path: dist/*.whl + + prerel: + name: "Build ${{ matrix.python-version }} wheels on ${{ matrix.os }}" + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + python-version: + - "3.10.0-rc.2" + fail-fast: false + + steps: + - name: "Check out the repo" + uses: actions/checkout@v2 + + - name: "Install Python ${{ matrix.python-version }}" + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: "Install wheel tools" + run: | + python -m pip install -r requirements/wheel.pip + + - name: "Build wheel" + run: | + python setup.py bdist_wheel + + - name: "Convert to manylinux wheel" + if: runner.os == 'Linux' + run: | + ls -la dist/ + auditwheel show dist/*.whl + auditwheel repair dist/*.whl + ls -la wheelhouse/ + auditwheel show wheelhouse/*.whl + rm dist/*.whl + mv wheelhouse/*.whl dist/ + ls -al dist/ - name: "Upload wheels" uses: actions/upload-artifact@v2 diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 1a1b7f03f..7df3c9886 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -14,6 +14,9 @@ defaults: run: shell: bash +env: + PIP_DISABLE_PIP_VERSION_CHECK: 1 + jobs: lint: name: "Pylint etc" @@ -36,7 +39,7 @@ jobs: set -xe python -VV python -m site - python -m pip install -r requirements/tox.pip + python -m pip install -c requirements/pins.pip tox - name: "Tox lint" run: | @@ -60,7 +63,7 @@ jobs: set -xe python -VV python -m site - python -m pip install -r requirements/tox.pip + python -m pip install -c requirements/pins.pip tox - name: "Tox doc" run: | diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index a88bfba4c..49df01e6e 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -14,6 +14,9 @@ defaults: run: shell: bash +env: + PIP_DISABLE_PIP_VERSION_CHECK: 1 + jobs: tests: name: "Python ${{ matrix.python-version }} on ${{ matrix.os }}" @@ -28,13 +31,11 @@ jobs: python-version: # When changing this list, be sure to check the [gh-actions] list in # tox.ini so that tox will run properly. - - "2.7" - - "3.5" - "3.6" - "3.7" - "3.8" - "3.9" - - "3.10.0-alpha.5" + - "3.10.0-rc.2" - "pypy3" exclude: # Windows PyPy doesn't seem to work? @@ -51,11 +52,6 @@ jobs: with: python-version: "${{ matrix.python-version }}" - - name: "Install Visual C++ if needed" - if: runner.os == 'Windows' && matrix.python-version == '2.7' - run: | - choco install vcpython27 -f -y - - name: "Install dependencies" run: | set -xe @@ -70,13 +66,13 @@ jobs: continue-on-error: true id: tox1 run: | - python -m tox + python -m tox -- -rfeXs - name: "Retry tox for ${{ matrix.python-version }}" id: tox2 if: steps.tox1.outcome == 'failure' run: | - python -m tox + python -m tox -- -rfeXs - name: "Set status" if: always() diff --git a/.gitignore b/.gitignore index f8813653a..c8b41f499 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ setuptools-*.egg # Stuff in the test directory. covmain.zip zipmods.zip +zip1.zip # Stuff in the doc directory. doc/_build @@ -42,8 +43,7 @@ doc/sample_html_beta # Build intermediaries. tmp -# Stuff in the ci directory. -*.token - # OS junk .DS_Store + +!.github diff --git a/.treerc b/.treerc index 34862ad4f..a2188587e 100644 --- a/.treerc +++ b/.treerc @@ -2,13 +2,11 @@ [default] ignore = .treerc - .hgtags build htmlcov html0 .tox* .coverage* .metacov - mock.py *.min.js style.css gold sample_html sample_html_beta diff --git a/CHANGES.rst b/CHANGES.rst index afd5f16ae..d9fcc2b8c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,6 +21,122 @@ want to know what's different in 5.0 since 4.5.x, see :ref:`whatsnew5x`. .. Version 9.8.1 --- 2027-07-27 .. ---------------------------- +.. _changes_60: + +Version 6.0 --- 2021-10-03 +-------------------------- + +- The ``coverage html`` command now prints a message indicating where the HTML + report was written. Fixes `issue 1195`_. + +- The ``coverage combine`` command now prints messages indicating each data + file being combined. Fixes `issue 1105`_. + +- The HTML report now includes a sentence about skipped files due to + ``skip_covered`` or ``skip_empty`` settings. Fixes `issue 1163`_. + +- Unrecognized options in the configuration file are no longer errors. They are + now warnings, to ease the use of coverage across versions. Fixes `issue + 1035`_. + +- Fix handling of exceptions through context managers in Python 3.10. A missing + exception is no longer considered a missing branch from the with statement. + Fixes `issue 1205`_. + +- Fix another rarer instance of "Error binding parameter 0 - probably + unsupported type." (`issue 1010`_). + +- Creating a directory for the coverage data file now is safer against + conflicts when two coverage runs happen simultaneously (`pull 1220`_). + Thanks, Clément Pit-Claudel. + +.. _issue 1035: https://github.com/nedbat/coveragepy/issues/1035 +.. _issue 1105: https://github.com/nedbat/coveragepy/issues/1105 +.. _issue 1163: https://github.com/nedbat/coveragepy/issues/1163 +.. _issue 1195: https://github.com/nedbat/coveragepy/issues/1195 +.. _issue 1205: https://github.com/nedbat/coveragepy/issues/1205 +.. _pull 1220: https://github.com/nedbat/coveragepy/pull/1220 + + +.. _changes_60b1: + +Version 6.0b1 --- 2021-07-18 +---------------------------- + +- Dropped support for Python 2.7, PyPy 2, and Python 3.5. + +- Added support for the Python 3.10 ``match/case`` syntax. + +- Data collection is now thread-safe. There may have been rare instances of + exceptions raised in multi-threaded programs. + +- Plugins (like the `Django coverage plugin`_) were generating "Already + imported a file that will be measured" warnings about Django itself. These + have been fixed, closing `issue 1150`_. + +- Warnings generated by coverage.py are now real Python warnings. + +- Using ``--fail-under=100`` with coverage near 100% could result in the + self-contradictory message :code:`total of 100 is less than fail-under=100`. + This bug (`issue 1168`_) is now fixed. + +- The ``COVERAGE_DEBUG_FILE`` environment variable now accepts ``stdout`` and + ``stderr`` to write to those destinations. + +- TOML parsing now uses the `tomli`_ library. + +- Some minor changes to usually invisible details of the HTML report: + + - Use a modern hash algorithm when fingerprinting, for high-security + environments (`issue 1189`_). When generating the HTML report, we save the + hash of the data, to avoid regenerating an unchanged HTML page. We used to + use MD5 to generate the hash, and now use SHA-3-256. This was never a + security concern, but security scanners would notice the MD5 algorithm and + raise a false alarm. + + - Change how report file names are generated, to avoid leading underscores + (`issue 1167`_), to avoid rare file name collisions (`issue 584`_), and to + avoid file names becoming too long (`issue 580`_). + +.. _Django coverage plugin: https://pypi.org/project/django-coverage-plugin/ +.. _issue 580: https://github.com/nedbat/coveragepy/issues/580 +.. _issue 584: https://github.com/nedbat/coveragepy/issues/584 +.. _issue 1150: https://github.com/nedbat/coveragepy/issues/1150 +.. _issue 1167: https://github.com/nedbat/coveragepy/issues/1167 +.. _issue 1168: https://github.com/nedbat/coveragepy/issues/1168 +.. _issue 1189: https://github.com/nedbat/coveragepy/issues/1189 +.. _tomli: https://pypi.org/project/tomli/ + + +.. _changes_56b1: + +Version 5.6b1 --- 2021-04-13 +---------------------------- + +Note: 5.6 final was never released. These changes are part of 6.0. + +- Third-party packages are now ignored in coverage reporting. This solves a + few problems: + + - Coverage will no longer report about other people's code (`issue 876`_). + This is true even when using ``--source=.`` with a venv in the current + directory. + + - Coverage will no longer generate "Already imported a file that will be + measured" warnings about coverage itself (`issue 905`_). + +- The HTML report uses j/k to move up and down among the highlighted chunks of + code. They used to highlight the current chunk, but 5.0 broke that behavior. + Now the highlighting is working again. + +- The JSON report now includes ``percent_covered_display``, a string with the + total percentage, rounded to the same number of decimal places as the other + reports' totals. + +.. _issue 876: https://github.com/nedbat/coveragepy/issues/876 +.. _issue 905: https://github.com/nedbat/coveragepy/issues/905 + + .. _changes_55: Version 5.5 --- 2021-02-28 @@ -412,7 +528,7 @@ Version 5.0b1 --- 2019-11-11 ``coverage html --show-contexts``) will issue a warning if there were no contexts measured (`issue 851`_). -.. _TOML: https://github.com/toml-lang/toml#readme +.. _TOML: https://toml.io/ .. _issue 664: https://github.com/nedbat/coveragepy/issues/664 .. _issue 851: https://github.com/nedbat/coveragepy/issues/851 .. _issue 855: https://github.com/nedbat/coveragepy/issues/855 @@ -530,7 +646,7 @@ Version 5.0a6 --- 2019-07-16 - The deprecated `Reporter.file_reporters` property has been removed. -.. _ShiningPanda: https://wiki.jenkins.io/display/JENKINS/ShiningPanda+Plugin +.. _ShiningPanda: https://plugins.jenkins.io/shiningpanda/ .. _issue 806: https://github.com/nedbat/coveragepy/pull/806 .. _issue 828: https://github.com/nedbat/coveragepy/issues/828 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 76fbd4c31..2642e6b1a 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -37,6 +37,7 @@ Chris Warrick Christian Heimes Christine Lytwynec Christoph Zwerschke +Clément Pit-Claudel Conrad Ho Cosimo Lupo Dan Hemberger diff --git a/MANIFEST.in b/MANIFEST.in index 049ee1fd9..50c1f790c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -26,7 +26,6 @@ include .editorconfig include .readthedocs.yml recursive-include ci * -exclude ci/*.token recursive-include .github * @@ -44,5 +43,3 @@ recursive-include tests *.py *.tok recursive-include tests/gold * recursive-include tests js/* qunit/* prune tests/eggsrc/build - -global-exclude *.py[co] diff --git a/Makefile b/Makefile index d7bc15b7d..d65fc3f50 100644 --- a/Makefile +++ b/Makefile @@ -24,13 +24,11 @@ clean: clean_platform ## Remove artifacts of test execution, i rm -f .coverage .coverage.* coverage.xml .metacov* rm -f .tox/*/lib/*/site-packages/zzz_metacov.pth rm -f */.coverage */*/.coverage */*/*/.coverage */*/*/*/.coverage */*/*/*/*/.coverage */*/*/*/*/*/.coverage - rm -f tests/covmain.zip tests/zipmods.zip - rm -rf tests/eggsrc/build tests/eggsrc/dist tests/eggsrc/*.egg-info - rm -f setuptools-*.egg distribute-*.egg distribute-*.tar.gz + rm -f tests/covmain.zip tests/zipmods.zip tests/zip1.zip rm -rf doc/_build doc/_spell doc/sample_html_beta rm -rf tmp rm -rf .cache .pytest_cache .hypothesis - rm -rf $$TMPDIR/coverage_test + rm -rf tests/actual -make -C tests/gold/html clean sterile: clean ## Remove all non-controlled content, even if expensive. @@ -48,7 +46,7 @@ $(CSS): $(SCSS) LINTABLE = coverage tests igor.py setup.py __main__.py lint: ## Run linters and checkers. - tox -e lint + tox -q -e lint todo: -grep -R --include=*.py TODO $(LINTABLE) @@ -57,21 +55,21 @@ pep8: pycodestyle --filename=*.py --repeat $(LINTABLE) test: - tox -e py27,py35 $(ARGS) + tox -q -e py35 $(ARGS) PYTEST_SMOKE_ARGS = -n 6 -m "not expensive" --maxfail=3 $(ARGS) smoke: ## Run tests quickly with the C tracer in the lowest supported Python versions. - COVERAGE_NO_PYTRACER=1 tox -q -e py27,py35 -- $(PYTEST_SMOKE_ARGS) + COVERAGE_NO_PYTRACER=1 tox -q -e py35 -- $(PYTEST_SMOKE_ARGS) pysmoke: ## Run tests quickly with the Python tracer in the lowest supported Python versions. - COVERAGE_NO_CTRACER=1 tox -q -e py27,py35 -- $(PYTEST_SMOKE_ARGS) + COVERAGE_NO_CTRACER=1 tox -q -e py35 -- $(PYTEST_SMOKE_ARGS) # Coverage measurement of coverage.py itself (meta-coverage). See metacov.ini # for details. metacov: ## Run meta-coverage, measuring ourself. - COVERAGE_COVERAGE=yes tox $(ARGS) + COVERAGE_COVERAGE=yes tox -q $(ARGS) metahtml: ## Produce meta-coverage HTML reports. python igor.py combine_html @@ -84,7 +82,7 @@ kit: ## Make the source distribution. kit_upload: ## Upload the built distributions to PyPI. twine upload --verbose dist/* -test_upload: ## Upload the distrubutions to PyPI's testing server. +test_upload: ## Upload the distributions to PyPI's testing server. twine upload --verbose --repository testpypi dist/* kit_local: @@ -97,7 +95,7 @@ kit_local: find ~/Library/Caches/pip/wheels -name 'coverage-*' -delete download_kits: ## Download the built kits from GitHub. - python ci/download_gha_artifacts.py + python ci/download_gha_artifacts.py nedbat/coveragepy check_kits: ## Check that dist/* are well-formed. python -m twine check dist/* @@ -165,8 +163,5 @@ relnotes_json: $(RELNOTES_JSON) ## Convert changelog to JSON for further parsin $(RELNOTES_JSON): $(CHANGES_MD) $(DOCBIN)/python ci/parse_relnotes.py tmp/rst_rst/changes.md $(RELNOTES_JSON) -tidelift_relnotes: $(RELNOTES_JSON) ## Upload parsed release notes to Tidelift. - $(DOCBIN)/python ci/tidelift_relnotes.py $(RELNOTES_JSON) pypi/coverage - github_releases: $(RELNOTES_JSON) ## Update GitHub releases. $(DOCBIN)/python ci/github_releases.py $(RELNOTES_JSON) nedbat/coveragepy diff --git a/README.rst b/README.rst index 072f30ffe..de045a9f1 100644 --- a/README.rst +++ b/README.rst @@ -17,11 +17,10 @@ Coverage.py measures code coverage, typically during test execution. It uses the code analysis tools and tracing hooks provided in the Python standard library to determine which lines are executable, and which have been executed. -Coverage.py runs on many versions of Python: +Coverage.py runs on these versions of Python: -* CPython 2.7. -* CPython 3.5 through 3.10 alpha. -* PyPy2 7.3.3 and PyPy3 7.3.3. +* CPython 3.6 through 3.10. +* PyPy3 7.3.3. Documentation is on `Read the Docs`_. Code repository and issue tracker are on `GitHub`_. @@ -30,8 +29,8 @@ Documentation is on `Read the Docs`_. Code repository and issue tracker are on .. _GitHub: https://github.com/nedbat/coveragepy -**New in 5.x:** SQLite data storage, JSON report, contexts, relative filenames, -dropped support for Python 2.6, 3.3 and 3.4. +**New in 6.x:** dropped support for Python 2.7 and 3.5; added support for 3.10 +match/case statements. For Enterprise diff --git a/ci/download_gha_artifacts.py b/ci/download_gha_artifacts.py index ed0bbe259..7828d3f85 100644 --- a/ci/download_gha_artifacts.py +++ b/ci/download_gha_artifacts.py @@ -4,8 +4,10 @@ """Use the GitHub API to download built artifacts.""" import datetime +import json import os import os.path +import sys import time import zipfile @@ -18,6 +20,8 @@ def download_url(url, filename): with open(filename, "wb") as f: for chunk in response.iter_content(16*1024): f.write(chunk) + else: + raise Exception(f"Fetching {url} produced: {response.status_code=}") def unpack_zipfile(filename): """Unpack a zipfile, using the names in the zip.""" @@ -41,20 +45,24 @@ def utc2local(timestring): return local.strftime("%Y-%m-%d %H:%M:%S") dest = "dist" -repo_owner = "nedbat/coveragepy" +repo_owner = sys.argv[1] temp_zip = "artifacts.zip" -if not os.path.exists(dest): - os.makedirs(dest) +os.makedirs(dest, exist_ok=True) os.chdir(dest) r = requests.get(f"https://api.github.com/repos/{repo_owner}/actions/artifacts") -dists = [a for a in r.json()["artifacts"] if a["name"] == "dist"] -if not dists: - print("No recent dists!") +if r.status_code == 200: + dists = [a for a in r.json()["artifacts"] if a["name"] == "dist"] + if not dists: + print("No recent dists!") + else: + latest = max(dists, key=lambda a: a["created_at"]) + print(f"Artifacts created at {utc2local(latest['created_at'])}") + download_url(latest["archive_download_url"], temp_zip) + unpack_zipfile(temp_zip) + os.remove(temp_zip) else: - latest = max(dists, key=lambda a: a["created_at"]) - print(f"Artifacts created at {utc2local(latest['created_at'])}") - download_url(latest["archive_download_url"], temp_zip) - unpack_zipfile(temp_zip) - os.remove(temp_zip) + print(f"Fetching artifacts returned status {r.status_code}:") + print(json.dumps(r.json(), indent=4)) + sys.exit(1) diff --git a/ci/github_releases.py b/ci/github_releases.py index 1c7ee6047..86dd7d1cd 100644 --- a/ci/github_releases.py +++ b/ci/github_releases.py @@ -78,7 +78,7 @@ def release_for_relnote(relnote): """ Turn a release note dict into the data needed by GitHub for a release. """ - tag = f"coverage-{relnote['version']}" + tag = relnote['version'] return { "tag_name": tag, "name": tag, @@ -122,7 +122,7 @@ def update_github_releases(json_filename, repo): relnotes = json.load(jf) relnotes.sort(key=lambda rel: pkg_resources.parse_version(rel["version"])) for relnote in relnotes: - tag = "coverage-" + relnote["version"] + tag = relnote["version"] if not does_tag_exist(tag): continue exists = tag in releases diff --git a/ci/tidelift_relnotes.py b/ci/tidelift_relnotes.py deleted file mode 100644 index bc3a37d44..000000000 --- a/ci/tidelift_relnotes.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python3 -""" -Upload release notes from a JSON file to Tidelift as Markdown chunks - -Put your Tidelift API token in a file called tidelift.token alongside this -program, for example: - - user/n3IwOpxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxc2ZwE4 - -Run with two arguments: the JSON file of release notes, and the Tidelift -package name: - - python tidelift_relnotes.py relnotes.json pypi/coverage - -Every section that has something that looks like a version number in it will -be uploaded as the release notes for that version. - -""" - -import json -import os.path -import sys - -import requests - - -def update_release_note(package, version, text): - """Update the release notes for one version of a package.""" - url = f"https://api.tidelift.com/external-api/lifting/{package}/release-notes/{version}" - token_file = os.path.join(os.path.dirname(__file__), "tidelift.token") - with open(token_file) as ftoken: - token = ftoken.read().strip() - headers = { - "Authorization": f"Bearer: {token}", - } - req_args = dict(url=url, data=text.encode('utf8'), headers=headers) - result = requests.post(**req_args) - if result.status_code == 409: - result = requests.put(**req_args) - print(f"{version}: {result.status_code}") - -def upload(json_filename, package): - """Main function: parse markdown and upload to Tidelift.""" - with open(json_filename) as jf: - relnotes = json.load(jf) - for relnote in relnotes: - update_release_note(package, relnote["version"], relnote["text"]) - -if __name__ == "__main__": - upload(*sys.argv[1:]) # pylint: disable=no-value-for-parameter diff --git a/coverage/__init__.py b/coverage/__init__.py index 331b304b6..429a7bd02 100644 --- a/coverage/__init__.py +++ b/coverage/__init__.py @@ -14,7 +14,7 @@ from coverage.control import Coverage, process_startup from coverage.data import CoverageData -from coverage.misc import CoverageException +from coverage.exceptions import CoverageException from coverage.plugin import CoveragePlugin, FileTracer, FileReporter from coverage.pytracer import PyTracer diff --git a/coverage/annotate.py b/coverage/annotate.py index 999ab6e55..a6ee4636c 100644 --- a/coverage/annotate.py +++ b/coverage/annotate.py @@ -3,7 +3,6 @@ """Source file annotation for coverage.py.""" -import io import os import re @@ -14,7 +13,7 @@ os = isolate_module(os) -class AnnotateReporter(object): +class AnnotateReporter: """Generate annotated source files showing line coverage. This reporter creates annotated copies of the measured source files. Each @@ -74,7 +73,7 @@ def annotate_file(self, fr, analysis): else: dest_file = fr.filename + ",cover" - with io.open(dest_file, 'w', encoding='utf8') as dest: + with open(dest_file, 'w', encoding='utf8') as dest: i = 0 j = 0 covered = True @@ -87,22 +86,22 @@ def annotate_file(self, fr, analysis): if i < len(statements) and statements[i] == lineno: covered = j >= len(missing) or missing[j] > lineno if self.blank_re.match(line): - dest.write(u' ') + dest.write(' ') elif self.else_re.match(line): # Special logic for lines containing only 'else:'. if i >= len(statements) and j >= len(missing): - dest.write(u'! ') + dest.write('! ') elif i >= len(statements) or j >= len(missing): - dest.write(u'> ') + dest.write('> ') elif statements[i] == missing[j]: - dest.write(u'! ') + dest.write('! ') else: - dest.write(u'> ') + dest.write('> ') elif lineno in excluded: - dest.write(u'- ') + dest.write('- ') elif covered: - dest.write(u'> ') + dest.write('> ') else: - dest.write(u'! ') + dest.write('! ') dest.write(line) diff --git a/coverage/backward.py b/coverage/backward.py deleted file mode 100644 index ac781ab96..000000000 --- a/coverage/backward.py +++ /dev/null @@ -1,267 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - -"""Add things to old Pythons so I can pretend they are newer.""" - -# This file's purpose is to provide modules to be imported from here. -# pylint: disable=unused-import - -import os -import sys - -from datetime import datetime - -from coverage import env - - -# Pythons 2 and 3 differ on where to get StringIO. -try: - from cStringIO import StringIO -except ImportError: - from io import StringIO - -# In py3, ConfigParser was renamed to the more-standard configparser. -# But there's a py3 backport that installs "configparser" in py2, and I don't -# want it because it has annoying deprecation warnings. So try the real py2 -# import first. -try: - import ConfigParser as configparser -except ImportError: - import configparser - -# What's a string called? -try: - string_class = basestring -except NameError: - string_class = str - -# What's a Unicode string called? -try: - unicode_class = unicode -except NameError: - unicode_class = str - -# range or xrange? -try: - range = xrange # pylint: disable=redefined-builtin -except NameError: - range = range - -try: - from itertools import zip_longest -except ImportError: - from itertools import izip_longest as zip_longest - -# Where do we get the thread id from? -try: - from thread import get_ident as get_thread_id -except ImportError: - from threading import get_ident as get_thread_id - -try: - os.PathLike -except AttributeError: - # This is Python 2 and 3 - path_types = (bytes, string_class, unicode_class) -else: - # 3.6+ - path_types = (bytes, str, os.PathLike) - -# shlex.quote is new, but there's an undocumented implementation in "pipes", -# who knew!? -try: - from shlex import quote as shlex_quote -except ImportError: - # Useful function, available under a different (undocumented) name - # in Python versions earlier than 3.3. - from pipes import quote as shlex_quote - -try: - import reprlib -except ImportError: # pragma: not covered - # We need this on Python 2, but in testing environments, a backport is - # installed, so this import isn't used. - import repr as reprlib - -# A function to iterate listlessly over a dict's items, and one to get the -# items as a list. -try: - {}.iteritems -except AttributeError: - # Python 3 - def iitems(d): - """Produce the items from dict `d`.""" - return d.items() - - def litems(d): - """Return a list of items from dict `d`.""" - return list(d.items()) -else: - # Python 2 - def iitems(d): - """Produce the items from dict `d`.""" - return d.iteritems() - - def litems(d): - """Return a list of items from dict `d`.""" - return d.items() - -# Getting the `next` function from an iterator is different in 2 and 3. -try: - iter([]).next -except AttributeError: - def iternext(seq): - """Get the `next` function for iterating over `seq`.""" - return iter(seq).__next__ -else: - def iternext(seq): - """Get the `next` function for iterating over `seq`.""" - return iter(seq).next - -# Python 3.x is picky about bytes and strings, so provide methods to -# get them right, and make them no-ops in 2.x -if env.PY3: - def to_bytes(s): - """Convert string `s` to bytes.""" - return s.encode('utf8') - - def to_string(b): - """Convert bytes `b` to string.""" - return b.decode('utf8') - - def binary_bytes(byte_values): - """Produce a byte string with the ints from `byte_values`.""" - return bytes(byte_values) - - def byte_to_int(byte): - """Turn a byte indexed from a bytes object into an int.""" - return byte - - def bytes_to_ints(bytes_value): - """Turn a bytes object into a sequence of ints.""" - # In Python 3, iterating bytes gives ints. - return bytes_value - -else: - def to_bytes(s): - """Convert string `s` to bytes (no-op in 2.x).""" - return s - - def to_string(b): - """Convert bytes `b` to string.""" - return b - - def binary_bytes(byte_values): - """Produce a byte string with the ints from `byte_values`.""" - return "".join(chr(b) for b in byte_values) - - def byte_to_int(byte): - """Turn a byte indexed from a bytes object into an int.""" - return ord(byte) - - def bytes_to_ints(bytes_value): - """Turn a bytes object into a sequence of ints.""" - for byte in bytes_value: - yield ord(byte) - - -try: - # In Python 2.x, the builtins were in __builtin__ - BUILTINS = sys.modules['__builtin__'] -except KeyError: - # In Python 3.x, they're in builtins - BUILTINS = sys.modules['builtins'] - - -# imp was deprecated in Python 3.3 -try: - import importlib - import importlib.util - imp = None -except ImportError: - importlib = None - -# We only want to use importlib if it has everything we need. -try: - importlib_util_find_spec = importlib.util.find_spec -except Exception: - import imp - importlib_util_find_spec = None - -# What is the .pyc magic number for this version of Python? -try: - PYC_MAGIC_NUMBER = importlib.util.MAGIC_NUMBER -except AttributeError: - PYC_MAGIC_NUMBER = imp.get_magic() - - -def code_object(fn): - """Get the code object from a function.""" - try: - return fn.func_code - except AttributeError: - return fn.__code__ - - -try: - from types import SimpleNamespace -except ImportError: - # The code from https://docs.python.org/3/library/types.html#types.SimpleNamespace - class SimpleNamespace: - """Python implementation of SimpleNamespace, for Python 2.""" - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - - def __repr__(self): - keys = sorted(self.__dict__) - items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys) - return "{}({})".format(type(self).__name__, ", ".join(items)) - - -def format_local_datetime(dt): - """Return a string with local timezone representing the date. - If python version is lower than 3.6, the time zone is not included. - """ - try: - return dt.astimezone().strftime('%Y-%m-%d %H:%M %z') - except (TypeError, ValueError): - # Datetime.astimezone in Python 3.5 can not handle naive datetime - return dt.strftime('%Y-%m-%d %H:%M') - - -def invalidate_import_caches(): - """Invalidate any import caches that may or may not exist.""" - if importlib and hasattr(importlib, "invalidate_caches"): - importlib.invalidate_caches() - - -def import_local_file(modname, modfile=None): - """Import a local file as a module. - - Opens a file in the current directory named `modname`.py, imports it - as `modname`, and returns the module object. `modfile` is the file to - import if it isn't in the current directory. - - """ - try: - import importlib.util as importlib_util - except ImportError: - importlib_util = None - - if modfile is None: - modfile = modname + '.py' - if importlib_util: - spec = importlib_util.spec_from_file_location(modname, modfile) - mod = importlib_util.module_from_spec(spec) - sys.modules[modname] = mod - spec.loader.exec_module(mod) - else: - for suff in imp.get_suffixes(): # pragma: part covered - if suff[0] == '.py': - break - - with open(modfile, 'r') as f: - # pylint: disable=undefined-loop-variable - mod = imp.load_module(modname, f, modfile, suff) - - return mod diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 0be0cca19..1fa52a976 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -3,10 +3,9 @@ """Command-line support for coverage.py.""" -from __future__ import print_function import glob -import optparse +import optparse # pylint: disable=deprecated-module import os.path import shlex import sys @@ -19,12 +18,12 @@ from coverage.collector import CTracer from coverage.data import line_counts from coverage.debug import info_formatter, info_header, short_stack +from coverage.exceptions import BaseCoverageException, ExceptionDuringRun, NoSource from coverage.execfile import PyRunner -from coverage.misc import BaseCoverageException, ExceptionDuringRun, NoSource, output_encoding -from coverage.results import should_fail_under +from coverage.results import Numbers, should_fail_under -class Opts(object): +class Opts: """A namespace class for individual options we'll build parsers from.""" append = optparse.make_option( @@ -47,8 +46,8 @@ class Opts(object): choices=CONCURRENCY_CHOICES, help=( "Properly measure code using a concurrency library. " - "Valid values are: %s." - ) % ", ".join(CONCURRENCY_CHOICES), + "Valid values are: {}." + ).format(", ".join(CONCURRENCY_CHOICES)), ) context = optparse.make_option( '', '--context', action='store', metavar="LABEL", @@ -176,7 +175,7 @@ class Opts(object): ) source = optparse.make_option( '', '--source', action='store', metavar="SRC1,SRC2,...", - help="A list of packages or directories of code to be measured.", + help="A list of directories or importable names of code to measure.", ) timid = optparse.make_option( '', '--timid', action='store_true', @@ -195,7 +194,7 @@ class Opts(object): ) -class CoverageOptionParser(optparse.OptionParser, object): +class CoverageOptionParser(optparse.OptionParser): """Base OptionParser for coverage.py. Problems don't exit the program. @@ -204,7 +203,7 @@ class CoverageOptionParser(optparse.OptionParser, object): """ def __init__(self, *args, **kwargs): - super(CoverageOptionParser, self).__init__( + super().__init__( add_help_option=False, *args, **kwargs ) self.set_defaults( @@ -251,7 +250,7 @@ def parse_args_ok(self, args=None, options=None): """ try: - options, args = super(CoverageOptionParser, self).parse_args(args, options) + options, args = super().parse_args(args, options) except self.OptionParserError: return False, None, None return True, options, args @@ -266,7 +265,7 @@ class GlobalOptionParser(CoverageOptionParser): """Command-line parser for coverage.py global option arguments.""" def __init__(self): - super(GlobalOptionParser, self).__init__() + super().__init__() self.add_options([ Opts.help, @@ -289,7 +288,7 @@ def __init__(self, action, options, defaults=None, usage=None, description=None) """ if usage: usage = "%prog " + usage - super(CmdOptionParser, self).__init__( + super().__init__( usage=usage, description=description, ) @@ -300,16 +299,16 @@ def __init__(self, action, options, defaults=None, usage=None, description=None) def __eq__(self, other): # A convenience equality, so that I can put strings in unit test # results, and they will compare equal to objects. - return (other == "" % self.cmd) + return (other == f"") __hash__ = None # This object doesn't need to be hashed. def get_prog_name(self): """Override of an undocumented function in optparse.OptionParser.""" - program_name = super(CmdOptionParser, self).get_prog_name() + program_name = super().get_prog_name() # Include the sub-command for this parser as part of the command. - return "{command} {subcommand}".format(command=program_name, subcommand=self.cmd) + return f"{program_name} {self.cmd}" GLOBAL_ARGS = [ @@ -498,7 +497,7 @@ def show_help(error=None, topic=None, parser=None): if error: print(error, file=sys.stderr) - print("Use '%s help' for help." % (program_name,), file=sys.stderr) + print(f"Use '{program_name} help' for help.", file=sys.stderr) elif parser: print(parser.format_help().strip()) print() @@ -507,14 +506,14 @@ def show_help(error=None, topic=None, parser=None): if help_msg: print(help_msg.format(**help_params)) else: - print("Don't know topic %r" % topic) + print(f"Don't know topic {topic!r}") print("Full documentation is at {__url__}".format(**help_params)) OK, ERR, FAIL_UNDER = 0, 1, 2 -class CoverageScript(object): +class CoverageScript: """The command-line interface to coverage.py.""" def __init__(self): @@ -542,7 +541,7 @@ def command_line(self, argv): else: parser = CMDS.get(argv[0]) if not parser: - show_help("Unknown command: '%s'" % argv[0]) + show_help(f"Unknown command: {argv[0]!r}") return ERR argv = argv[1:] @@ -575,6 +574,7 @@ def command_line(self, argv): concurrency=options.concurrency, check_preimported=True, context=options.context, + messages=True, ) if options.action == "debug": @@ -656,8 +656,10 @@ def command_line(self, argv): fail_under = self.coverage.get_option("report:fail_under") precision = self.coverage.get_option("report:precision") if should_fail_under(total, fail_under, precision): - msg = "total of {total:.{p}f} is less than fail-under={fail_under:.{p}f}".format( - total=total, fail_under=fail_under, p=precision, + msg = "total of {total} is less than fail-under={fail_under:.{p}f}".format( + total=Numbers(precision=precision).display_covered(total), + fail_under=fail_under, + p=precision, ) print("Coverage failure:", msg) return FAIL_UNDER @@ -708,7 +710,7 @@ def do_run(self, options, args): command_line = self.coverage.get_option("run:command_line") if command_line is not None: args = shlex.split(command_line) - if args and args[0] == "-m": + if args and args[0] in {"-m", "--module"}: options.module = True args = args[1:] if not args: @@ -766,22 +768,22 @@ def do_debug(self, args): sys_info = self.coverage.sys_info() print(info_header("sys")) for line in info_formatter(sys_info): - print(" %s" % line) + print(f" {line}") elif info == 'data': self.coverage.load() data = self.coverage.get_data() print(info_header("data")) - print("path: %s" % data.data_filename()) + print(f"path: {data.data_filename()}") if data: - print("has_arcs: %r" % data.has_arcs()) + print(f"has_arcs: {data.has_arcs()!r}") summary = line_counts(data, fullpath=True) filenames = sorted(summary.keys()) - print("\n%d files:" % len(filenames)) + print(f"\n{len(filenames)} files:") for f in filenames: - line = "%s: %d lines" % (f, summary[f]) + line = f"{f}: {summary[f]} lines" plugin = data.file_tracer(f) if plugin: - line += " [%s]" % plugin + line += f" [{plugin}]" print(line) else: print("No data collected") @@ -789,12 +791,12 @@ def do_debug(self, args): print(info_header("config")) config_info = self.coverage.config.__dict__.items() for line in info_formatter(config_info): - print(" %s" % line) + print(f" {line}") elif info == "premain": print(info_header("premain")) print(short_stack()) else: - show_help("Don't know what you mean by %r" % info) + show_help(f"Don't know what you mean by {info!r}") return ERR return OK @@ -878,8 +880,6 @@ def main(argv=None): except BaseCoverageException as err: # A controlled error inside coverage.py: print the message to the user. msg = err.args[0] - if env.PY2: - msg = msg.encode(output_encoding()) print(msg) status = ERR except SystemExit as err: diff --git a/coverage/collector.py b/coverage/collector.py index a4f1790dd..73babf44e 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -7,10 +7,10 @@ import sys from coverage import env -from coverage.backward import litems, range # pylint: disable=redefined-builtin from coverage.debug import short_stack from coverage.disposition import FileDisposition -from coverage.misc import CoverageException, isolate_module +from coverage.exceptions import CoverageException +from coverage.misc import isolate_module from coverage.pytracer import PyTracer os = isolate_module(os) @@ -33,7 +33,7 @@ CTracer = None -class Collector(object): +class Collector: """Collects trace data. Creates a Tracer object for each thread, since they track stack @@ -116,7 +116,7 @@ def __init__( # We can handle a few concurrency options here, but only one at a time. these_concurrencies = self.SUPPORTED_CONCURRENCIES.intersection(concurrency) if len(these_concurrencies) > 1: - raise CoverageException("Conflicting concurrency settings: %s" % concurrency) + raise CoverageException(f"Conflicting concurrency settings: {concurrency}") self.concurrency = these_concurrencies.pop() if these_concurrencies else '' try: @@ -136,13 +136,13 @@ def __init__( import threading self.threading = threading else: - raise CoverageException("Don't understand concurrency=%s" % concurrency) - except ImportError: + raise CoverageException(f"Don't understand concurrency={concurrency}") + except ImportError as ex: raise CoverageException( - "Couldn't trace with concurrency=%s, the module isn't installed." % ( + "Couldn't trace with concurrency={}, the module isn't installed.".format( self.concurrency, ) - ) + ) from ex self.reset() @@ -162,7 +162,7 @@ def __init__( self.supports_plugins = False def __repr__(self): - return "" % (id(self), self.tracer_name()) + return f"" def use_data(self, covdata, context): """Use `covdata` for recording data.""" @@ -244,7 +244,7 @@ def _start_tracer(self): tracer.concur_id_func = self.concur_id_func elif self.concur_id_func: raise CoverageException( - "Can't support concurrency=%s with %s, only threads are supported" % ( + "Can't support concurrency={} with {}, only threads are supported".format( self.concurrency, self.tracer_name(), ) ) @@ -318,8 +318,8 @@ def start(self): (frame, event, arg), lineno = args try: fn(frame, event, arg, lineno=lineno) - except TypeError: - raise Exception("fullcoverage must be run with the C trace function.") + except TypeError as ex: + raise Exception("fullcoverage must be run with the C trace function.") from ex # Install our installation tracer in threading, to jump-start other # threads. @@ -332,9 +332,9 @@ def stop(self): if self._collectors[-1] is not self: print("self._collectors:") for c in self._collectors: - print(" {!r}\n{}".format(c, c.origin)) + print(f" {c!r}\n{c.origin}") assert self._collectors[-1] is self, ( - "Expected current collector to be %r, but it's %r" % (self, self._collectors[-1]) + f"Expected current collector to be {self!r}, but it's {self._collectors[-1]!r}" ) self.pause() @@ -353,7 +353,7 @@ def pause(self): if stats: print("\nCoverage.py tracer stats:") for k in sorted(stats.keys()): - print("%20s: %s" % (k, stats[k])) + print(f"{k:>20}: {stats[k]}") if self.threading: self.threading.settrace(None) @@ -390,7 +390,7 @@ def disable_plugin(self, disposition): file_tracer = disposition.file_tracer plugin = file_tracer._coverage_plugin plugin_name = plugin._coverage_plugin_name - self.warn("Disabling plug-in {!r} due to previous exception".format(plugin_name)) + self.warn(f"Disabling plug-in {plugin_name!r} due to previous exception") plugin._coverage_enabled = False disposition.trace = False @@ -404,14 +404,14 @@ def cached_mapped_file(self, filename): def mapped_file_dict(self, d): """Return a dict like d, but with keys modified by file_mapper.""" - # The call to litems() ensures that the GIL protects the dictionary + # The call to list(items()) ensures that the GIL protects the dictionary # iterator against concurrent modifications by tracers running # in other threads. We try three times in case of concurrent # access, hoping to get a clean copy. runtime_err = None for _ in range(3): try: - items = litems(d) + items = list(d.items()) except RuntimeError as ex: runtime_err = ex else: @@ -419,7 +419,7 @@ def mapped_file_dict(self, d): else: raise runtime_err - return dict((self.cached_mapped_file(k), v) for k, v in items if v) + return {self.cached_mapped_file(k): v for k, v in items if v} def plugin_was_disabled(self, plugin): """Record that `plugin` was disabled during the run.""" diff --git a/coverage/config.py b/coverage/config.py index 7ef7e7ae7..3b8735799 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -4,15 +4,14 @@ """Config file for coverage.py""" import collections +import configparser import copy import os import os.path import re -from coverage import env -from coverage.backward import configparser, iitems, string_class -from coverage.misc import contract, CoverageException, isolate_module -from coverage.misc import substitute_variables +from coverage.exceptions import CoverageException +from coverage.misc import contract, isolate_module, substitute_variables from coverage.tomlconfig import TomlConfigParser, TomlDecodeError @@ -35,12 +34,9 @@ def __init__(self, our_file): if our_file: self.section_prefixes.append("") - def read(self, filenames, encoding=None): + def read(self, filenames, encoding_unused=None): """Read a file name as UTF-8 configuration data.""" - kwargs = {} - if env.PYVERSION >= (3, 2): - kwargs['encoding'] = encoding or "utf-8" - return configparser.RawConfigParser.read(self, filenames, **kwargs) + return configparser.RawConfigParser.read(self, filenames, encoding="utf-8") def has_option(self, section, option): for section_prefix in self.section_prefixes: @@ -128,8 +124,8 @@ def getregexlist(self, section, option): re.compile(value) except re.error as e: raise CoverageException( - "Invalid [%s].%s value %r: %s" % (section, option, value, e) - ) + f"Invalid [{section}].{option} value {value!r}: {e}" + ) from e if value: value_list.append(value) return value_list @@ -154,7 +150,7 @@ def getregexlist(self, section, option): ] -class CoverageConfig(object): +class CoverageConfig: """Coverage.py configuration. The attributes of this class are the various settings that control the @@ -245,14 +241,14 @@ def __init__(self): def from_args(self, **kwargs): """Read config values from `kwargs`.""" - for k, v in iitems(kwargs): + for k, v in kwargs.items(): if v is not None: - if k in self.MUST_BE_LIST and isinstance(v, string_class): + if k in self.MUST_BE_LIST and isinstance(v, str): v = [v] setattr(self, k, v) @contract(filename=str) - def from_file(self, filename, our_file): + def from_file(self, filename, warn, our_file): """Read configuration from a .rc file. `filename` is a file name to read. @@ -276,7 +272,7 @@ def from_file(self, filename, our_file): try: files_read = cp.read(filename) except (configparser.Error, TomlDecodeError) as err: - raise CoverageException("Couldn't read config file %s: %s" % (filename, err)) + raise CoverageException(f"Couldn't read config file {filename}: {err}") from err if not files_read: return False @@ -289,7 +285,7 @@ def from_file(self, filename, our_file): if was_set: any_set = True except ValueError as err: - raise CoverageException("Couldn't read config file %s: %s" % (filename, err)) + raise CoverageException(f"Couldn't read config file {filename}: {err}") from err # Check that there are no unrecognized options. all_options = collections.defaultdict(set) @@ -297,12 +293,12 @@ def from_file(self, filename, our_file): section, option = option_spec[1].split(":") all_options[section].add(option) - for section, options in iitems(all_options): + for section, options in all_options.items(): real_section = cp.has_section(section) if real_section: for unknown in set(cp.options(section)) - options: - raise CoverageException( - "Unrecognized option '[%s] %s=' in config file %s" % ( + warn( + "Unrecognized option '[{}] {}=' in config file {}".format( real_section, unknown, filename ) ) @@ -447,7 +443,7 @@ def set_option(self, option_name, value): return # If we get here, we didn't find the option. - raise CoverageException("No such option: %r" % option_name) + raise CoverageException(f"No such option: {option_name!r}") def get_option(self, option_name): """Get an option from the configuration. @@ -475,7 +471,7 @@ def get_option(self, option_name): return self.plugin_options.get(plugin_name, {}).get(key) # If we get here, we didn't find the option. - raise CoverageException("No such option: %r" % option_name) + raise CoverageException(f"No such option: {option_name!r}") def post_process_file(self, path): """Make final adjustments to a file path to make it usable.""" @@ -521,12 +517,13 @@ def config_files_to_try(config_file): return files_to_try -def read_coverage_config(config_file, **kwargs): +def read_coverage_config(config_file, warn, **kwargs): """Read the coverage.py configuration. Arguments: config_file: a boolean or string, see the `Coverage` class for the tricky details. + warn: a function to issue warnings. all others: keyword arguments from the `Coverage` class, used for setting values in the configuration. @@ -545,11 +542,11 @@ def read_coverage_config(config_file, **kwargs): files_to_try = config_files_to_try(config_file) for fname, our_file, specified_file in files_to_try: - config_read = config.from_file(fname, our_file=our_file) + config_read = config.from_file(fname, warn, our_file=our_file) if config_read: break if specified_file: - raise CoverageException("Couldn't read '%s' as a config file" % fname) + raise CoverageException(f"Couldn't read {fname!r} as a config file") # $set_env.py: COVERAGE_DEBUG - Options for --debug. # 3) from environment variables: diff --git a/coverage/context.py b/coverage/context.py index ea13da21e..45e86a5c1 100644 --- a/coverage/context.py +++ b/coverage/context.py @@ -80,7 +80,7 @@ def mro(bases): if f is func: return base.__module__ + '.' + base.__name__ + "." + fname for base in bases: - qname = mro(base.__bases__) + qname = mro(base.__bases__) # pylint: disable=cell-var-from-loop if qname is not None: return qname return None diff --git a/coverage/control.py b/coverage/control.py index 1623b0932..8a55a3174 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -11,27 +11,28 @@ import platform import sys import time +import warnings from coverage import env from coverage.annotate import AnnotateReporter -from coverage.backward import string_class, iitems from coverage.collector import Collector, CTracer from coverage.config import read_coverage_config from coverage.context import should_start_context_test_function, combine_context_switchers from coverage.data import CoverageData, combine_parallel_data from coverage.debug import DebugControl, short_stack, write_formatted_info from coverage.disposition import disposition_debug_msg +from coverage.exceptions import CoverageException, CoverageWarning from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory from coverage.html import HtmlReporter from coverage.inorout import InOrOut from coverage.jsonreport import JsonReporter -from coverage.misc import CoverageException, bool_or_none, join_regex +from coverage.misc import bool_or_none, join_regex from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module from coverage.plugin import FileReporter from coverage.plugin_support import Plugins from coverage.python import PythonFileReporter from coverage.report import render_report -from coverage.results import Analysis, Numbers +from coverage.results import Analysis from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter @@ -61,7 +62,7 @@ def override_config(cov, **kwargs): _DEFAULT_DATAFILE = DefaultValue("MISSING") -class Coverage(object): +class Coverage: """Programmatic access to coverage.py. To use:: @@ -102,6 +103,7 @@ def __init__( auto_data=False, timid=None, branch=None, config_file=True, source=None, source_pkgs=None, omit=None, include=None, debug=None, concurrency=None, check_preimported=False, context=None, + messages=False, ): # pylint: disable=too-many-arguments """ Many of these arguments duplicate and override values that can be @@ -172,6 +174,9 @@ def __init__( `context` is a string to use as the :ref:`static context ` label for collected data. + If `messages` is true, some messages will be printed to stdout + indicating what is happening. + .. versionadded:: 4.0 The `concurrency` parameter. @@ -184,6 +189,9 @@ def __init__( .. versionadded:: 5.3 The `source_pkgs` parameter. + .. versionadded:: 6.0 + The `messages` parameter. + """ # data_file=None means no disk file at all. data_file missing means # use the value from the config file. @@ -191,15 +199,7 @@ def __init__( if data_file is _DEFAULT_DATAFILE: data_file = None - # Build our configuration from a number of sources. - self.config = read_coverage_config( - config_file=config_file, - data_file=data_file, cover_pylib=cover_pylib, timid=timid, - branch=branch, parallel=bool_or_none(data_suffix), - source=source, source_pkgs=source_pkgs, run_omit=omit, run_include=include, debug=debug, - report_omit=omit, report_include=include, - concurrency=concurrency, context=context, - ) + self.config = None # This is injectable by tests. self._debug_file = None @@ -212,6 +212,7 @@ def __init__( self._warn_unimported_source = True self._warn_preimported_source = check_preimported self._no_warn_slugs = None + self._messages = messages # A record of all the warnings that have been issued. self._warnings = [] @@ -234,6 +235,16 @@ def __init__( # Should we write the debug output? self._should_write_debug = True + # Build our configuration from a number of sources. + self.config = read_coverage_config( + config_file=config_file, warn=self._warn, + data_file=data_file, cover_pylib=cover_pylib, timid=timid, + branch=branch, parallel=bool_or_none(data_suffix), + source=source, source_pkgs=source_pkgs, run_omit=omit, run_include=include, debug=debug, + report_omit=omit, report_include=include, + concurrency=concurrency, context=context, + ) + # If we have sub-process measurement happening automatically, then we # want any explicit creation of a Coverage object to mean, this process # is already coverage-aware, so don't auto-measure it. By now, the @@ -291,7 +302,7 @@ def _post_init(self): # '[run] _crash' will raise an exception if the value is close by in # the call stack, for testing error handling. if self.config._crash and self.config._crash in short_stack(limit=4): - raise Exception("Crashing because called by {}".format(self.config._crash)) + raise Exception(f"Crashing because called by {self.config._crash}") def _write_startup_debug(self): """Write out debug info at startup if needed.""" @@ -334,9 +345,9 @@ def _check_include_omit_etc(self, filename, frame): reason = self._inorout.check_include_omit_etc(filename, frame) if self._debug.should('trace'): if not reason: - msg = "Including %r" % (filename,) + msg = f"Including {filename!r}" else: - msg = "Not including %r: %s" % (filename, reason) + msg = f"Not including {filename!r}: {reason}" self._debug.write(msg) return not reason @@ -351,22 +362,29 @@ def _warn(self, msg, slug=None, once=False): """ if self._no_warn_slugs is None: - self._no_warn_slugs = list(self.config.disable_warnings) + if self.config is not None: + self._no_warn_slugs = list(self.config.disable_warnings) - if slug in self._no_warn_slugs: - # Don't issue the warning - return + if self._no_warn_slugs is not None: + if slug in self._no_warn_slugs: + # Don't issue the warning + return self._warnings.append(msg) if slug: - msg = "%s (%s)" % (msg, slug) - if self._debug.should('pid'): - msg = "[%d] %s" % (os.getpid(), msg) - sys.stderr.write("Coverage.py warning: %s\n" % msg) + msg = f"{msg} ({slug})" + if self._debug is not None and self._debug.should('pid'): + msg = f"[{os.getpid()}] {msg}" + warnings.warn(msg, category=CoverageWarning, stacklevel=2) if once: self._no_warn_slugs.append(slug) + def _message(self, msg): + """Write a message to the user, if configured to do so.""" + if self._messages: + print(msg) + def get_option(self, option_name): """Get an option from the configuration. @@ -442,9 +460,7 @@ def _init_for_start(self): elif dycon == "test_function": context_switchers = [should_start_context_test_function] else: - raise CoverageException( - "Don't understand dynamic_context setting: {!r}".format(dycon) - ) + raise CoverageException(f"Don't understand dynamic_context setting: {dycon!r}") context_switchers.extend( plugin.dynamic_context for plugin in self._plugins.context_switchers @@ -465,7 +481,7 @@ def _init_for_start(self): suffix = self._data_suffix_specified if suffix or self.config.parallel: - if not isinstance(suffix, string_class): + if not isinstance(suffix, str): # if data_suffix=True, use .machinename.pid.random suffix = True else: @@ -478,7 +494,7 @@ def _init_for_start(self): # Early warning if we aren't going to be able to support plugins. if self._plugins.file_tracers and not self._collector.supports_plugins: self._warn( - "Plugin file tracers (%s) aren't supported with %s" % ( + "Plugin file tracers ({}) aren't supported with {}".format( ", ".join( plugin._coverage_plugin_name for plugin in self._plugins.file_tracers @@ -562,7 +578,7 @@ def stop(self): def _atexit(self): """Clean up on process shutdown.""" if self._debug.should("process"): - self._debug.write("atexit: pid: {}, instance: {!r}".format(os.getpid(), self)) + self._debug.write(f"atexit: pid: {os.getpid()}, instance: {self!r}") if self._started: self.stop() if self._auto_save: @@ -598,9 +614,7 @@ def switch_context(self, new_context): """ if not self._started: # pragma: part started - raise CoverageException( - "Cannot switch context, coverage is not started" - ) + raise CoverageException("Cannot switch context, coverage is not started") if self._collector.should_start_context: self._warn("Conflicting dynamic contexts", slug="dynamic-conflict", once=True) @@ -704,6 +718,7 @@ def combine(self, data_paths=None, strict=False, keep=False): data_paths=data_paths, strict=strict, keep=keep, + message=self._message, ) def get_data(self): @@ -798,21 +813,20 @@ def _analyze(self, it): """ # All reporting comes through here, so do reporting initialization. self._init() - Numbers.set_precision(self.config.precision) self._post_init() data = self.get_data() if not isinstance(it, FileReporter): it = self._get_file_reporter(it) - return Analysis(data, it, self._file_mapper) + return Analysis(data, self.config.precision, it, self._file_mapper) def _get_file_reporter(self, morf): """Get a FileReporter for a module or file name.""" plugin = None file_reporter = "python" - if isinstance(morf, string_class): + if isinstance(morf, str): mapped_morf = self._file_mapper(morf) plugin_name = self._data.file_tracer(mapped_morf) if plugin_name: @@ -822,7 +836,7 @@ def _get_file_reporter(self, morf): file_reporter = plugin.file_reporter(mapped_morf) if file_reporter is None: raise CoverageException( - "Plugin %r did not provide a file reporter for %r." % ( + "Plugin {!r} did not provide a file reporter for {!r}.".format( plugin._coverage_plugin_name, morf ) ) @@ -969,7 +983,8 @@ def html_report( html_skip_empty=skip_empty, precision=precision, ): reporter = HtmlReporter(self) - return reporter.report(morfs) + ret = reporter.report(morfs) + return ret def xml_report( self, morfs=None, outfile=None, ignore_errors=None, @@ -1036,8 +1051,8 @@ def plugin_info(plugins): return entries info = [ - ('version', covmod.__version__), - ('coverage', covmod.__file__), + ('coverage_version', covmod.__version__), + ('coverage_module', covmod.__file__), ('tracer', self._collector.tracer_name() if self._collector else "-none-"), ('CTracer', 'available' if CTracer else "unavailable"), ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)), @@ -1062,9 +1077,12 @@ def plugin_info(plugins): ('cwd', os.getcwd()), ('path', sys.path), ('environment', sorted( - ("%s = %s" % (k, v)) - for k, v in iitems(os.environ) - if any(slug in k for slug in ("COV", "PY")) + f"{k} = {v}" + for k, v in os.environ.items() + if ( + any(slug in k for slug in ("COV", "PY")) or + (k in ("HOME", "TEMP", "TMP")) + ) )), ('command_line', " ".join(getattr(sys, 'argv', ['-none-']))), ] diff --git a/coverage/ctracer/datastack.h b/coverage/ctracer/datastack.h index 3b3078ba2..c383e1e16 100644 --- a/coverage/ctracer/datastack.h +++ b/coverage/ctracer/datastack.h @@ -12,7 +12,7 @@ * possible. */ typedef struct DataStackEntry { - /* The current file_data dictionary. Owned. */ + /* The current file_data set. Owned. */ PyObject * file_data; /* The disposition object for this frame. A borrowed instance of CFileDisposition. */ diff --git a/coverage/ctracer/filedisp.c b/coverage/ctracer/filedisp.c index 47782ae09..f0052c4a0 100644 --- a/coverage/ctracer/filedisp.c +++ b/coverage/ctracer/filedisp.c @@ -44,7 +44,7 @@ CFileDisposition_members[] = { PyTypeObject CFileDispositionType = { - MyType_HEAD_INIT + PyVarObject_HEAD_INIT(NULL, 0) "coverage.CFileDispositionType", /*tp_name*/ sizeof(CFileDisposition), /*tp_basicsize*/ 0, /*tp_itemsize*/ diff --git a/coverage/ctracer/module.c b/coverage/ctracer/module.c index f308902b6..d564a8128 100644 --- a/coverage/ctracer/module.c +++ b/coverage/ctracer/module.c @@ -9,8 +9,6 @@ #define MODULE_DOC PyDoc_STR("Fast coverage tracer.") -#if PY_MAJOR_VERSION >= 3 - static PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, @@ -69,40 +67,3 @@ PyInit_tracer(void) return mod; } - -#else - -void -inittracer(void) -{ - PyObject * mod; - - mod = Py_InitModule3("coverage.tracer", NULL, MODULE_DOC); - if (mod == NULL) { - return; - } - - if (CTracer_intern_strings() < 0) { - return; - } - - /* Initialize CTracer */ - CTracerType.tp_new = PyType_GenericNew; - if (PyType_Ready(&CTracerType) < 0) { - return; - } - - Py_INCREF(&CTracerType); - PyModule_AddObject(mod, "CTracer", (PyObject *)&CTracerType); - - /* Initialize CFileDisposition */ - CFileDispositionType.tp_new = PyType_GenericNew; - if (PyType_Ready(&CFileDispositionType) < 0) { - return; - } - - Py_INCREF(&CFileDispositionType); - PyModule_AddObject(mod, "CFileDisposition", (PyObject *)&CFileDispositionType); -} - -#endif /* Py3k */ diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c index 00e4218d8..00d9f106a 100644 --- a/coverage/ctracer/tracer.c +++ b/coverage/ctracer/tracer.c @@ -13,7 +13,7 @@ static int pyint_as_int(PyObject * pyint, int *pint) { - int the_int = MyInt_AsInt(pyint); + int the_int = (int)PyLong_AsLong(pyint); if (the_int == -1 && PyErr_Occurred()) { return RET_ERROR; } @@ -39,7 +39,7 @@ CTracer_intern_strings(void) int ret = RET_ERROR; #define INTERN_STRING(v, s) \ - v = MyText_InternFromString(s); \ + v = PyUnicode_InternFromString(s); \ if (v == NULL) { \ goto error; \ } @@ -133,14 +133,15 @@ indent(int n) static BOOL logging = FALSE; /* Set these constants to be a file substring and line number to start logging. */ -static const char * start_file = "tests/views"; -static int start_line = 27; +static const char * start_file = "nested.py"; +static int start_line = 1; static void -showlog(int depth, int lineno, PyObject * filename, const char * msg) +CTracer_showlog(CTracer * self, int lineno, PyObject * filename, const char * msg) { if (logging) { - printf("%s%3d ", indent(depth), depth); + int depth = self->pdata_stack->depth; + printf("%x: %s%3d ", (int)self, indent(depth), depth); if (lineno) { printf("%4d", lineno); } @@ -148,8 +149,8 @@ showlog(int depth, int lineno, PyObject * filename, const char * msg) printf(" "); } if (filename) { - PyObject *ascii = MyText_AS_BYTES(filename); - printf(" %s", MyBytes_AS_STRING(ascii)); + PyObject *ascii = PyUnicode_AsASCIIString(filename); + printf(" %s", PyBytes_AS_STRING(ascii)); Py_DECREF(ascii); } if (msg) { @@ -159,9 +160,9 @@ showlog(int depth, int lineno, PyObject * filename, const char * msg) } } -#define SHOWLOG(a,b,c,d) showlog(a,b,c,d) +#define SHOWLOG(l,f,m) CTracer_showlog(self,l,f,m) #else -#define SHOWLOG(a,b,c,d) +#define SHOWLOG(l,f,m) #endif /* TRACE_LOG */ #if WHAT_LOG @@ -181,7 +182,7 @@ CTracer_record_pair(CTracer *self, int l1, int l2) goto error; } - if (PyDict_SetItem(self->pcur_entry->file_data, t, Py_None) < 0) { + if (PySet_Add(self->pcur_entry->file_data, t) < 0) { goto error; } @@ -232,7 +233,7 @@ CTracer_set_pdata_stack(CTracer *self) /* A new concurrency object. Make a new data stack. */ the_index = self->data_stacks_used; - stack_index = MyInt_FromInt(the_index); + stack_index = PyLong_FromLong((long)the_index); if (stack_index == NULL) { goto error; } @@ -305,7 +306,7 @@ CTracer_check_missing_return(CTracer *self, PyFrameObject *frame) goto error; } } - SHOWLOG(self->pdata_stack->depth, PyFrame_GetLineNumber(frame), frame->f_code->co_filename, "missedreturn"); + SHOWLOG(PyFrame_GetLineNumber(frame), MyFrame_GetCode(frame)->co_filename, "missedreturn"); self->pdata_stack->depth--; self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth]; } @@ -384,7 +385,7 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) } /* Check if we should trace this line. */ - filename = frame->f_code->co_filename; + filename = MyFrame_GetCode(frame)->co_filename; disposition = PyDict_GetItem(self->should_trace_cache, filename); if (disposition == NULL) { if (PyErr_Occurred()) { @@ -503,7 +504,7 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) if (PyErr_Occurred()) { goto error; } - file_data = PyDict_New(); + file_data = PySet_New(NULL); if (file_data == NULL) { goto error; } @@ -529,27 +530,27 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) self->pcur_entry->file_data = file_data; self->pcur_entry->file_tracer = file_tracer; - SHOWLOG(self->pdata_stack->depth, PyFrame_GetLineNumber(frame), filename, "traced"); + SHOWLOG(PyFrame_GetLineNumber(frame), filename, "traced"); } else { Py_XDECREF(self->pcur_entry->file_data); self->pcur_entry->file_data = NULL; self->pcur_entry->file_tracer = Py_None; - SHOWLOG(self->pdata_stack->depth, PyFrame_GetLineNumber(frame), filename, "skipped"); + SHOWLOG(PyFrame_GetLineNumber(frame), filename, "skipped"); } self->pcur_entry->disposition = disposition; /* Make the frame right in case settrace(gettrace()) happens. */ Py_INCREF(self); - My_XSETREF(frame->f_trace, (PyObject*)self); + Py_XSETREF(frame->f_trace, (PyObject*)self); /* A call event is really a "start frame" event, and can happen for * re-entering a generator also. f_lasti is -1 for a true call, and a * real byte offset for a generator re-entry. */ if (frame->f_lasti < 0) { - self->pcur_entry->last_line = -frame->f_code->co_firstlineno; + self->pcur_entry->last_line = -MyFrame_GetCode(frame)->co_firstlineno; } else { self->pcur_entry->last_line = PyFrame_GetLineNumber(frame); @@ -633,7 +634,7 @@ CTracer_handle_line(CTracer *self, PyFrameObject *frame) STATS( self->stats.lines++; ) if (self->pdata_stack->depth >= 0) { - SHOWLOG(self->pdata_stack->depth, PyFrame_GetLineNumber(frame), frame->f_code->co_filename, "line"); + SHOWLOG(PyFrame_GetLineNumber(frame), MyFrame_GetCode(frame)->co_filename, "line"); if (self->pcur_entry->file_data) { int lineno_from = -1; int lineno_to = -1; @@ -668,12 +669,12 @@ CTracer_handle_line(CTracer *self, PyFrameObject *frame) } else { /* Tracing lines: key is simply this_line. */ - PyObject * this_line = MyInt_FromInt(lineno_from); + PyObject * this_line = PyLong_FromLong((long)lineno_from); if (this_line == NULL) { goto error; } - ret2 = PyDict_SetItem(self->pcur_entry->file_data, this_line, Py_None); + ret2 = PySet_Add(self->pcur_entry->file_data, this_line); Py_DECREF(this_line); if (ret2 < 0) { goto error; @@ -714,14 +715,14 @@ CTracer_handle_return(CTracer *self, PyFrameObject *frame) * f_lasti before reading the byte. */ int bytecode = RETURN_VALUE; - PyObject * pCode = frame->f_code->co_code; - int lasti = frame->f_lasti; + PyObject * pCode = MyFrame_GetCode(frame)->co_code; + int lasti = MyFrame_lasti(frame); - if (lasti < MyBytes_GET_SIZE(pCode)) { - bytecode = MyBytes_AS_STRING(pCode)[lasti]; + if (lasti < PyBytes_GET_SIZE(pCode)) { + bytecode = PyBytes_AS_STRING(pCode)[lasti]; } if (bytecode != YIELD_VALUE) { - int first = frame->f_code->co_firstlineno; + int first = MyFrame_GetCode(frame)->co_firstlineno; if (CTracer_record_pair(self, self->pcur_entry->last_line, -first) < 0) { goto error; } @@ -744,7 +745,7 @@ CTracer_handle_return(CTracer *self, PyFrameObject *frame) } /* Pop the stack. */ - SHOWLOG(self->pdata_stack->depth, PyFrame_GetLineNumber(frame), frame->f_code->co_filename, "return"); + SHOWLOG(PyFrame_GetLineNumber(frame), MyFrame_GetCode(frame)->co_filename, "return"); self->pdata_stack->depth--; self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth]; } @@ -775,7 +776,7 @@ CTracer_handle_exception(CTracer *self, PyFrameObject *frame) */ STATS( self->stats.exceptions++; ) self->last_exc_back = frame->f_back; - self->last_exc_firstlineno = frame->f_code->co_firstlineno; + self->last_exc_firstlineno = MyFrame_GetCode(frame)->co_firstlineno; return RET_OK; } @@ -806,15 +807,15 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse #if WHAT_LOG if (what <= (int)(sizeof(what_sym)/sizeof(const char *))) { - ascii = MyText_AS_BYTES(frame->f_code->co_filename); - printf("trace: %s @ %s %d\n", what_sym[what], MyBytes_AS_STRING(ascii), PyFrame_GetLineNumber(frame)); + ascii = PyUnicode_AsASCIIString(MyFrame_GetCode(frame)->co_filename); + printf("%x trace: f:%x %s @ %s %d\n", (int)self, (int)frame, what_sym[what], PyBytes_AS_STRING(ascii), PyFrame_GetLineNumber(frame)); Py_DECREF(ascii); } #endif #if TRACE_LOG - ascii = MyText_AS_BYTES(frame->f_code->co_filename); - if (strstr(MyBytes_AS_STRING(ascii), start_file) && PyFrame_GetLineNumber(frame) == start_line) { + ascii = PyUnicode_AsASCIIString(MyFrame_GetCode(frame)->co_filename); + if (strstr(PyBytes_AS_STRING(ascii), start_file) && PyFrame_GetLineNumber(frame) == start_line) { logging = TRUE; } Py_DECREF(ascii); @@ -913,7 +914,7 @@ CTracer_call(CTracer *self, PyObject *args, PyObject *kwds) static char *kwlist[] = {"frame", "event", "arg", "lineno", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!O!O|i:Tracer_call", kwlist, - &PyFrame_Type, &frame, &MyText_Type, &what_str, &arg, &lineno)) { + &PyFrame_Type, &frame, &PyUnicode_Type, &what_str, &arg, &lineno)) { goto done; } @@ -921,8 +922,8 @@ CTracer_call(CTracer *self, PyObject *args, PyObject *kwds) for the C function. */ for (what = 0; what_names[what]; what++) { int should_break; - ascii = MyText_AS_BYTES(what_str); - should_break = !strcmp(MyBytes_AS_STRING(ascii), what_names[what]); + ascii = PyUnicode_AsASCIIString(what_str); + should_break = !strcmp(PyBytes_AS_STRING(ascii), what_names[what]); Py_DECREF(ascii); if (should_break) { break; @@ -930,8 +931,8 @@ CTracer_call(CTracer *self, PyObject *args, PyObject *kwds) } #if WHAT_LOG - ascii = MyText_AS_BYTES(frame->f_code->co_filename); - printf("pytrace: %s @ %s %d\n", what_sym[what], MyBytes_AS_STRING(ascii), PyFrame_GetLineNumber(frame)); + ascii = PyUnicode_AsASCIIString(MyFrame_GetCode(frame)->co_filename); + printf("pytrace: %s @ %s %d\n", what_sym[what], PyBytes_AS_STRING(ascii), PyFrame_GetLineNumber(frame)); Py_DECREF(ascii); #endif @@ -1108,7 +1109,7 @@ CTracer_methods[] = { PyTypeObject CTracerType = { - MyType_HEAD_INIT + PyVarObject_HEAD_INIT(NULL, 0) "coverage.CTracer", /*tp_name*/ sizeof(CTracer), /*tp_basicsize*/ 0, /*tp_itemsize*/ diff --git a/coverage/ctracer/tracer.h b/coverage/ctracer/tracer.h index 8994a9e3d..fbbfa202c 100644 --- a/coverage/ctracer/tracer.h +++ b/coverage/ctracer/tracer.h @@ -39,15 +39,14 @@ typedef struct CTracer { PyObject * context; /* - The data stack is a stack of dictionaries. Each dictionary collects + The data stack is a stack of sets. Each set collects data for a single source file. The data stack parallels the call stack: each call pushes the new frame's file data onto the data stack, and each return pops file data off. - The file data is a dictionary whose form depends on the tracing options. - If tracing arcs, the keys are line number pairs. If not tracing arcs, - the keys are line numbers. In both cases, the value is irrelevant - (None). + The file data is a set whose form depends on the tracing options. + If tracing arcs, the values are line number pairs. If not tracing arcs, + the values are line numbers. */ DataStack data_stack; /* Used if we aren't doing concurrency. */ diff --git a/coverage/ctracer/util.h b/coverage/ctracer/util.h index 5cba9b309..a0b0e236e 100644 --- a/coverage/ctracer/util.h +++ b/coverage/ctracer/util.h @@ -12,45 +12,20 @@ #undef COLLECT_STATS /* Collect counters: stats are printed when tracer is stopped. */ #undef DO_NOTHING /* Define this to make the tracer do nothing. */ -/* Py 2.x and 3.x compatibility */ - -#if PY_MAJOR_VERSION >= 3 - -#define MyText_Type PyUnicode_Type -#define MyText_AS_BYTES(o) PyUnicode_AsASCIIString(o) -#define MyBytes_GET_SIZE(o) PyBytes_GET_SIZE(o) -#define MyBytes_AS_STRING(o) PyBytes_AS_STRING(o) -#define MyText_AsString(o) PyUnicode_AsUTF8(o) -#define MyText_FromFormat PyUnicode_FromFormat -#define MyInt_FromInt(i) PyLong_FromLong((long)i) -#define MyInt_AsInt(o) (int)PyLong_AsLong(o) -#define MyText_InternFromString(s) PyUnicode_InternFromString(s) - -#define MyType_HEAD_INIT PyVarObject_HEAD_INIT(NULL, 0) - +// The f_lasti field changed meaning in 3.10.0a7. It had been bytes, but +// now is instructions, so we need to adjust it to use it as a byte index. +#if PY_VERSION_HEX >= 0x030A00A7 +#define MyFrame_lasti(f) (f->f_lasti * 2) #else +#define MyFrame_lasti(f) f->f_lasti +#endif // 3.10.0a7 -#define MyText_Type PyString_Type -#define MyText_AS_BYTES(o) (Py_INCREF(o), o) -#define MyBytes_GET_SIZE(o) PyString_GET_SIZE(o) -#define MyBytes_AS_STRING(o) PyString_AS_STRING(o) -#define MyText_AsString(o) PyString_AsString(o) -#define MyText_FromFormat PyUnicode_FromFormat -#define MyInt_FromInt(i) PyInt_FromLong((long)i) -#define MyInt_AsInt(o) (int)PyInt_AsLong(o) -#define MyText_InternFromString(s) PyString_InternFromString(s) - -#define MyType_HEAD_INIT PyObject_HEAD_INIT(NULL) 0, - -#endif /* Py3k */ - -// Undocumented, and not in all 2.7.x, so our own copy of it. -#define My_XSETREF(op, op2) \ - do { \ - PyObject *_py_tmp = (PyObject *)(op); \ - (op) = (op2); \ - Py_XDECREF(_py_tmp); \ - } while (0) +// Access f_code should be done through a helper starting in 3.9. +#if PY_VERSION_HEX >= 0x03090000 +#define MyFrame_GetCode(f) (PyFrame_GetCode(f)) +#else +#define MyFrame_GetCode(f) ((f)->f_code) +#endif // 3.11 /* The values returned to indicate ok or error. */ #define RET_OK 0 diff --git a/coverage/data.py b/coverage/data.py index 5dd1dfe3f..68ba7ec33 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -13,7 +13,8 @@ import glob import os.path -from coverage.misc import CoverageException, file_be_gone +from coverage.exceptions import CoverageException +from coverage.misc import file_be_gone from coverage.sqldata import CoverageData @@ -52,7 +53,9 @@ def add_data_to_hash(data, filename, hasher): hasher.update(data.file_tracer(filename)) -def combine_parallel_data(data, aliases=None, data_paths=None, strict=False, keep=False): +def combine_parallel_data( + data, aliases=None, data_paths=None, strict=False, keep=False, message=None, +): """Combine a number of data files together. Treat `data.filename` as a file prefix, and combine the data from all @@ -90,7 +93,7 @@ def combine_parallel_data(data, aliases=None, data_paths=None, strict=False, kee pattern = os.path.join(os.path.abspath(p), localdot) files_to_combine.extend(glob.glob(pattern)) else: - raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) + raise CoverageException(f"Couldn't combine from non-existent path '{p}'") if strict and not files_to_combine: raise CoverageException("No data to combine") @@ -101,10 +104,10 @@ def combine_parallel_data(data, aliases=None, data_paths=None, strict=False, kee # Sometimes we are combining into a file which is one of the # parallel files. Skip that file. if data._debug.should('dataio'): - data._debug.write("Skipping combining ourself: %r" % (f,)) + data._debug.write(f"Skipping combining ourself: {f!r}") continue if data._debug.should('dataio'): - data._debug.write("Combining data file %r" % (f,)) + data._debug.write(f"Combining data file {f!r}") try: new_data = CoverageData(f, debug=data._debug) new_data.read() @@ -116,9 +119,11 @@ def combine_parallel_data(data, aliases=None, data_paths=None, strict=False, kee else: data.update(new_data, aliases=aliases) files_combined += 1 + if message: + message(f"Combined data file {os.path.relpath(f)}") if not keep: if data._debug.should('dataio'): - data._debug.write("Deleting combined data file %r" % (f,)) + data._debug.write(f"Deleting combined data file {f!r}") file_be_gone(f) if strict and not files_combined: diff --git a/coverage/debug.py b/coverage/debug.py index 194f16f50..da4093ffa 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -6,16 +6,17 @@ import contextlib import functools import inspect +import io import itertools import os import pprint +import reprlib import sys try: import _thread except ImportError: import thread as _thread -from coverage.backward import reprlib, StringIO from coverage.misc import isolate_module os = isolate_module(os) @@ -28,7 +29,7 @@ FORCED_DEBUG_FILE = None -class DebugControl(object): +class DebugControl: """Control and output for debugging.""" show_repr_attr = False # For SimpleReprMixin @@ -49,7 +50,7 @@ def __init__(self, options, output): self.raw_output = self.output.outfile def __repr__(self): - return "" % (self.options, self.raw_output) + return f"" def should(self, option): """Decide whether to output debug information in category `option`.""" @@ -77,7 +78,7 @@ def write(self, msg): if self.should('self'): caller_self = inspect.stack()[1][0].f_locals.get('self') if caller_self is not None: - self.output.write("self: {!r}\n".format(caller_self)) + self.output.write(f"self: {caller_self!r}\n") if self.should('callers'): dump_stack_frames(out=self.output, skip=1) self.output.flush() @@ -86,14 +87,14 @@ def write(self, msg): class DebugControlString(DebugControl): """A `DebugControl` that writes to a StringIO, for testing.""" def __init__(self, options): - super(DebugControlString, self).__init__(options, StringIO()) + super().__init__(options, io.StringIO()) def get_output(self): """Get the output text from the `DebugControl`.""" return self.raw_output.getvalue() -class NoDebugging(object): +class NoDebugging: """A replacement for DebugControl that will never try to do anything.""" def should(self, option): # pylint: disable=unused-argument """Should we write debug messages? Never.""" @@ -183,12 +184,12 @@ def short_id(id64): def add_pid_and_tid(text): """A filter to add pid and tid to debug messages.""" # Thread ids are useful, but too long. Make a shorter one. - tid = "{:04x}".format(short_id(_thread.get_ident())) - text = "{:5d}.{}: {}".format(os.getpid(), tid, text) + tid = f"{short_id(_thread.get_ident()):04x}" + text = f"{os.getpid():5d}.{tid}: {text}" return text -class SimpleReprMixin(object): +class SimpleReprMixin: """A mixin implementing a simple __repr__.""" simple_repr_ignore = ['simple_repr_ignore', '$coverage.object_id'] @@ -202,7 +203,7 @@ def __repr__(self): return "<{klass} @0x{id:x} {attrs}>".format( klass=self.__class__.__name__, id=id(self), - attrs=" ".join("{}={!r}".format(k, v) for k, v in show_attrs), + attrs=" ".join(f"{k}={v!r}" for k, v in show_attrs), ) @@ -245,7 +246,7 @@ def filter_text(text, filters): return text + ending -class CwdTracker(object): # pragma: debugging +class CwdTracker: # pragma: debugging """A class to add cwd info to debug messages.""" def __init__(self): self.cwd = None @@ -254,12 +255,12 @@ def filter(self, text): """Add a cwd message for each new cwd.""" cwd = os.getcwd() if cwd != self.cwd: - text = "cwd is now {!r}\n".format(cwd) + text + text = f"cwd is now {cwd!r}\n" + text self.cwd = cwd return text -class DebugOutputFile(object): # pragma: debugging +class DebugOutputFile: # pragma: debugging """A file-like object that includes pid and cwd information.""" def __init__(self, outfile, show_process, filters): self.outfile = outfile @@ -268,10 +269,10 @@ def __init__(self, outfile, show_process, filters): if self.show_process: self.filters.insert(0, CwdTracker().filter) - self.write("New process: executable: %r\n" % (sys.executable,)) - self.write("New process: cmd: %r\n" % (getattr(sys, 'argv', None),)) + self.write(f"New process: executable: {sys.executable!r}\n") + self.write("New process: cmd: {!r}\n".format(getattr(sys, 'argv', None))) if hasattr(os, 'getppid'): - self.write("New process: pid: %r, parent pid: %r\n" % (os.getpid(), os.getppid())) + self.write(f"New process: pid: {os.getpid()!r}, parent pid: {os.getppid()!r}\n") SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one' @@ -306,7 +307,9 @@ def get_one(cls, fileobj=None, show_process=True, filters=(), interim=False): if the_one is None or is_interim: if fileobj is None: debug_file_name = os.environ.get("COVERAGE_DEBUG_FILE", FORCED_DEBUG_FILE) - if debug_file_name: + if debug_file_name in ("stdout", "stderr"): + fileobj = getattr(sys, debug_file_name) + elif debug_file_name: fileobj = open(debug_file_name, "a") else: fileobj = sys.stderr @@ -370,7 +373,7 @@ def _decorator(func): def _wrapper(self, *args, **kwargs): oid = getattr(self, OBJ_ID_ATTR, None) if oid is None: - oid = "{:08d} {:04d}".format(os.getpid(), next(OBJ_IDS)) + oid = f"{os.getpid():08d} {next(OBJ_IDS):04d}" setattr(self, OBJ_ID_ATTR, oid) extra = "" if show_args: @@ -386,11 +389,11 @@ def _wrapper(self, *args, **kwargs): extra += " @ " extra += "; ".join(_clean_stack_line(l) for l in short_stack().splitlines()) callid = next(CALLS) - msg = "{} {:04d} {}{}\n".format(oid, callid, func.__name__, extra) + msg = f"{oid} {callid:04d} {func.__name__}{extra}\n" DebugOutputFile.get_one(interim=True).write(msg) ret = func(self, *args, **kwargs) if show_return: - msg = "{} {:04d} {} return {!r}\n".format(oid, callid, func.__name__, ret) + msg = f"{oid} {callid:04d} {func.__name__} return {ret!r}\n" DebugOutputFile.get_one(interim=True).write(msg) return ret return _wrapper diff --git a/coverage/disposition.py b/coverage/disposition.py index 9b9a997d8..dfcc6def3 100644 --- a/coverage/disposition.py +++ b/coverage/disposition.py @@ -4,7 +4,7 @@ """Simple value objects for tracking what to do with files.""" -class FileDisposition(object): +class FileDisposition: """A simple value type for recording what to do with a file.""" pass @@ -29,9 +29,9 @@ def disposition_init(cls, original_filename): def disposition_debug_msg(disp): """Make a nice debug message of what the FileDisposition is doing.""" if disp.trace: - msg = "Tracing %r" % (disp.original_filename,) + msg = f"Tracing {disp.original_filename!r}" if disp.file_tracer: msg += ": will be traced by %r" % disp.file_tracer else: - msg = "Not tracing %r: %s" % (disp.original_filename, disp.reason) + msg = f"Not tracing {disp.original_filename!r}: {disp.reason}" return msg diff --git a/coverage/env.py b/coverage/env.py index ea78a5be8..c300f8029 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -20,26 +20,21 @@ # Python versions. We amend version_info with one more value, a zero if an # official version, or 1 if built from source beyond an official version. PYVERSION = sys.version_info + (int(platform.python_version()[-1] == "+"),) -PY2 = PYVERSION < (3, 0) -PY3 = PYVERSION >= (3, 0) if PYPY: PYPYVERSION = sys.pypy_version_info -PYPY2 = PYPY and PY2 -PYPY3 = PYPY and PY3 - # Python behavior. -class PYBEHAVIOR(object): +class PYBEHAVIOR: """Flags indicating this Python's behavior.""" + # Does Python conform to PEP626, Precise line numbers for debugging and other tools. + # https://www.python.org/dev/peps/pep-0626 pep626 = CPYTHON and (PYVERSION > (3, 10, 0, 'alpha', 4)) # Is "if __debug__" optimized away? - if PYPY3: + if PYPY: optimize_if_debug = True - elif PYPY2: - optimize_if_debug = False else: optimize_if_debug = not pep626 @@ -47,7 +42,7 @@ class PYBEHAVIOR(object): optimize_if_not_debug = (not PYPY) and (PYVERSION >= (3, 7, 0, 'alpha', 4)) if pep626: optimize_if_not_debug = False - if PYPY3: + if PYPY: optimize_if_not_debug = True # Is "if not __debug__" optimized away even better? @@ -55,23 +50,8 @@ class PYBEHAVIOR(object): if pep626: optimize_if_not_debug2 = False - # Do we have yield-from? - yield_from = (PYVERSION >= (3, 3)) - - # Do we have PEP 420 namespace packages? - namespaces_pep420 = (PYVERSION >= (3, 3)) - - # Do .pyc files have the source file size recorded in them? - size_in_pyc = (PYVERSION >= (3, 3)) - - # Do we have async and await syntax? - async_syntax = (PYVERSION >= (3, 5)) - - # PEP 448 defined additional unpacking generalizations - unpackings_pep448 = (PYVERSION >= (3, 5)) - # Can co_lnotab have negative deltas? - negative_lnotab = (PYVERSION >= (3, 6)) and not (PYPY and PYPYVERSION < (7, 2)) + negative_lnotab = not (PYPY and PYPYVERSION < (7, 2)) # Do .pyc files conform to PEP 552? Hash-based pyc's. hashed_pyc_pep552 = (PYVERSION >= (3, 7, 0, 'alpha', 4)) @@ -80,7 +60,10 @@ class PYBEHAVIOR(object): # used to be an empty string (meaning the current directory). It changed # to be the actual path to the current directory, so that os.chdir wouldn't # affect the outcome. - actual_syspath0_dash_m = CPYTHON and (PYVERSION >= (3, 7, 0, 'beta', 3)) + actual_syspath0_dash_m = ( + (CPYTHON and (PYVERSION >= (3, 7, 0, 'beta', 3))) or + (PYPY and (PYPYVERSION >= (7, 3, 4))) + ) # 3.7 changed how functions with only docstrings are numbered. docstring_only_function = (not PYPY) and ((3, 7, 0, 'beta', 5) <= PYVERSION <= (3, 10)) @@ -116,6 +99,16 @@ class PYBEHAVIOR(object): # Are "if 0:" lines (and similar) kept in the compiled code? keep_constant_test = pep626 + # When leaving a with-block, do we visit the with-line again for the exit? + exit_through_with = (PYVERSION >= (3, 10, 0, 'beta')) + + # Match-case construct. + match_case = (PYVERSION >= (3, 10)) + + # Some words are keywords in some places, identifiers in other places. + soft_keywords = (PYVERSION >= (3, 10)) + + # Coverage.py specifics. # Are we using the C-implemented trace function? diff --git a/coverage/exceptions.py b/coverage/exceptions.py new file mode 100644 index 000000000..6631e1adc --- /dev/null +++ b/coverage/exceptions.py @@ -0,0 +1,53 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Exceptions coverage.py can raise.""" + + +class BaseCoverageException(Exception): + """The base of all Coverage exceptions.""" + pass + + +class CoverageException(BaseCoverageException): + """An exception raised by a coverage.py function.""" + pass + + +class NoSource(CoverageException): + """We couldn't find the source for a module.""" + pass + + +class NoCode(NoSource): + """We couldn't find any code at all.""" + pass + + +class NotPython(CoverageException): + """A source file turned out not to be parsable Python.""" + pass + + +class ExceptionDuringRun(CoverageException): + """An exception happened while running customer code. + + Construct it with three arguments, the values from `sys.exc_info`. + + """ + pass + + +class StopEverything(BaseCoverageException): + """An exception that means everything should stop. + + The CoverageTest class converts these to SkipTest, so that when running + tests, raising this exception will automatically skip the test. + + """ + pass + + +class CoverageWarning(Warning): + """A warning from Coverage.py.""" + pass diff --git a/coverage/execfile.py b/coverage/execfile.py index 29409d517..f46955bce 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -3,6 +3,8 @@ """Execute files of Python code.""" +import importlib.machinery +import importlib.util import inspect import marshal import os @@ -11,17 +13,18 @@ import types from coverage import env -from coverage.backward import BUILTINS -from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec +from coverage.exceptions import CoverageException, ExceptionDuringRun, NoCode, NoSource from coverage.files import canonical_filename, python_reported_file -from coverage.misc import CoverageException, ExceptionDuringRun, NoCode, NoSource, isolate_module +from coverage.misc import isolate_module from coverage.phystokens import compile_unicode from coverage.python import get_python_source os = isolate_module(os) -class DummyLoader(object): +PYC_MAGIC_NUMBER = importlib.util.MAGIC_NUMBER + +class DummyLoader: """A shim for the pep302 __loader__, emulating pkgutil.ImpLoader. Currently only implements the .fullname attribute @@ -30,79 +33,36 @@ def __init__(self, fullname, *_args): self.fullname = fullname -if importlib_util_find_spec: - def find_module(modulename): - """Find the module named `modulename`. +def find_module(modulename): + """Find the module named `modulename`. - Returns the file path of the module, the name of the enclosing - package, and the spec. - """ - try: - spec = importlib_util_find_spec(modulename) - except ImportError as err: - raise NoSource(str(err)) + Returns the file path of the module, the name of the enclosing + package, and the spec. + """ + try: + spec = importlib.util.find_spec(modulename) + except ImportError as err: + raise NoSource(str(err)) from err + if not spec: + raise NoSource(f"No module named {modulename!r}") + pathname = spec.origin + packagename = spec.name + if spec.submodule_search_locations: + mod_main = modulename + ".__main__" + spec = importlib.util.find_spec(mod_main) if not spec: - raise NoSource("No module named %r" % (modulename,)) + raise NoSource( + "No module named %s; " + "%r is a package and cannot be directly executed" + % (mod_main, modulename) + ) pathname = spec.origin packagename = spec.name - if spec.submodule_search_locations: - mod_main = modulename + ".__main__" - spec = importlib_util_find_spec(mod_main) - if not spec: - raise NoSource( - "No module named %s; " - "%r is a package and cannot be directly executed" - % (mod_main, modulename) - ) - pathname = spec.origin - packagename = spec.name - packagename = packagename.rpartition(".")[0] - return pathname, packagename, spec -else: - def find_module(modulename): - """Find the module named `modulename`. - - Returns the file path of the module, the name of the enclosing - package, and None (where a spec would have been). - """ - openfile = None - glo, loc = globals(), locals() - try: - # Search for the module - inside its parent package, if any - using - # standard import mechanics. - if '.' in modulename: - packagename, name = modulename.rsplit('.', 1) - package = __import__(packagename, glo, loc, ['__path__']) - searchpath = package.__path__ - else: - packagename, name = None, modulename - searchpath = None # "top-level search" in imp.find_module() - openfile, pathname, _ = imp.find_module(name, searchpath) - - # Complain if this is a magic non-file module. - if openfile is None and pathname is None: - raise NoSource( - "module does not live in a file: %r" % modulename - ) - - # If `modulename` is actually a package, not a mere module, then we - # pretend to be Python 2.7 and try running its __main__.py script. - if openfile is None: - packagename = modulename - name = '__main__' - package = __import__(packagename, glo, loc, ['__path__']) - searchpath = package.__path__ - openfile, pathname, _ = imp.find_module(name, searchpath) - except ImportError as err: - raise NoSource(str(err)) - finally: - if openfile: - openfile.close() + packagename = packagename.rpartition(".")[0] + return pathname, packagename, spec - return pathname, packagename, None - -class PyRunner(object): +class PyRunner: """Multi-stage execution of Python code. This is meant to emulate real Python execution as closely as possible. @@ -176,29 +136,25 @@ def _prepare2(self): # directory. for ext in [".py", ".pyc", ".pyo"]: try_filename = os.path.join(self.arg0, "__main__" + ext) + # 3.8.10 changed how files are reported when running a + # directory. But I'm not sure how far this change is going to + # spread, so I'll just hard-code it here for now. + if env.PYVERSION >= (3, 8, 10): + try_filename = os.path.abspath(try_filename) if os.path.exists(try_filename): self.arg0 = try_filename break else: raise NoSource("Can't find '__main__' module in '%s'" % self.arg0) - if env.PY2: - self.arg0 = os.path.abspath(self.arg0) - # Make a spec. I don't know if this is the right way to do it. - try: - import importlib.machinery - except ImportError: - pass - else: - try_filename = python_reported_file(try_filename) - self.spec = importlib.machinery.ModuleSpec("__main__", None, origin=try_filename) - self.spec.has_location = True + try_filename = python_reported_file(try_filename) + self.spec = importlib.machinery.ModuleSpec("__main__", None, origin=try_filename) + self.spec.has_location = True self.package = "" self.loader = DummyLoader("__main__") else: - if env.PY3: - self.loader = DummyLoader("__main__") + self.loader = DummyLoader("__main__") self.arg0 = python_reported_file(self.arg0) @@ -220,7 +176,7 @@ def run(self): if self.spec is not None: main_mod.__spec__ = self.spec - main_mod.__builtins__ = BUILTINS + main_mod.__builtins__ = sys.modules['builtins'] sys.modules['__main__'] = main_mod @@ -236,8 +192,8 @@ def run(self): except CoverageException: raise except Exception as exc: - msg = "Couldn't run '{filename}' as Python code: {exc.__class__.__name__}: {exc}" - raise CoverageException(msg.format(filename=self.arg0, exc=exc)) + msg = f"Couldn't run '{self.arg0}' as Python code: {exc.__class__.__name__}: {exc}" + raise CoverageException(msg) from exc # Execute the code object. # Return to the original directory in case the test code exits in @@ -270,7 +226,7 @@ def run(self): sys.excepthook(typ, err, tb.tb_next) except SystemExit: # pylint: disable=try-except-raise raise - except Exception: + except Exception as exc: # Getting the output right in the case of excepthook # shenanigans is kind of involved. sys.stderr.write("Error in sys.excepthook:\n") @@ -280,7 +236,7 @@ def run(self): err2.__traceback__ = err2.__traceback__.tb_next sys.__excepthook__(typ2, err2, tb2.tb_next) sys.stderr.write("\nOriginal exception was:\n") - raise ExceptionDuringRun(typ, err, tb.tb_next) + raise ExceptionDuringRun(typ, err, tb.tb_next) from exc else: sys.exit(1) finally: @@ -321,8 +277,8 @@ def make_code_from_py(filename): # Open the source file. try: source = get_python_source(filename) - except (IOError, NoSource): - raise NoSource("No file to run: '%s'" % filename) + except (OSError, NoSource) as exc: + raise NoSource(f"No file to run: '{filename}'") from exc code = compile_unicode(source, filename, "exec") return code @@ -332,15 +288,15 @@ def make_code_from_pyc(filename): """Get a code object from a .pyc file.""" try: fpyc = open(filename, "rb") - except IOError: - raise NoCode("No file to run: '%s'" % filename) + except OSError as exc: + raise NoCode(f"No file to run: '{filename}'") from exc with fpyc: # First four bytes are a version-specific magic number. It has to # match or we won't run the file. magic = fpyc.read(4) if magic != PYC_MAGIC_NUMBER: - raise NoCode("Bad magic number in .pyc file: {} != {}".format(magic, PYC_MAGIC_NUMBER)) + raise NoCode(f"Bad magic number in .pyc file: {magic} != {PYC_MAGIC_NUMBER}") date_based = True if env.PYBEHAVIOR.hashed_pyc_pep552: @@ -352,9 +308,8 @@ def make_code_from_pyc(filename): if date_based: # Skip the junk in the header that we don't need. fpyc.read(4) # Skip the moddate. - if env.PYBEHAVIOR.size_in_pyc: - # 3.3 added another long to the header (size), skip it. - fpyc.read(4) + # 3.3 added another long to the header (size), skip it. + fpyc.read(4) # The rest of the file is the code object we want. code = marshal.load(fpyc) diff --git a/coverage/files.py b/coverage/files.py index 59b2bd61d..252e42ec5 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -13,8 +13,8 @@ import sys from coverage import env -from coverage.backward import unicode_class -from coverage.misc import contract, CoverageException, join_regex, isolate_module +from coverage.exceptions import CoverageException +from coverage.misc import contract, join_regex, isolate_module os = isolate_module(os) @@ -48,7 +48,7 @@ def relative_filename(filename): fnorm = os.path.normcase(filename) if fnorm.startswith(RELATIVE_DIR): filename = filename[len(RELATIVE_DIR):] - return unicode_filename(filename) + return filename @contract(returns='unicode') @@ -77,7 +77,7 @@ def canonical_filename(filename): return CANONICAL_FILENAME_CACHE[filename] -MAX_FLAT = 200 +MAX_FLAT = 100 @contract(filename='unicode', returns='unicode') def flat_rootname(filename): @@ -87,15 +87,16 @@ def flat_rootname(filename): the same directory, but need to differentiate same-named files from different directories. - For example, the file a/b/c.py will return 'a_b_c_py' + For example, the file a/b/c.py will return 'd_86bbcbe134d28fd2_c_py' """ - name = ntpath.splitdrive(filename)[1] - name = re.sub(r"[\\/.:]", "_", name) - if len(name) > MAX_FLAT: - h = hashlib.sha1(name.encode('UTF-8')).hexdigest() - name = name[-(MAX_FLAT-len(h)-1):] + '_' + h - return name + dirname, basename = ntpath.split(filename) + if dirname: + fp = hashlib.new("sha3_256", dirname.encode("UTF-8")).hexdigest()[:16] + prefix = f"d_{fp}_" + else: + prefix = "" + return prefix + basename.replace(".", "_") if env.WINDOWS: @@ -105,8 +106,6 @@ def flat_rootname(filename): def actual_path(path): """Get the actual path of `path`, including the correct case.""" - if env.PY2 and isinstance(path, unicode_class): - path = path.encode(sys.getfilesystemencoding()) if path in _ACTUAL_PATH_CACHE: return _ACTUAL_PATH_CACHE[path] @@ -143,21 +142,6 @@ def actual_path(filename): return filename -if env.PY2: - @contract(returns='unicode') - def unicode_filename(filename): - """Return a Unicode version of `filename`.""" - if isinstance(filename, str): - encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() - filename = filename.decode(encoding, "replace") - return filename -else: - @contract(filename='unicode', returns='unicode') - def unicode_filename(filename): - """Return a Unicode version of `filename`.""" - return filename - - @contract(returns='unicode') def abs_file(path): """Return the absolute normalized form of `path`.""" @@ -167,7 +151,6 @@ def abs_file(path): pass path = os.path.abspath(path) path = actual_path(path) - path = unicode_filename(path) return path @@ -207,7 +190,7 @@ def prep_patterns(patterns): return prepped -class TreeMatcher(object): +class TreeMatcher: """A matcher for files in a tree. Construct with a list of paths, either files or directories. Paths match @@ -215,18 +198,21 @@ class TreeMatcher(object): somewhere in a subtree rooted at one of the directories. """ - def __init__(self, paths): - self.paths = list(paths) + def __init__(self, paths, name): + self.original_paths = list(paths) + self.paths = list(map(os.path.normcase, paths)) + self.name = name def __repr__(self): - return "" % self.paths + return f"" def info(self): """A list of strings for displaying when dumping state.""" - return self.paths + return self.original_paths def match(self, fpath): """Does `fpath` indicate a file in one of our trees?""" + fpath = os.path.normcase(fpath) for p in self.paths: if fpath.startswith(p): if fpath == p: @@ -238,13 +224,14 @@ def match(self, fpath): return False -class ModuleMatcher(object): +class ModuleMatcher: """A matcher for modules in a tree.""" - def __init__(self, module_names): + def __init__(self, module_names, name): self.modules = list(module_names) + self.name = name def __repr__(self): - return "" % (self.modules) + return f"" def info(self): """A list of strings for displaying when dumping state.""" @@ -266,14 +253,15 @@ def match(self, module_name): return False -class FnmatchMatcher(object): +class FnmatchMatcher: """A matcher for files by file name pattern.""" - def __init__(self, pats): + def __init__(self, pats, name): self.pats = list(pats) self.re = fnmatches_to_regex(self.pats, case_insensitive=env.WINDOWS) + self.name = name def __repr__(self): - return "" % self.pats + return f"" def info(self): """A list of strings for displaying when dumping state.""" @@ -327,7 +315,7 @@ def fnmatches_to_regex(patterns, case_insensitive=False, partial=False): return compiled -class PathAliases(object): +class PathAliases: """A collection of aliases for paths. When combining data files from remote machines, often the paths to source @@ -344,7 +332,7 @@ def __init__(self): def pprint(self): # pragma: debugging """Dump the important parts of the PathAliases, for debugging.""" for regex, result in self.aliases: - print("{!r} --> {!r}".format(regex.pattern, result)) + print(f"{regex.pattern!r} --> {result!r}") def add(self, pattern, result): """Add the `pattern`/`result` pair to the list of aliases. diff --git a/coverage/fullcoverage/encodings.py b/coverage/fullcoverage/encodings.py index aeb416e40..b248bdbc4 100644 --- a/coverage/fullcoverage/encodings.py +++ b/coverage/fullcoverage/encodings.py @@ -18,7 +18,7 @@ import sys -class FullCoverageTracer(object): +class FullCoverageTracer: def __init__(self): # `traces` is a list of trace events. Frames are tricky: the same # frame object is used for a whole scope, with new line numbers diff --git a/coverage/html.py b/coverage/html.py index 0dfee7ca8..208554c8e 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -8,13 +8,13 @@ import os import re import shutil +import types import coverage -from coverage import env -from coverage.backward import iitems, SimpleNamespace, format_local_datetime from coverage.data import add_data_to_hash +from coverage.exceptions import CoverageException from coverage.files import flat_rootname -from coverage.misc import CoverageException, ensure_dir, file_be_gone, Hasher, isolate_module +from coverage.misc import ensure_dir, file_be_gone, Hasher, isolate_module, format_local_datetime from coverage.report import get_analysis_to_report from coverage.results import Numbers from coverage.templite import Templite @@ -56,7 +56,7 @@ def data_filename(fname, pkgdir=""): else: tried.append(static_filename) raise CoverageException( - "Couldn't find static file %r from %r, tried: %r" % (fname, os.getcwd(), tried) + f"Couldn't find static file {fname!r} from {os.getcwd()!r}, tried: {tried!r}" ) @@ -73,7 +73,7 @@ def write_html(fname, html): fout.write(html.encode('ascii', 'xmlcharrefreplace')) -class HtmlDataGeneration(object): +class HtmlDataGeneration: """Generate structured data to be turned into HTML reports.""" EMPTY = "(empty)" @@ -127,10 +127,10 @@ def data_for_file(self, fr, analysis): if contexts == [self.EMPTY]: contexts_label = self.EMPTY else: - contexts_label = "{} ctx".format(len(contexts)) + contexts_label = f"{len(contexts)} ctx" context_list = contexts - lines.append(SimpleNamespace( + lines.append(types.SimpleNamespace( tokens=tokens, number=lineno, category=category, @@ -142,7 +142,7 @@ def data_for_file(self, fr, analysis): long_annotations=long_annotations, )) - file_data = SimpleNamespace( + file_data = types.SimpleNamespace( relative_filename=fr.relative_filename(), nums=analysis.numbers, lines=lines, @@ -151,7 +151,7 @@ def data_for_file(self, fr, analysis): return file_data -class HtmlReporter(object): +class HtmlReporter: """HTML reporting.""" # These files will be copied from the htmlfiles directory to the output @@ -179,11 +179,11 @@ def __init__(self, cov): self.skip_covered = self.config.skip_covered self.skip_empty = self.config.html_skip_empty if self.skip_empty is None: - self.skip_empty= self.config.skip_empty + self.skip_empty = self.config.skip_empty + self.skipped_covered_count = 0 + self.skipped_empty_count = 0 title = self.config.html_title - if env.PY2: - title = title.decode("utf8") if self.config.extra_css: self.extra_css = os.path.basename(self.config.extra_css) @@ -197,7 +197,7 @@ def __init__(self, cov): self.all_files_nums = [] self.incr = IncrementalChecker(self.directory) self.datagen = HtmlDataGeneration(self.coverage) - self.totals = Numbers() + self.totals = Numbers(precision=self.config.precision) self.template_globals = { # Functions available in the templates. @@ -286,12 +286,14 @@ def html_file(self, fr, analysis): if no_missing_lines and no_missing_branches: # If there's an existing file, remove it. file_be_gone(html_path) + self.skipped_covered_count += 1 return if self.skip_empty: # Don't report on empty files. if nums.n_statements == 0: file_be_gone(html_path) + self.skipped_empty_count += 1 return # Find out if the file on disk is already correct. @@ -310,15 +312,15 @@ def html_file(self, fr, analysis): else: tok_html = escape(tok_text) or ' ' html.append( - u'{}'.format(tok_type, tok_html) + f'{tok_html}' ) ldata.html = ''.join(html) if ldata.short_annotations: # 202F is NARROW NO-BREAK SPACE. # 219B is RIGHTWARDS ARROW WITH STROKE. - ldata.annotate = u",   ".join( - u"{} ↛ {}".format(ldata.number, d) + ldata.annotate = ",   ".join( + f"{ldata.number} ↛ {d}" for d in ldata.short_annotations ) else: @@ -329,10 +331,10 @@ def html_file(self, fr, analysis): if len(longs) == 1: ldata.annotate_long = longs[0] else: - ldata.annotate_long = u"{:d} missed branches: {}".format( + ldata.annotate_long = "{:d} missed branches: {}".format( len(longs), - u", ".join( - u"{:d}) {}".format(num, ann_long) + ", ".join( + f"{num:d}) {ann_long}" for num, ann_long in enumerate(longs, start=1) ), ) @@ -360,18 +362,36 @@ def index_file(self): """Write the index.html file for this report.""" index_tmpl = Templite(read_data("index.html"), self.template_globals) + skipped_covered_msg = skipped_empty_msg = "" + if self.skipped_covered_count: + msg = "{} {} skipped due to complete coverage." + skipped_covered_msg = msg.format( + self.skipped_covered_count, + "file" if self.skipped_covered_count == 1 else "files", + ) + if self.skipped_empty_count: + msg = "{} empty {} skipped." + skipped_empty_msg = msg.format( + self.skipped_empty_count, + "file" if self.skipped_empty_count == 1 else "files", + ) + html = index_tmpl.render({ 'files': self.file_summaries, 'totals': self.totals, + 'skipped_covered_msg': skipped_covered_msg, + 'skipped_empty_msg': skipped_empty_msg, }) - write_html(os.path.join(self.directory, "index.html"), html) + index_file = os.path.join(self.directory, "index.html") + write_html(index_file, html) + self.coverage._message(f"Wrote HTML report to {index_file}") # Write the latest hashes for next time. self.incr.write() -class IncrementalChecker(object): +class IncrementalChecker: """Logic and data to support incremental reporting.""" STATUS_FILE = "status.json" @@ -421,7 +441,7 @@ def read(self): status_file = os.path.join(self.directory, self.STATUS_FILE) with open(status_file) as fstatus: status = json.load(fstatus) - except (IOError, ValueError): + except (OSError, ValueError): usable = False else: usable = True @@ -432,7 +452,7 @@ def read(self): if usable: self.files = {} - for filename, fileinfo in iitems(status['files']): + for filename, fileinfo in status['files'].items(): fileinfo['index']['nums'] = Numbers(*fileinfo['index']['nums']) self.files[filename] = fileinfo self.globals = status['globals'] @@ -443,7 +463,7 @@ def write(self): """Write the current status.""" status_file = os.path.join(self.directory, self.STATUS_FILE) files = {} - for filename, fileinfo in iitems(self.files): + for filename, fileinfo in self.files.items(): fileinfo['index']['nums'] = fileinfo['index']['nums'].init_args() files[filename] = fileinfo diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js index 27b49b36f..30d3a067f 100644 --- a/coverage/htmlfiles/coverage_html.js +++ b/coverage/htmlfiles/coverage_html.js @@ -29,8 +29,8 @@ coverage.wire_up_help_panel = function () { var koff = $("#keyboard_icon").offset(); var poff = $("#panel_icon").position(); $(".help_panel").offset({ - top: koff.top-poff.top, - left: koff.left-poff.left + top: koff.top-poff.top-1, + left: koff.left-poff.left-1 }); }); $("#panel_icon").click(function () { @@ -311,11 +311,6 @@ coverage.line_elt = function (n) { return $("#t" + n); }; -// Return the nth line number div. -coverage.num_elt = function (n) { - return $("#n" + n); -}; - // Set the selection. b and e are line numbers. coverage.set_sel = function (b, e) { // The first line selected. @@ -514,9 +509,9 @@ coverage.show_selection = function () { var c = coverage; // Highlight the lines in the chunk - $(".linenos .highlight").removeClass("highlight"); + $("#source .highlight").removeClass("highlight"); for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) { - c.num_elt(probe).addClass("highlight"); + c.line_elt(probe).addClass("highlight"); } c.scroll_to_selection(); diff --git a/coverage/htmlfiles/index.html b/coverage/htmlfiles/index.html index 983db0612..3654d66a0 100644 --- a/coverage/htmlfiles/index.html +++ b/coverage/htmlfiles/index.html @@ -104,6 +104,13 @@

{{ title|escape }}:

No items found using the specified filter.

+ + {% if skipped_covered_msg %} +

{{ skipped_covered_msg }}

+ {% endif %} + {% if skipped_empty_msg %} +

{{ skipped_empty_msg }}

+ {% endif %}