diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 5686493e2..c305477bc 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -37,7 +37,7 @@ jobs: - "3.7" - "3.8" - "3.9" - - "3.10.0-rc.2" + - "3.10" - "pypy3" exclude: # Windows PyPy doesn't seem to work? diff --git a/.github/workflows/kit.yml b/.github/workflows/kit.yml index 726cefaca..c6b9e43da 100644 --- a/.github/workflows/kit.yml +++ b/.github/workflows/kit.yml @@ -122,6 +122,7 @@ jobs: prerel: name: "Build ${{ matrix.python-version }} wheels on ${{ matrix.os }}" + if: ${{ false }} # disable for now, since there are no pre-rel Python versions. runs-on: ${{ matrix.os }} strategy: matrix: diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index 49df01e6e..6b0de1b3d 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -35,7 +35,7 @@ jobs: - "3.7" - "3.8" - "3.9" - - "3.10.0-rc.2" + - "3.10" - "pypy3" exclude: # Windows PyPy doesn't seem to work? diff --git a/CHANGES.rst b/CHANGES.rst index d9fcc2b8c..9d810dbde 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,9 +9,7 @@ These changes are listed in decreasing version number order. Note this can be different from a strict chronological order when there are two branches in development at the same time, such as 4.5.x and 5.0. -This list is detailed and covers changes in each pre-release version. If you -want to know what's different in 5.0 since 4.5.x, see :ref:`whatsnew5x`. - +This list is detailed and covers changes in each pre-release version. .. When updating the "Unreleased" header to a specific version, use this .. format. Don't forget the jump target: @@ -21,6 +19,29 @@ want to know what's different in 5.0 since 4.5.x, see :ref:`whatsnew5x`. .. Version 9.8.1 --- 2027-07-27 .. ---------------------------- +.. _changes_601: + +Version 6.0.1 --- 2021-10-06 +---------------------------- + +- In 6.0, the coverage.py exceptions moved from coverage.misc to + coverage.exceptions. These exceptions are not part of the public supported + API, CoverageException is. But a number of other third-party packages were + importing the exceptions from coverage.misc, so they are now available from + there again (`issue 1226`_). + +- Changed an internal detail of how tomli is imported, so that tomli can use + coverage.py for their own test suite (`issue 1228`_). + +- Defend against an obscure possibility under code obfuscation, where a + function can have an argument called "self", but no local named "self" + (`pull request 1210`_). Thanks, Ben Carlsson. + +.. _pull request 1210: https://github.com/nedbat/coveragepy/pull/1210 +.. _issue 1226: https://github.com/nedbat/coveragepy/issues/1226 +.. _issue 1228: https://github.com/nedbat/coveragepy/issues/1228 + + .. _changes_60: Version 6.0 --- 2021-10-03 @@ -462,6 +483,9 @@ Version 5.0 --- 2019-12-14 Nothing new beyond 5.0b2. +A summary of major changes in 5.0 since 4.5.x is in see :ref:`whatsnew5x`. + + .. _changes_50b2: diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 2642e6b1a..1c1fe0e9c 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -20,6 +20,7 @@ Arcadiy Ivanov Aron Griffis Artem Dayneko Arthur Deygin +Ben Carlsson Ben Finney Bernát Gábor Bill Hart diff --git a/coverage/context.py b/coverage/context.py index 45e86a5c1..43d2b1cc7 100644 --- a/coverage/context.py +++ b/coverage/context.py @@ -48,7 +48,7 @@ def qualname_from_frame(frame): fname = co.co_name method = None if co.co_argcount and co.co_varnames[0] == "self": - self = frame.f_locals["self"] + self = frame.f_locals.get("self", None) method = getattr(self, fname, None) if method is None: diff --git a/coverage/misc.py b/coverage/misc.py index 11dad23e0..0f985be0e 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -5,6 +5,7 @@ import errno import hashlib +import importlib import importlib.util import inspect import locale @@ -19,6 +20,11 @@ from coverage import env from coverage.exceptions import CoverageException +# In 6.0, the exceptions moved from misc.py to exceptions.py. But a number of +# other packages were importing the exceptions from misc, so import them here. +# pylint: disable=unused-wildcard-import +from coverage.exceptions import * # pylint: disable=wildcard-import + ISOLATED_MODULES = {} @@ -43,6 +49,32 @@ def isolate_module(mod): os = isolate_module(os) +def import_third_party(modname): + """Import a third-party module we need, but might not be installed. + + This also cleans out the module after the import, so that coverage won't + appear to have imported it. This lets the third party use coverage for + their own tests. + + Arguments: + modname (str): the name of the module to import. + + Returns: + The imported module, or None if the module couldn't be imported. + + """ + try: + mod = importlib.import_module(modname) + except ImportError: + mod = None + + imported = [m for m in sys.modules if m.startswith(modname)] + for name in imported: + del sys.modules[name] + + return mod + + def dummy_decorator_with_args(*args_unused, **kwargs_unused): """Dummy no-op implementation of a decorator with arguments.""" def _decorator(func): diff --git a/coverage/tomlconfig.py b/coverage/tomlconfig.py index 203192c93..3301acc8e 100644 --- a/coverage/tomlconfig.py +++ b/coverage/tomlconfig.py @@ -8,13 +8,10 @@ import re from coverage.exceptions import CoverageException -from coverage.misc import substitute_variables +from coverage.misc import import_third_party, substitute_variables # TOML support is an install-time extra option. -try: - import tomli -except ImportError: # pragma: not covered - tomli = None +tomli = import_third_party("tomli") class TomlDecodeError(Exception): diff --git a/coverage/version.py b/coverage/version.py index 98bbda8b5..c9b537e37 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (6, 0, 0, "final", 0) +version_info = (6, 0, 1, "final", 0) def _make_version(major, minor, micro, releaselevel, serial): diff --git a/doc/conf.py b/doc/conf.py index 5874b6376..5109ff726 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -67,9 +67,9 @@ # The short X.Y version. version = "6.0" # CHANGEME # The full version, including alpha/beta/rc tags. -release = "6.0" # CHANGEME +release = "6.0.1" # CHANGEME # The date of release, in "monthname day, year" format. -release_date = "October 3, 2021" # CHANGEME +release_date = "October 6, 2021" # CHANGEME rst_epilog = """ .. |release_date| replace:: {release_date} diff --git a/doc/index.rst b/doc/index.rst index 2b4a6a45c..dc34c3f79 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -23,7 +23,7 @@ supported on: .. ifconfig:: prerelease **This is a pre-release build. The usual warnings about possible bugs - apply.** The latest stable version is coverage.py 6.0, `described here`_. + apply.** The latest stable version is coverage.py 6.0.1, `described here`_. .. _described here: http://coverage.readthedocs.io/ @@ -222,6 +222,5 @@ More information contributing trouble faq - whatsnew5x changes sleepy diff --git a/doc/whatsnew5x.rst b/doc/whatsnew5x.rst index bf0fe6cae..f49739ef0 100644 --- a/doc/whatsnew5x.rst +++ b/doc/whatsnew5x.rst @@ -1,6 +1,8 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 .. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt +:orphan: + .. _whatsnew5x: ==================== diff --git a/tests/test_context.py b/tests/test_context.py index 3f80803bd..36eff2f0d 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -5,6 +5,7 @@ import inspect import os.path +from unittest import mock import coverage from coverage.context import qualname_from_frame @@ -275,3 +276,10 @@ def test_bug_829(self): # A class with a name like a function shouldn't confuse qualname_from_frame. class test_something: # pylint: disable=unused-variable assert get_qualname() is None + + def test_bug_1210(self): + # Under pyarmor (an obfuscator), a function can have a "self" argument, + # but then not have a "self" local. + co = mock.Mock(co_name="a_co_name", co_argcount=1, co_varnames=["self"]) + frame = mock.Mock(f_code=co, f_locals={}) + assert qualname_from_frame(frame) == "unittest.mock.a_co_name" diff --git a/tests/test_misc.py b/tests/test_misc.py index 3858c4f8b..077c24344 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -3,11 +3,13 @@ """Tests of miscellaneous stuff.""" +import sys + import pytest from coverage.exceptions import CoverageException from coverage.misc import contract, dummy_decorator_with_args, file_be_gone -from coverage.misc import Hasher, one_of, substitute_variables +from coverage.misc import Hasher, one_of, substitute_variables, import_third_party from coverage.misc import USE_CONTRACTS from tests.coveragetest import CoverageTest @@ -155,3 +157,19 @@ def test_substitute_variables_errors(text): substitute_variables(text, VARS) assert text in str(exc_info.value) assert "Variable NOTHING is undefined" in str(exc_info.value) + + +class ImportThirdPartyTest(CoverageTest): + """Test import_third_party.""" + + run_in_temp_dir = False + + def test_success(self): + mod = import_third_party("pytest") + assert mod.__name__ == "pytest" + assert "pytest" not in sys.modules + + def test_failure(self): + mod = import_third_party("xyzzy") + assert mod is None + assert "xyzzy" not in sys.modules