diff -Nru python-coverage-6.5.0+dfsg1/CHANGES.rst python-coverage-7.2.7+dfsg1/CHANGES.rst --- python-coverage-6.5.0+dfsg1/CHANGES.rst 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/CHANGES.rst 2023-05-29 19:46:30.000000000 +0000 @@ -17,13 +17,475 @@ .. Version 9.8.1 — 2027-07-27 .. -------------------------- +.. scriv-start-here + +.. _changes_7-2-7: + +Version 7.2.7 — 2023-05-29 +-------------------------- + +- Fix: reverted a `change from 6.4.3 `_ that helped Cython, but + also increased the size of data files when using dynamic contexts, as + described in the now-fixed `issue 1586`_. The problem is now avoided due to a + recent change (`issue 1538`_). Thanks to `Anders Kaseorg `_ + and David Szotten for persisting with problem reports and detailed diagnoses. + +- Wheels are now provided for CPython 3.12. + +.. _issue 1586: https://github.com/nedbat/coveragepy/issues/1586 +.. _pull 1629: https://github.com/nedbat/coveragepy/pull/1629 + + +.. _changes_7-2-6: + +Version 7.2.6 — 2023-05-23 +-------------------------- + +- Fix: the ``lcov`` command could raise an IndexError exception if a file is + translated to Python but then executed under its own name. Jinja2 does this + when rendering templates. Fixes `issue 1553`_. + +- Python 3.12 beta 1 now inlines comprehensions. Previously they were compiled + as invisible functions and coverage.py would warn you if they weren't + completely executed. This no longer happens under Python 3.12. + +- Fix: the ``coverage debug sys`` command includes some environment variables + in its output. This could have included sensitive data. Those values are + now hidden with asterisks, closing `issue 1628`_. + +.. _issue 1553: https://github.com/nedbat/coveragepy/issues/1553 +.. _issue 1628: https://github.com/nedbat/coveragepy/issues/1628 + + +.. _changes_7-2-5: + +Version 7.2.5 — 2023-04-30 +-------------------------- + +- Fix: ``html_report()`` could fail with an AttributeError on ``isatty`` if run + in an unusual environment where sys.stdout had been replaced. This is now + fixed. + + +.. _changes_7-2-4: + +Version 7.2.4 — 2023-04-28 +-------------------------- + +PyCon 2023 sprint fixes! + +- Fix: with ``relative_files = true``, specifying a specific file to include or + omit wouldn't work correctly (`issue 1604`_). This is now fixed, with + testing help by `Marc Gibbons `_. + +- Fix: the XML report would have an incorrect ```` element when using + relative files and the source option ended with a slash (`issue 1541`_). + This is now fixed, thanks to `Kevin Brown-Silva `_. + +- When the HTML report location is printed to the terminal, it's now a + terminal-compatible URL, so that you can click the location to open the HTML + file in your browser. Finishes `issue 1523`_ thanks to `Ricardo Newbery + `_. + +- Docs: a new :ref:`Migrating page ` with details about how to + migrate between major versions of coverage.py. It currently covers the + wildcard changes in 7.x. Thanks, `Brian Grohe `_. + +.. _issue 1523: https://github.com/nedbat/coveragepy/issues/1523 +.. _issue 1541: https://github.com/nedbat/coveragepy/issues/1541 +.. _issue 1604: https://github.com/nedbat/coveragepy/issues/1604 +.. _pull 1608: https://github.com/nedbat/coveragepy/pull/1608 +.. _pull 1609: https://github.com/nedbat/coveragepy/pull/1609 +.. _pull 1610: https://github.com/nedbat/coveragepy/pull/1610 +.. _pull 1613: https://github.com/nedbat/coveragepy/pull/1613 + + +.. _changes_7-2-3: + +Version 7.2.3 — 2023-04-06 +-------------------------- + +- Fix: the :ref:`config_run_sigterm` setting was meant to capture data if a + process was terminated with a SIGTERM signal, but it didn't always. This was + fixed thanks to `Lewis Gaul `_, closing `issue 1599`_. + +- Performance: HTML reports with context information are now much more compact. + File sizes are typically as small as one-third the previous size, but can be + dramatically smaller. This closes `issue 1584`_ thanks to `Oleh Krehel + `_. + +- Development dependencies no longer use hashed pins, closing `issue 1592`_. + +.. _issue 1584: https://github.com/nedbat/coveragepy/issues/1584 +.. _pull 1587: https://github.com/nedbat/coveragepy/pull/1587 +.. _issue 1592: https://github.com/nedbat/coveragepy/issues/1592 +.. _issue 1599: https://github.com/nedbat/coveragepy/issues/1599 +.. _pull 1600: https://github.com/nedbat/coveragepy/pull/1600 + + +.. _changes_7-2-2: + +Version 7.2.2 — 2023-03-16 +-------------------------- + +- Fix: if a virtualenv was created inside a source directory, and a sourced + package was installed inside the virtualenv, then all of the third-party + packages inside the virtualenv would be measured. This was incorrect, but + has now been fixed: only the specified packages will be measured, thanks to + `Manuel Jacob `_. + +- Fix: the ``coverage lcov`` command could create a .lcov file with incorrect + LF (lines found) and LH (lines hit) totals. This is now fixed, thanks to + `Ian Moore `_. + +- Fix: the ``coverage xml`` command on Windows could create a .xml file with + duplicate ```` elements. This is now fixed, thanks to `Benjamin + Parzella `_, closing `issue 1573`_. + +.. _pull 1560: https://github.com/nedbat/coveragepy/pull/1560 +.. _issue 1573: https://github.com/nedbat/coveragepy/issues/1573 +.. _pull 1574: https://github.com/nedbat/coveragepy/pull/1574 +.. _pull 1583: https://github.com/nedbat/coveragepy/pull/1583 + + +.. _changes_7-2-1: + +Version 7.2.1 — 2023-02-26 +-------------------------- + +- Fix: the PyPI page had broken links to documentation pages, but no longer + does, closing `issue 1566`_. + +- Fix: public members of the coverage module are now properly indicated so that + mypy will find them, fixing `issue 1564`_. + +.. _issue 1564: https://github.com/nedbat/coveragepy/issues/1564 +.. _issue 1566: https://github.com/nedbat/coveragepy/issues/1566 + + +.. _changes_7-2-0: + +Version 7.2.0 — 2023-02-22 +-------------------------- + +- Added a new setting ``[report] exclude_also`` to let you add more exclusions + without overwriting the defaults. Thanks, `Alpha Chen `_, + closing `issue 1391`_. + +- Added a :meth:`.CoverageData.purge_files` method to remove recorded data for + a particular file. Contributed by `Stephan Deibel `_. + +- Fix: when reporting commands fail, they will no longer congratulate + themselves with messages like "Wrote XML report to file.xml" before spewing a + traceback about their failure. + +- Fix: arguments in the public API that name file paths now accept pathlib.Path + objects. This includes the ``data_file`` and ``config_file`` arguments to + the Coverage constructor and the ``basename`` argument to CoverageData. + Closes `issue 1552`_. + +- Fix: In some embedded environments, an IndexError could occur on stop() when + the originating thread exits before completion. This is now fixed, thanks to + `Russell Keith-Magee `_, closing `issue 1542`_. + +- Added a ``py.typed`` file to announce our type-hintedness. Thanks, + `KotlinIsland `_. + +.. _issue 1391: https://github.com/nedbat/coveragepy/issues/1391 +.. _issue 1542: https://github.com/nedbat/coveragepy/issues/1542 +.. _pull 1543: https://github.com/nedbat/coveragepy/pull/1543 +.. _pull 1547: https://github.com/nedbat/coveragepy/pull/1547 +.. _pull 1550: https://github.com/nedbat/coveragepy/pull/1550 +.. _issue 1552: https://github.com/nedbat/coveragepy/issues/1552 +.. _pull 1557: https://github.com/nedbat/coveragepy/pull/1557 + + +.. _changes_7-1-0: + +Version 7.1.0 — 2023-01-24 +-------------------------- + +- Added: the debug output file can now be specified with ``[run] debug_file`` + in the configuration file. Closes `issue 1319`_. + +- Performance: fixed a slowdown with dynamic contexts that's been around since + 6.4.3. The fix closes `issue 1538`_. Thankfully this doesn't break the + `Cython change`_ that fixed `issue 972`_. Thanks to Mathieu Kniewallner for + the deep investigative work and comprehensive issue report. + +- Typing: all product and test code has type annotations. + +.. _Cython change: https://github.com/nedbat/coveragepy/pull/1347 +.. _issue 972: https://github.com/nedbat/coveragepy/issues/972 +.. _issue 1319: https://github.com/nedbat/coveragepy/issues/1319 +.. _issue 1538: https://github.com/nedbat/coveragepy/issues/1538 + + +.. _changes_7-0-5: + +Version 7.0.5 — 2023-01-10 +-------------------------- + +- Fix: On Python 3.7, a file with type annotations but no ``from __future__ + import annotations`` would be missing statements in the coverage report. This + is now fixed, closing `issue 1524`_. + +.. _issue 1524: https://github.com/nedbat/coveragepy/issues/1524 + + +.. _changes_7-0-4: + +Version 7.0.4 — 2023-01-07 +-------------------------- + +- Performance: an internal cache of file names was accidentally disabled, + resulting in sometimes drastic reductions in performance. This is now fixed, + closing `issue 1527`_. Thanks to Ivan Ciuvalschii for the reproducible test + case. + +.. _issue 1527: https://github.com/nedbat/coveragepy/issues/1527 + + +.. _changes_7-0-3: + +Version 7.0.3 — 2023-01-03 +-------------------------- + +- Fix: when using pytest-cov or pytest-xdist, or perhaps both, the combining + step could fail with ``assert row is not None`` using 7.0.2. This was due to + a race condition that has always been possible and is still possible. In + 7.0.1 and before, the error was silently swallowed by the combining code. + Now it will produce a message "Couldn't combine data file" and ignore the + data file as it used to do before 7.0.2. Closes `issue 1522`_. + +.. _issue 1522: https://github.com/nedbat/coveragepy/issues/1522 + + +.. _changes_7-0-2: + +Version 7.0.2 — 2023-01-02 +-------------------------- + +- Fix: when using the ``[run] relative_files = True`` setting, a relative + ``[paths]`` pattern was still being made absolute. This is now fixed, + closing `issue 1519`_. + +- Fix: if Python doesn't provide tomllib, then TOML configuration files can + only be read if coverage.py is installed with the ``[toml]`` extra. + Coverage.py will raise an error if TOML support is not installed when it sees + your settings are in a .toml file. But it didn't understand that + ``[tools.coverage]`` was a valid section header, so the error wasn't reported + if you used that header, and settings were silently ignored. This is now + fixed, closing `issue 1516`_. + +- Fix: adjusted how decorators are traced on PyPy 7.3.10, fixing `issue 1515`_. + +- Fix: the ``coverage lcov`` report did not properly implement the + ``--fail-under=MIN`` option. This has been fixed. + +- Refactor: added many type annotations, including a number of refactorings. + This should not affect outward behavior, but they were a bit invasive in some + places, so keep your eyes peeled for oddities. + +- Refactor: removed the vestigial and long untested support for Jython and + IronPython. + +.. _issue 1515: https://github.com/nedbat/coveragepy/issues/1515 +.. _issue 1516: https://github.com/nedbat/coveragepy/issues/1516 +.. _issue 1519: https://github.com/nedbat/coveragepy/issues/1519 + + +.. _changes_7-0-1: + +Version 7.0.1 — 2022-12-23 +-------------------------- + +- When checking if a file mapping resolved to a file that exists, we weren't + considering files in .whl files. This is now fixed, closing `issue 1511`_. + +- File pattern rules were too strict, forbidding plus signs and curly braces in + directory and file names. This is now fixed, closing `issue 1513`_. + +- Unusual Unicode or control characters in source files could prevent + reporting. This is now fixed, closing `issue 1512`_. + +- The PyPy wheel now installs on PyPy 3.7, 3.8, and 3.9, closing `issue 1510`_. + +.. _issue 1510: https://github.com/nedbat/coveragepy/issues/1510 +.. _issue 1511: https://github.com/nedbat/coveragepy/issues/1511 +.. _issue 1512: https://github.com/nedbat/coveragepy/issues/1512 +.. _issue 1513: https://github.com/nedbat/coveragepy/issues/1513 + + +.. _changes_7-0-0: + +Version 7.0.0 — 2022-12-18 +-------------------------- + +Nothing new beyond 7.0.0b1. + + +.. _changes_7-0-0b1: + +Version 7.0.0b1 — 2022-12-03 +---------------------------- + +A number of changes have been made to file path handling, including pattern +matching and path remapping with the ``[paths]`` setting (see +:ref:`config_paths`). These changes might affect you, and require you to +update your settings. + +(This release includes the changes from `6.6.0b1 `_, since +6.6.0 was never released.) + +- Changes to file pattern matching, which might require updating your + configuration: + + - Previously, ``*`` would incorrectly match directory separators, making + precise matching difficult. This is now fixed, closing `issue 1407`_. + + - Now ``**`` matches any number of nested directories, including none. + +- Improvements to combining data files when using the + :ref:`config_run_relative_files` setting, which might require updating your + configuration: + + - During ``coverage combine``, relative file paths are implicitly combined + without needing a ``[paths]`` configuration setting. This also fixed + `issue 991`_. + + - A ``[paths]`` setting like ``*/foo`` will now match ``foo/bar.py`` so that + relative file paths can be combined more easily. + + - The :ref:`config_run_relative_files` setting is properly interpreted in + more places, fixing `issue 1280`_. + +- When remapping file paths with ``[paths]``, a path will be remapped only if + the resulting path exists. The documentation has long said the prefix had to + exist, but it was never enforced. This fixes `issue 608`_, improves `issue + 649`_, and closes `issue 757`_. + +- Reporting operations now implicitly use the ``[paths]`` setting to remap file + paths within a single data file. Combining multiple files still requires the + ``coverage combine`` step, but this simplifies some single-file situations. + Closes `issue 1212`_ and `issue 713`_. + +- The ``coverage report`` command now has a ``--format=`` option. The original + style is now ``--format=text``, and is the default. + + - Using ``--format=markdown`` will write the table in Markdown format, thanks + to `Steve Oswald `_, closing `issue 1418`_. + + - Using ``--format=total`` will write a single total number to the + output. This can be useful for making badges or writing status updates. + +- Combining data files with ``coverage combine`` now hashes the data files to + skip files that add no new information. This can reduce the time needed. + Many details affect the speed-up, but for coverage.py's own test suite, + combining is about 40% faster. Closes `issue 1483`_. + +- When searching for completely un-executed files, coverage.py uses the + presence of ``__init__.py`` files to determine which directories have source + that could have been imported. However, `implicit namespace packages`_ don't + require ``__init__.py``. A new setting ``[report] + include_namespace_packages`` tells coverage.py to consider these directories + during reporting. Thanks to `Felix Horvat `_ for the + contribution. Closes `issue 1383`_ and `issue 1024`_. + +- Fixed environment variable expansion in pyproject.toml files. It was overly + broad, causing errors outside of coverage.py settings, as described in `issue + 1481`_ and `issue 1345`_. This is now fixed, but in rare cases will require + changing your pyproject.toml to quote non-string values that use environment + substitution. + +- An empty file has a coverage total of 100%, but used to fail with + ``--fail-under``. This has been fixed, closing `issue 1470`_. + +- The text report table no longer writes out two separator lines if there are + no files listed in the table. One is plenty. + +- Fixed a mis-measurement of a strange use of wildcard alternatives in + match/case statements, closing `issue 1421`_. + +- Fixed internal logic that prevented coverage.py from running on + implementations other than CPython or PyPy (`issue 1474`_). + +- The deprecated ``[run] note`` setting has been completely removed. + +.. _implicit namespace packages: https://peps.python.org/pep-0420/ +.. _issue 608: https://github.com/nedbat/coveragepy/issues/608 +.. _issue 649: https://github.com/nedbat/coveragepy/issues/649 +.. _issue 713: https://github.com/nedbat/coveragepy/issues/713 +.. _issue 757: https://github.com/nedbat/coveragepy/issues/757 +.. _issue 991: https://github.com/nedbat/coveragepy/issues/991 +.. _issue 1024: https://github.com/nedbat/coveragepy/issues/1024 +.. _issue 1212: https://github.com/nedbat/coveragepy/issues/1212 +.. _issue 1280: https://github.com/nedbat/coveragepy/issues/1280 +.. _issue 1345: https://github.com/nedbat/coveragepy/issues/1345 +.. _issue 1383: https://github.com/nedbat/coveragepy/issues/1383 +.. _issue 1407: https://github.com/nedbat/coveragepy/issues/1407 +.. _issue 1418: https://github.com/nedbat/coveragepy/issues/1418 +.. _issue 1421: https://github.com/nedbat/coveragepy/issues/1421 +.. _issue 1470: https://github.com/nedbat/coveragepy/issues/1470 +.. _issue 1474: https://github.com/nedbat/coveragepy/issues/1474 +.. _issue 1481: https://github.com/nedbat/coveragepy/issues/1481 +.. _issue 1483: https://github.com/nedbat/coveragepy/issues/1483 +.. _pull 1387: https://github.com/nedbat/coveragepy/pull/1387 +.. _pull 1479: https://github.com/nedbat/coveragepy/pull/1479 + + +.. _changes_6-6-0b1: + +Version 6.6.0b1 — 2022-10-31 +---------------------------- + +(Note: 6.6.0 final was never released. These changes are part of `7.0.0b1 +`_.) + +- Changes to file pattern matching, which might require updating your + configuration: + + - Previously, ``*`` would incorrectly match directory separators, making + precise matching difficult. This is now fixed, closing `issue 1407`_. + + - Now ``**`` matches any number of nested directories, including none. + +- Improvements to combining data files when using the + :ref:`config_run_relative_files` setting: + + - During ``coverage combine``, relative file paths are implicitly combined + without needing a ``[paths]`` configuration setting. This also fixed + `issue 991`_. + + - A ``[paths]`` setting like ``*/foo`` will now match ``foo/bar.py`` so that + relative file paths can be combined more easily. + + - The setting is properly interpreted in more places, fixing `issue 1280`_. + +- Fixed environment variable expansion in pyproject.toml files. It was overly + broad, causing errors outside of coverage.py settings, as described in `issue + 1481`_ and `issue 1345`_. This is now fixed, but in rare cases will require + changing your pyproject.toml to quote non-string values that use environment + substitution. + +- Fixed internal logic that prevented coverage.py from running on + implementations other than CPython or PyPy (`issue 1474`_). + +.. _issue 991: https://github.com/nedbat/coveragepy/issues/991 +.. _issue 1280: https://github.com/nedbat/coveragepy/issues/1280 +.. _issue 1345: https://github.com/nedbat/coveragepy/issues/1345 +.. _issue 1407: https://github.com/nedbat/coveragepy/issues/1407 +.. _issue 1474: https://github.com/nedbat/coveragepy/issues/1474 +.. _issue 1481: https://github.com/nedbat/coveragepy/issues/1481 + + .. _changes_6-5-0: Version 6.5.0 — 2022-09-29 -------------------------- - The JSON report now includes details of which branches were taken, and which - are missing for each file. Thanks, Christoph Blessing (`pull 1438`_). Closes + are missing for each file. Thanks, `Christoph Blessing `_. Closes `issue 1425`_. - Starting with coverage.py 6.2, ``class`` statements were marked as a branch. @@ -43,8 +505,8 @@ .. _PEP 517: https://peps.python.org/pep-0517/ .. _issue 1395: https://github.com/nedbat/coveragepy/issues/1395 .. _issue 1425: https://github.com/nedbat/coveragepy/issues/1425 -.. _pull 1438: https://github.com/nedbat/coveragepy/pull/1438 .. _issue 1449: https://github.com/nedbat/coveragepy/issues/1449 +.. _pull 1438: https://github.com/nedbat/coveragepy/pull/1438 .. _changes_6-4-4: @@ -60,29 +522,28 @@ Version 6.4.3 — 2022-08-06 -------------------------- -- Fix a failure when combining data files if the file names contained - glob-like patterns (`pull 1405`_). Thanks, Michael Krebs and Benjamin - Schubert. +- Fix a failure when combining data files if the file names contained glob-like + patterns. Thanks, `Michael Krebs and Benjamin Schubert `_. - Fix a messaging failure when combining Windows data files on a different - drive than the current directory. (`pull 1430`_, fixing `issue 1428`_). - Thanks, Lorenzo Micò. + drive than the current directory, closing `issue 1428`_. Thanks, `Lorenzo + Micò `_. - Fix path calculations when running in the root directory, as you might do in - a Docker container: `pull 1403`_, thanks Arthur Rio. + a Docker container. Thanks `Arthur Rio `_. - Filtering in the HTML report wouldn't work when reloading the index page. - This is now fixed (`pull 1413`_). Thanks, Marc Legendre. + This is now fixed. Thanks, `Marc Legendre `_. -- Fix a problem with Cython code measurement (`pull 1347`_, fixing `issue - 972`_). Thanks, Matus Valo. +- Fix a problem with Cython code measurement, closing `issue 972`_. Thanks, + `Matus Valo `_. .. _issue 972: https://github.com/nedbat/coveragepy/issues/972 +.. _issue 1428: https://github.com/nedbat/coveragepy/issues/1428 .. _pull 1347: https://github.com/nedbat/coveragepy/pull/1347 .. _pull 1403: https://github.com/nedbat/coveragepy/issues/1403 .. _pull 1405: https://github.com/nedbat/coveragepy/issues/1405 .. _pull 1413: https://github.com/nedbat/coveragepy/issues/1413 -.. _issue 1428: https://github.com/nedbat/coveragepy/issues/1428 .. _pull 1430: https://github.com/nedbat/coveragepy/pull/1430 @@ -92,17 +553,17 @@ -------------------------- - Updated for a small change in Python 3.11.0 beta 4: modules now start with a - line with line number 0, which is ignored. This line cannnot be executed, so + line with line number 0, which is ignored. This line cannot be executed, so coverage totals were thrown off. This line is now ignored by coverage.py, but this also means that truly empty modules (like ``__init__.py``) have no lines in them, rather than one phantom line. Fixes `issue 1419`_. - Internal debugging data added to sys.modules is now an actual module, to avoid confusing code that examines everything in sys.modules. Thanks, - Yilei Yang (`pull 1399`_). + `Yilei Yang `_. -.. _pull 1399: https://github.com/nedbat/coveragepy/pull/1399 .. _issue 1419: https://github.com/nedbat/coveragepy/issues/1419 +.. _pull 1399: https://github.com/nedbat/coveragepy/pull/1399 .. _changes_6-4-1: @@ -143,7 +604,7 @@ ``?`` to open/close the help panel. Thanks, `J. M. F. Tsang `_. - - The timestamp and version are displayed at the top of the report. Thanks, + - The time stamp and version are displayed at the top of the report. Thanks, `Ammar Askar `_. Closes `issue 1351`_. - A new debug option ``debug=sqldata`` adds more detail to ``debug=sql``, @@ -432,7 +893,7 @@ - Packages named as "source packages" (with ``source``, or ``source_pkgs``, or pytest-cov's ``--cov``) might have been only partially measured. Their - top-level statements could be marked as unexecuted, because they were + top-level statements could be marked as un-executed, because they were imported by coverage.py before measurement began (`issue 1232`_). This is now fixed, but the package will be imported twice, once by coverage.py, then again by your test suite. This could cause problems if importing the package @@ -682,6 +1143,7 @@ .. _issue 1010: https://github.com/nedbat/coveragepy/issues/1010 .. _pull request 1066: https://github.com/nedbat/coveragepy/pull/1066 + .. _changes_53: Version 5.3 — 2020-09-13 @@ -701,7 +1163,7 @@ .. _issue 1011: https://github.com/nedbat/coveragepy/issues/1011 -.. endchangesinclude +.. scriv-end-here Older changes ------------- diff -Nru python-coverage-6.5.0+dfsg1/ci/download_gha_artifacts.py python-coverage-7.2.7+dfsg1/ci/download_gha_artifacts.py --- python-coverage-6.5.0+dfsg1/ci/download_gha_artifacts.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/ci/download_gha_artifacts.py 2023-05-29 19:46:30.000000000 +0000 @@ -21,7 +21,7 @@ for chunk in response.iter_content(16*1024): f.write(chunk) else: - raise Exception(f"Fetching {url} produced: status={response.status_code}") + raise RuntimeError(f"Fetching {url} produced: status={response.status_code}") def unpack_zipfile(filename): """Unpack a zipfile, using the names in the zip.""" diff -Nru python-coverage-6.5.0+dfsg1/ci/ghrel_template.md.j2 python-coverage-7.2.7+dfsg1/ci/ghrel_template.md.j2 --- python-coverage-6.5.0+dfsg1/ci/ghrel_template.md.j2 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/ci/ghrel_template.md.j2 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,5 @@ + +{{body}} + +:arrow_right:  PyPI page: [coverage {{version}}](https://pypi.org/project/coverage/{{version}}). +:arrow_right:  To install: `python3 -m pip install coverage=={{version}}` diff -Nru python-coverage-6.5.0+dfsg1/ci/github_releases.py python-coverage-7.2.7+dfsg1/ci/github_releases.py --- python-coverage-6.5.0+dfsg1/ci/github_releases.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/ci/github_releases.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,138 +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 - -"""Upload release notes into GitHub releases.""" - -import json -import shlex -import subprocess -import sys - -import pkg_resources -import requests - - -RELEASES_URL = "https://api.github.com/repos/{repo}/releases" - -def run_command(cmd): - """ - Run a command line (with no shell). - - Returns a tuple: - bool: true if the command succeeded. - str: the output of the command. - - """ - proc = subprocess.run( - shlex.split(cmd), - shell=False, - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - output = proc.stdout.decode("utf-8") - succeeded = proc.returncode == 0 - return succeeded, output - -def does_tag_exist(tag_name): - """ - Does `tag_name` exist as a tag in git? - """ - return run_command(f"git rev-parse --verify {tag_name}")[0] - -def check_ok(resp): - """ - Check that the Requests response object was successful. - - Raise an exception if not. - """ - if not resp: - print(f"text: {resp.text!r}") - resp.raise_for_status() - -def github_paginated(session, url): - """ - Get all the results from a paginated GitHub url. - """ - while True: - resp = session.get(url) - check_ok(resp) - yield from resp.json() - next_link = resp.links.get("next", None) - if not next_link: - break - url = next_link["url"] - -def get_releases(session, repo): - """ - Get all the releases from a name/project repo. - - Returns: - A dict mapping tag names to release dictionaries. - """ - url = RELEASES_URL.format(repo=repo) - releases = { r['tag_name']: r for r in github_paginated(session, url) } - return releases - -def release_for_relnote(relnote): - """ - Turn a release note dict into the data needed by GitHub for a release. - """ - tag = relnote['version'] - return { - "tag_name": tag, - "name": tag, - "body": relnote["text"], - "draft": False, - "prerelease": relnote["prerelease"], - } - -def create_release(session, repo, relnote): - """ - Create a new GitHub release. - """ - print(f"Creating {relnote['version']}") - data = release_for_relnote(relnote) - resp = session.post(RELEASES_URL.format(repo=repo), json=data) - check_ok(resp) - -def update_release(session, url, relnote): - """ - Update an existing GitHub release. - """ - print(f"Updating {relnote['version']}") - data = release_for_relnote(relnote) - resp = session.patch(url, json=data) - check_ok(resp) - -def update_github_releases(json_filename, repo): - """ - Read the json file, and create or update releases in GitHub. - """ - gh_session = requests.Session() - releases = get_releases(gh_session, repo) - if 0: # if you need to delete all the releases! - for release in releases.values(): - print(release["tag_name"]) - resp = gh_session.delete(release["url"]) - check_ok(resp) - return - - with open(json_filename) as jf: - relnotes = json.load(jf) - relnotes.sort(key=lambda rel: pkg_resources.parse_version(rel["version"])) - for relnote in relnotes: - tag = relnote["version"] - if not does_tag_exist(tag): - continue - exists = tag in releases - if not exists: - create_release(gh_session, repo, relnote) - else: - release = releases[tag] - if release["body"] != relnote["text"]: - url = release["url"] - update_release(gh_session, url, relnote) - -if __name__ == "__main__": - update_github_releases(*sys.argv[1:3]) diff -Nru python-coverage-6.5.0+dfsg1/ci/parse_relnotes.py python-coverage-7.2.7+dfsg1/ci/parse_relnotes.py --- python-coverage-6.5.0+dfsg1/ci/parse_relnotes.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/ci/parse_relnotes.py 2023-05-29 19:46:30.000000000 +0000 @@ -74,7 +74,7 @@ elif ttype == "text": text.append(ttext) else: - raise Exception(f"Don't know ttype {ttype!r}") + raise RuntimeError(f"Don't know ttype {ttype!r}") yield (*header, "\n".join(text)) diff -Nru python-coverage-6.5.0+dfsg1/CONTRIBUTORS.txt python-coverage-7.2.7+dfsg1/CONTRIBUTORS.txt --- python-coverage-6.5.0+dfsg1/CONTRIBUTORS.txt 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/CONTRIBUTORS.txt 2023-05-29 19:46:30.000000000 +0000 @@ -14,7 +14,9 @@ Alex Sandro Alexander Todorov Alexander Walters +Alpha Chen Ammar Askar +Anders Kaseorg Andrew Hoos Anthony Sottile Arcadiy Ivanov @@ -22,28 +24,36 @@ Artem Dayneko Arthur Deygin Arthur Rio +Asher Foa Ben Carlsson Ben Finney +Benjamin Parzella Benjamin Schubert Bernát Gábor Bill Hart Bradley Burns Brandon Rhodes Brett Cannon +Brian Grohe +Bruno Oliveira Bruno P. Kinoshita Buck Evan +Buck Golemon Calen Pennington Carl Friedrich Bolz-Tereick Carl Gieringer Catherine Proulx +Charles Chan Chris Adams Chris Jerdonek Chris Rose Chris Warrick +Christian Clauss Christian Heimes Christine Lytwynec Christoph Blessing Christoph Zwerschke +Christopher Pickering Clément Pit-Claudel Conrad Ho Cosimo Lupo @@ -57,6 +67,7 @@ David MacIver David Stanek David Szotten +Dennis Sweeney Detlev Offenbach Devin Jeanpierre Dirk Thomas @@ -68,6 +79,7 @@ Emil Madsen Éric Larivière Federico Bond +Felix Horvat Frazer McLean Geoff Bache George Paci @@ -76,16 +88,22 @@ Greg Rogers Guido van Rossum Guillaume Chazarain +Holger Krekel Hugo van Kemenade +Ian Moore Ilia Meerovich Imri Goldberg Ionel Cristian Mărieș +Ivan Ciuvalschii J. M. F. Tsang JT Olds +Jakub Wilk +Janakarajan Natarajan Jerin Peter George Jessamyn Smith Joe Doherty Joe Jevnik +John Vandenberg Jon Chappell Jon Dufresne Joseph Tate @@ -94,71 +112,105 @@ Julian Berman Julien Voisin Justas Sadzevičius +Karthikeyan Singaravelan +Kassandra Keeton +Kevin Brown-Silva Kjell Braden Krystian Kichewko Kyle Altendorf Lars Hupfeldt Nielsen +Latrice Wilgus Leonardo Pistone +Lewis Gaul Lex Berezhny Loïc Dachary Lorenzo Micò +Louis Heredero +Luis Nell +Łukasz Stolcman +Manuel Jacob Marc Abramowitz +Marc Gibbons Marc Legendre Marcelo Trylesinski Marcus Cobden +Mariatta Marius Gedminas Mark van der Wal Martin Fuzzey +Mathieu Kniewallner Matt Bachmann Matthew Boehm Matthew Desmarais Matus Valo Max Linke +Mayank Singhal Michael Krebs Michał Bultrowicz +Michał Górny Mickie Betz Mike Fiedler -Naveen Yadav +Min ho Kim Nathan Land +Naveen Srinivasan +Naveen Yadav +Neil Pilgrim +Nicholas Nadeau Nikita Bloshchanevich +Nikita Sobolev Nils Kattenbeck Noel O'Boyle +Oleg Höfling +Oleh Krehel Olivier Grisel Ori Avtalion -Pankaj Pandey Pablo Carballo +Pankaj Pandey Patrick Mezard +Pavel Tsialnou Peter Baughman Peter Ebden Peter Portante +Phebe Polk Reya B +Ricardo Newbery Rodrigue Cloutier Roger Hu +Roland Illig Ross Lawley Roy Williams +Russell Keith-Magee +S. Y. Lee Salvatore Zagaria Sandra Martocchia Scott Belden Sebastián Ramírez Sergey B Kirpichev +Shantanu Sigve Tjora Simon Willison Stan Hu +Stanisław Pitucha Stefan Behnel +Stephan Deibel Stephan Richter Stephen Finucane Steve Dower Steve Leonard +Steve Oswald Steve Peak -S. Y. Lee +Sviatoslav Sydorenko Teake Nutma Ted Wexler Thijs Triemstra Thomas Grainger +Timo Furrer Titus Brown +Tom Gurion Valentin Lab -Vince Salvino Ville Skyttä +Vince Salvino +Wonwin McBrootles Xie Yanbo Yilei "Dolee" Yang Yury Selivanov diff -Nru python-coverage-6.5.0+dfsg1/coverage/annotate.py python-coverage-7.2.7+dfsg1/coverage/annotate.py --- python-coverage-6.5.0+dfsg1/coverage/annotate.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/annotate.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,12 +3,22 @@ """Source file annotation for coverage.py.""" +from __future__ import annotations + import os import re +from typing import Iterable, Optional, TYPE_CHECKING + from coverage.files import flat_rootname from coverage.misc import ensure_dir, isolate_module -from coverage.report import get_analysis_to_report +from coverage.plugin import FileReporter +from coverage.report_core import get_analysis_to_report +from coverage.results import Analysis +from coverage.types import TMorf + +if TYPE_CHECKING: + from coverage import Coverage os = isolate_module(os) @@ -30,20 +40,20 @@ > h(2) - Executed lines use '>', lines not executed use '!', lines excluded from - consideration use '-'. + Executed lines use ">", lines not executed use "!", lines excluded from + consideration use "-". """ - def __init__(self, coverage): + def __init__(self, coverage: Coverage) -> None: self.coverage = coverage self.config = self.coverage.config - self.directory = None + self.directory: Optional[str] = None blank_re = re.compile(r"\s*(#|$)") else_re = re.compile(r"\s*else\s*:\s*(#|$)") - def report(self, morfs, directory=None): + def report(self, morfs: Optional[Iterable[TMorf]], directory: Optional[str] = None) -> None: """Run the report. See `coverage.report()` for arguments. @@ -54,7 +64,7 @@ for fr, analysis in get_analysis_to_report(self.coverage, morfs): self.annotate_file(fr, analysis) - def annotate_file(self, fr, analysis): + def annotate_file(self, fr: FileReporter, analysis: Analysis) -> None: """Annotate a single file. `fr` is the FileReporter for the file to annotate. @@ -73,7 +83,7 @@ else: dest_file = fr.filename + ",cover" - with open(dest_file, 'w', encoding='utf-8') as dest: + with open(dest_file, "w", encoding="utf-8") as dest: i = j = 0 covered = True source = fr.source() @@ -85,20 +95,20 @@ if i < len(statements) and statements[i] == lineno: covered = j >= len(missing) or missing[j] > lineno if self.blank_re.match(line): - dest.write(' ') + dest.write(" ") elif self.else_re.match(line): - # Special logic for lines containing only 'else:'. + # Special logic for lines containing only "else:". if j >= len(missing): - dest.write('> ') + dest.write("> ") elif statements[i] == missing[j]: - dest.write('! ') + dest.write("! ") else: - dest.write('> ') + dest.write("> ") elif lineno in excluded: - dest.write('- ') + dest.write("- ") elif covered: - dest.write('> ') + dest.write("> ") else: - dest.write('! ') + dest.write("! ") dest.write(line) diff -Nru python-coverage-6.5.0+dfsg1/coverage/bytecode.py python-coverage-7.2.7+dfsg1/coverage/bytecode.py --- python-coverage-6.5.0+dfsg1/coverage/bytecode.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/bytecode.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,10 +3,13 @@ """Bytecode manipulation for coverage.py""" -import types +from __future__ import annotations +from types import CodeType +from typing import Iterator -def code_objects(code): + +def code_objects(code: CodeType) -> Iterator[CodeType]: """Iterate over all the code objects in `code`.""" stack = [code] while stack: @@ -14,6 +17,6 @@ # push its children for later returning. code = stack.pop() for c in code.co_consts: - if isinstance(c, types.CodeType): + if isinstance(c, CodeType): stack.append(c) yield code diff -Nru python-coverage-6.5.0+dfsg1/coverage/cmdline.py python-coverage-7.2.7+dfsg1/coverage/cmdline.py --- python-coverage-6.5.0+dfsg1/coverage/cmdline.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/cmdline.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Command-line support for coverage.py.""" +from __future__ import annotations + import glob import optparse # pylint: disable=deprecated-module import os @@ -12,10 +14,12 @@ import textwrap import traceback +from typing import cast, Any, List, NoReturn, Optional, Tuple + import coverage from coverage import Coverage from coverage import env -from coverage.collector import CTracer +from coverage.collector import HAS_CTRACER from coverage.config import CoverageConfig from coverage.control import DEFAULT_DATAFILE from coverage.data import combinable_files, debug_data_file @@ -23,6 +27,7 @@ from coverage.exceptions import _BaseCoverageException, _ExceptionDuringRun, NoSource from coverage.execfile import PyRunner from coverage.results import Numbers, should_fail_under +from coverage.version import __url__ # When adding to this file, alphabetization is important. Look for # "alphabetize" comments throughout. @@ -34,126 +39,130 @@ # appears on the command line. append = optparse.make_option( - '-a', '--append', action='store_true', + "-a", "--append", action="store_true", help="Append coverage data to .coverage, otherwise it starts clean each time.", ) keep = optparse.make_option( - '', '--keep', action='store_true', + "", "--keep", action="store_true", help="Keep original coverage files, otherwise they are deleted.", ) branch = optparse.make_option( - '', '--branch', action='store_true', + "", "--branch", action="store_true", help="Measure branch coverage in addition to statement coverage.", ) concurrency = optparse.make_option( - '', '--concurrency', action='store', metavar="LIBS", + "", "--concurrency", action="store", metavar="LIBS", help=( "Properly measure code using a concurrency library. " + "Valid values are: {}, or a comma-list of them." ).format(", ".join(sorted(CoverageConfig.CONCURRENCY_CHOICES))), ) context = optparse.make_option( - '', '--context', action='store', metavar="LABEL", + "", "--context", action="store", metavar="LABEL", help="The context label to record for this coverage run.", ) contexts = optparse.make_option( - '', '--contexts', action='store', metavar="REGEX1,REGEX2,...", + "", "--contexts", action="store", metavar="REGEX1,REGEX2,...", help=( "Only display data from lines covered in the given contexts. " + "Accepts Python regexes, which must be quoted." ), ) combine_datafile = optparse.make_option( - '', '--data-file', action='store', metavar="DATAFILE", + "", "--data-file", action="store", metavar="DATAFILE", help=( "Base name of the data files to operate on. " + "Defaults to '.coverage'. [env: COVERAGE_FILE]" ), ) input_datafile = optparse.make_option( - '', '--data-file', action='store', metavar="INFILE", + "", "--data-file", action="store", metavar="INFILE", help=( "Read coverage data for report generation from this file. " + "Defaults to '.coverage'. [env: COVERAGE_FILE]" ), ) output_datafile = optparse.make_option( - '', '--data-file', action='store', metavar="OUTFILE", + "", "--data-file", action="store", metavar="OUTFILE", help=( "Write the recorded coverage data to this file. " + "Defaults to '.coverage'. [env: COVERAGE_FILE]" ), ) debug = optparse.make_option( - '', '--debug', action='store', metavar="OPTS", + "", "--debug", action="store", metavar="OPTS", help="Debug options, separated by commas. [env: COVERAGE_DEBUG]", ) directory = optparse.make_option( - '-d', '--directory', action='store', metavar="DIR", + "-d", "--directory", action="store", metavar="DIR", help="Write the output files to DIR.", ) fail_under = optparse.make_option( - '', '--fail-under', action='store', metavar="MIN", type="float", + "", "--fail-under", action="store", metavar="MIN", type="float", help="Exit with a status of 2 if the total coverage is less than MIN.", ) + format = optparse.make_option( + "", "--format", action="store", metavar="FORMAT", + help="Output format, either text (default), markdown, or total.", + ) help = optparse.make_option( - '-h', '--help', action='store_true', + "-h", "--help", action="store_true", help="Get help on this command.", ) ignore_errors = optparse.make_option( - '-i', '--ignore-errors', action='store_true', + "-i", "--ignore-errors", action="store_true", help="Ignore errors while reading source files.", ) include = optparse.make_option( - '', '--include', action='store', metavar="PAT1,PAT2,...", + "", "--include", action="store", metavar="PAT1,PAT2,...", help=( "Include only files whose paths match one of these patterns. " + "Accepts shell-style wildcards, which must be quoted." ), ) pylib = optparse.make_option( - '-L', '--pylib', action='store_true', + "-L", "--pylib", action="store_true", help=( "Measure coverage even inside the Python installed library, " + "which isn't done by default." ), ) show_missing = optparse.make_option( - '-m', '--show-missing', action='store_true', + "-m", "--show-missing", action="store_true", help="Show line numbers of statements in each module that weren't executed.", ) module = optparse.make_option( - '-m', '--module', action='store_true', + "-m", "--module", action="store_true", help=( " is an importable Python module, not a script path, " + "to be run as 'python -m' would run it." ), ) omit = optparse.make_option( - '', '--omit', action='store', metavar="PAT1,PAT2,...", + "", "--omit", action="store", metavar="PAT1,PAT2,...", help=( "Omit files whose paths match one of these patterns. " + "Accepts shell-style wildcards, which must be quoted." ), ) output_xml = optparse.make_option( - '-o', '', action='store', dest="outfile", metavar="OUTFILE", + "-o", "", action="store", dest="outfile", metavar="OUTFILE", help="Write the XML report to this file. Defaults to 'coverage.xml'", ) output_json = optparse.make_option( - '-o', '', action='store', dest="outfile", metavar="OUTFILE", + "-o", "", action="store", dest="outfile", metavar="OUTFILE", help="Write the JSON report to this file. Defaults to 'coverage.json'", ) output_lcov = optparse.make_option( - '-o', '', action='store', dest='outfile', metavar="OUTFILE", + "-o", "", action="store", dest="outfile", metavar="OUTFILE", help="Write the LCOV report to this file. Defaults to 'coverage.lcov'", ) json_pretty_print = optparse.make_option( - '', '--pretty-print', action='store_true', + "", "--pretty-print", action="store_true", help="Format the JSON for human readers.", ) parallel_mode = optparse.make_option( - '-p', '--parallel-mode', action='store_true', + "-p", "--parallel-mode", action="store_true", help=( "Append the machine name, process id and random number to the " + "data file name to simplify collecting data from " + @@ -161,18 +170,18 @@ ), ) precision = optparse.make_option( - '', '--precision', action='store', metavar='N', type=int, + "", "--precision", action="store", metavar="N", type=int, help=( "Number of digits after the decimal point to display for " + "reported coverage percentages." ), ) quiet = optparse.make_option( - '-q', '--quiet', action='store_true', + "-q", "--quiet", action="store_true", help="Don't print messages about what is happening.", ) rcfile = optparse.make_option( - '', '--rcfile', action='store', + "", "--rcfile", action="store", help=( "Specify configuration file. " + "By default '.coveragerc', 'setup.cfg', 'tox.ini', and " + @@ -180,45 +189,45 @@ ), ) show_contexts = optparse.make_option( - '--show-contexts', action='store_true', + "--show-contexts", action="store_true", help="Show contexts for covered lines.", ) skip_covered = optparse.make_option( - '--skip-covered', action='store_true', + "--skip-covered", action="store_true", help="Skip files with 100% coverage.", ) no_skip_covered = optparse.make_option( - '--no-skip-covered', action='store_false', dest='skip_covered', + "--no-skip-covered", action="store_false", dest="skip_covered", help="Disable --skip-covered.", ) skip_empty = optparse.make_option( - '--skip-empty', action='store_true', + "--skip-empty", action="store_true", help="Skip files with no code.", ) sort = optparse.make_option( - '--sort', action='store', metavar='COLUMN', + "--sort", action="store", metavar="COLUMN", help=( "Sort the report by the named column: name, stmts, miss, branch, brpart, or cover. " + "Default is name." ), ) source = optparse.make_option( - '', '--source', action='store', metavar="SRC1,SRC2,...", + "", "--source", action="store", metavar="SRC1,SRC2,...", help="A list of directories or importable names of code to measure.", ) timid = optparse.make_option( - '', '--timid', action='store_true', + "", "--timid", action="store_true", help=( "Use a simpler but slower trace method. Try this if you get " + "seemingly impossible results!" ), ) title = optparse.make_option( - '', '--title', action='store', metavar="TITLE", + "", "--title", action="store", metavar="TITLE", help="A text string to use as the title on the HTML.", ) version = optparse.make_option( - '', '--version', action='store_true', + "", "--version", action="store_true", help="Display version information and exit.", ) @@ -231,8 +240,9 @@ """ - def __init__(self, *args, **kwargs): - super().__init__(add_help_option=False, *args, **kwargs) + def __init__(self, *args: Any, **kwargs: Any) -> None: + kwargs["add_help_option"] = False + super().__init__(*args, **kwargs) self.set_defaults( # Keep these arguments alphabetized by their names. action=None, @@ -245,6 +255,7 @@ debug=None, directory=None, fail_under=None, + format=None, help=None, ignore_errors=None, include=None, @@ -273,19 +284,19 @@ """Used to stop the optparse error handler ending the process.""" pass - def parse_args_ok(self, args=None, options=None): + def parse_args_ok(self, args: List[str]) -> Tuple[bool, Optional[optparse.Values], List[str]]: """Call optparse.parse_args, but return a triple: (ok, options, args) """ try: - options, args = super().parse_args(args, options) + options, args = super().parse_args(args) except self.OptionParserError: - return False, None, None + return False, None, [] return True, options, args - def error(self, msg): + def error(self, msg: str) -> NoReturn: """Override optparse.error so sys.exit doesn't get called.""" show_help(msg) raise self.OptionParserError @@ -294,7 +305,7 @@ class GlobalOptionParser(CoverageOptionParser): """Command-line parser for coverage.py global option arguments.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self.add_options([ @@ -306,14 +317,19 @@ class CmdOptionParser(CoverageOptionParser): """Parse one of the new-style commands for coverage.py.""" - def __init__(self, action, options, defaults=None, usage=None, description=None): + def __init__( + self, + action: str, + options: List[optparse.Option], + description: str, + usage: Optional[str] = None, + ): """Create an OptionParser for a coverage.py command. `action` is the slug to put into `options.action`. `options` is a list of Option's for the command. - `defaults` is a dict of default value for options. - `usage` is the usage string to display in help. `description` is the description of the command, for the help text. + `usage` is the usage string to display in help. """ if usage: @@ -322,18 +338,18 @@ usage=usage, description=description, ) - self.set_defaults(action=action, **(defaults or {})) + self.set_defaults(action=action) self.add_options(options) self.cmd = action - def __eq__(self, other): + def __eq__(self, other: str) -> bool: # type: ignore[override] # A convenience equality, so that I can put strings in unit test # results, and they will compare equal to objects. return (other == f"") - __hash__ = None # This object doesn't need to be hashed. + __hash__ = None # type: ignore[assignment] - def get_prog_name(self): + def get_prog_name(self) -> str: """Override of an undocumented function in optparse.OptionParser.""" program_name = super().get_prog_name() @@ -353,7 +369,7 @@ ] COMMANDS = { - 'annotate': CmdOptionParser( + "annotate": CmdOptionParser( "annotate", [ Opts.directory, @@ -369,7 +385,7 @@ ), ), - 'combine': CmdOptionParser( + "combine": CmdOptionParser( "combine", [ Opts.append, @@ -379,8 +395,8 @@ ] + GLOBAL_ARGS, usage="[options] ... ", description=( - "Combine data from multiple coverage files collected " + - "with 'run -p'. The combined results are written to a single " + + "Combine data from multiple coverage files. " + + "The combined results are written to a single " + "file representing the union of the data. The positional " + "arguments are data files or directories containing data files. " + "If no paths are provided, data files in the default data file's " + @@ -388,7 +404,7 @@ ), ), - 'debug': CmdOptionParser( + "debug": CmdOptionParser( "debug", GLOBAL_ARGS, usage="", description=( @@ -403,7 +419,7 @@ ), ), - 'erase': CmdOptionParser( + "erase": CmdOptionParser( "erase", [ Opts.combine_datafile @@ -411,13 +427,13 @@ description="Erase previously collected coverage data.", ), - 'help': CmdOptionParser( + "help": CmdOptionParser( "help", GLOBAL_ARGS, usage="[command]", description="Describe how to use coverage.py", ), - 'html': CmdOptionParser( + "html": CmdOptionParser( "html", [ Opts.contexts, @@ -443,7 +459,7 @@ ), ), - 'json': CmdOptionParser( + "json": CmdOptionParser( "json", [ Opts.contexts, @@ -461,7 +477,7 @@ description="Generate a JSON report of coverage results.", ), - 'lcov': CmdOptionParser( + "lcov": CmdOptionParser( "lcov", [ Opts.input_datafile, @@ -476,12 +492,13 @@ description="Generate an LCOV report of coverage results.", ), - 'report': CmdOptionParser( + "report": CmdOptionParser( "report", [ Opts.contexts, Opts.input_datafile, Opts.fail_under, + Opts.format, Opts.ignore_errors, Opts.include, Opts.omit, @@ -496,7 +513,7 @@ description="Report coverage statistics on modules.", ), - 'run': CmdOptionParser( + "run": CmdOptionParser( "run", [ Opts.append, @@ -516,7 +533,7 @@ description="Run a Python program, measuring code execution.", ), - 'xml': CmdOptionParser( + "xml": CmdOptionParser( "xml", [ Opts.input_datafile, @@ -534,17 +551,21 @@ } -def show_help(error=None, topic=None, parser=None): +def show_help( + error: Optional[str] = None, + topic: Optional[str] = None, + parser: Optional[optparse.OptionParser] = None, +) -> None: """Display an error message, or the named topic.""" assert error or topic or parser program_path = sys.argv[0] - if program_path.endswith(os.path.sep + '__main__.py'): + if program_path.endswith(os.path.sep + "__main__.py"): # The path is the main module of a package; get that path instead. program_path = os.path.dirname(program_path) program_name = os.path.basename(program_path) if env.WINDOWS: - # entry_points={'console_scripts':...} on Windows makes files + # entry_points={"console_scripts":...} on Windows makes files # called coverage.exe, coverage3.exe, and coverage-3.5.exe. These # invoke coverage-script.py, coverage3-script.py, and # coverage-3.5-script.py. argv[0] is the .py file, but we want to @@ -554,11 +575,12 @@ program_name = program_name[:-len(auto_suffix)] help_params = dict(coverage.__dict__) - help_params['program_name'] = program_name - if CTracer is not None: - help_params['extension_modifier'] = 'with C extension' + help_params["__url__"] = __url__ + help_params["program_name"] = program_name + if HAS_CTRACER: + help_params["extension_modifier"] = "with C extension" else: - help_params['extension_modifier'] = 'without C extension' + help_params["extension_modifier"] = "without C extension" if error: print(error, file=sys.stderr) @@ -567,7 +589,8 @@ print(parser.format_help().strip()) print() else: - help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip() + assert topic is not None + help_msg = textwrap.dedent(HELP_TOPICS.get(topic, "")).strip() if help_msg: print(help_msg.format(**help_params)) else: @@ -581,11 +604,11 @@ class CoverageScript: """The command-line interface to coverage.py.""" - def __init__(self): + def __init__(self) -> None: self.global_option = False - self.coverage = None + self.coverage: Coverage - def command_line(self, argv): + def command_line(self, argv: List[str]) -> int: """The bulk of the command line interface to coverage.py. `argv` is the argument list to process. @@ -595,12 +618,13 @@ """ # Collect the command-line options. if not argv: - show_help(topic='minimum_help') + show_help(topic="minimum_help") return OK # The command syntax we parse depends on the first argument. Global # switch syntax always starts with an option. - self.global_option = argv[0].startswith('-') + parser: Optional[optparse.OptionParser] + self.global_option = argv[0].startswith("-") if self.global_option: parser = GlobalOptionParser() else: @@ -613,6 +637,7 @@ ok, options, args = parser.parse_args_ok(argv) if not ok: return ERR + assert options is not None # Handle help and version. if self.do_help(options, args, parser): @@ -677,7 +702,7 @@ # We need to be able to import from the current directory, because # plugins may try to, for example, to read Django settings. - sys.path.insert(0, '') + sys.path.insert(0, "") self.coverage.load() @@ -689,6 +714,7 @@ skip_covered=options.skip_covered, skip_empty=options.skip_empty, sort=options.sort, + output_format=options.format, **report_args ) elif options.action == "annotate": @@ -733,8 +759,8 @@ if options.precision is not None: self.coverage.set_option("report:precision", options.precision) - fail_under = self.coverage.get_option("report:fail_under") - precision = self.coverage.get_option("report:precision") + fail_under = cast(float, self.coverage.get_option("report:fail_under")) + precision = cast(int, self.coverage.get_option("report:precision")) if should_fail_under(total, fail_under, precision): msg = "total of {total} is less than fail-under={fail_under:.{p}f}".format( total=Numbers(precision=precision).display_covered(total), @@ -746,7 +772,12 @@ return OK - def do_help(self, options, args, parser): + def do_help( + self, + options: optparse.Values, + args: List[str], + parser: optparse.OptionParser, + ) -> bool: """Deal with help requests. Return True if it handled the request, False if not. @@ -755,7 +786,7 @@ # Handle help. if options.help: if self.global_option: - show_help(topic='help') + show_help(topic="help") else: show_help(parser=parser) return True @@ -763,23 +794,23 @@ if options.action == "help": if args: for a in args: - parser = COMMANDS.get(a) - if parser: - show_help(parser=parser) + parser_maybe = COMMANDS.get(a) + if parser_maybe is not None: + show_help(parser=parser_maybe) else: show_help(topic=a) else: - show_help(topic='help') + show_help(topic="help") return True # Handle version. if options.version: - show_help(topic='version') + show_help(topic="version") return True return False - def do_run(self, options, args): + def do_run(self, options: optparse.Values, args: List[str]) -> int: """Implementation of 'coverage run'.""" if not args: @@ -787,7 +818,7 @@ # Specified -m with nothing else. show_help("No module specified for -m") return ERR - command_line = self.coverage.get_option("run:command_line") + command_line = cast(str, self.coverage.get_option("run:command_line")) if command_line is not None: args = shlex.split(command_line) if args and args[0] in {"-m", "--module"}: @@ -804,7 +835,7 @@ if options.concurrency == "multiprocessing": # Can't set other run-affecting command line options with # multiprocessing. - for opt_name in ['branch', 'include', 'omit', 'pylib', 'source', 'timid']: + for opt_name in ["branch", "include", "omit", "pylib", "source", "timid"]: # As it happens, all of these options have no default, meaning # they will be None if they have not been specified. if getattr(options, opt_name) is not None: @@ -838,7 +869,7 @@ return OK - def do_debug(self, args): + def do_debug(self, args: List[str]) -> int: """Implementation of 'coverage debug'.""" if not args: @@ -871,7 +902,7 @@ return OK -def unshell_list(s): +def unshell_list(s: str) -> Optional[List[str]]: """Turn a command-line argument into a list.""" if not s: return None @@ -882,15 +913,15 @@ # line, but (not) helpfully, the single quotes are included in the # argument, so we have to strip them off here. s = s.strip("'") - return s.split(',') + return s.split(",") -def unglob_args(args): +def unglob_args(args: List[str]) -> List[str]: """Interpret shell wildcards for platforms that need it.""" if env.WINDOWS: globbed = [] for arg in args: - if '?' in arg or '*' in arg: + if "?" in arg or "*" in arg: globbed.extend(glob.glob(arg)) else: globbed.append(arg) @@ -899,7 +930,7 @@ HELP_TOPICS = { - 'help': """\ + "help": """\ Coverage.py, version {__version__} {extension_modifier} Measure, collect, and report on code coverage in Python programs. @@ -921,17 +952,16 @@ Use "{program_name} help " for detailed help on any command. """, - 'minimum_help': """\ - Code coverage for Python, version {__version__} {extension_modifier}. Use '{program_name} help' for help. - """, + "minimum_help": ( + "Code coverage for Python, version {__version__} {extension_modifier}. " + + "Use '{program_name} help' for help." + ), - 'version': """\ - Coverage.py, version {__version__} {extension_modifier} - """, + "version": "Coverage.py, version {__version__} {extension_modifier}", } -def main(argv=None): +def main(argv: Optional[List[str]] = None) -> Optional[int]: """The main entry point to coverage.py. This is installed as the script entry point. @@ -969,12 +999,14 @@ from ox_profile.core.launchers import SimpleLauncher # pylint: disable=import-error original_main = main - def main(argv=None): # pylint: disable=function-redefined + def main( # pylint: disable=function-redefined + argv: Optional[List[str]] = None, + ) -> Optional[int]: """A wrapper around main that profiles.""" profiler = SimpleLauncher.launch() try: return original_main(argv) finally: - data, _ = profiler.query(re_filter='coverage', max_records=100) - print(profiler.show(query=data, limit=100, sep='', col='')) + data, _ = profiler.query(re_filter="coverage", max_records=100) + print(profiler.show(query=data, limit=100, sep="", col="")) profiler.cancel() diff -Nru python-coverage-6.5.0+dfsg1/coverage/collector.py python-coverage-7.2.7+dfsg1/coverage/collector.py --- python-coverage-6.5.0+dfsg1/coverage/collector.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/collector.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,16 +3,29 @@ """Raw data collector for coverage.py.""" +from __future__ import annotations + +import functools import os import sys +from types import FrameType +from typing import ( + cast, Any, Callable, Dict, List, Mapping, Optional, Set, Tuple, Type, TypeVar, +) + from coverage import env from coverage.config import CoverageConfig +from coverage.data import CoverageData from coverage.debug import short_stack from coverage.disposition import FileDisposition from coverage.exceptions import ConfigError -from coverage.misc import human_sorted, isolate_module +from coverage.misc import human_sorted_items, isolate_module +from coverage.plugin import CoveragePlugin from coverage.pytracer import PyTracer +from coverage.types import ( + TArc, TFileDisposition, TLineNo, TTraceData, TTraceFn, TTracer, TWarnFn, +) os = isolate_module(os) @@ -20,6 +33,7 @@ try: # Use the C extension code when we can, for speed. from coverage.tracer import CTracer, CFileDisposition + HAS_CTRACER = True except ImportError: # Couldn't import the C extension, maybe it isn't built. if os.getenv('COVERAGE_TEST_TRACER') == 'c': # pragma: part covered @@ -31,8 +45,9 @@ # exception here causes all sorts of other noise in unittest. sys.stderr.write("*** COVERAGE_TEST_TRACER is 'c' but can't import CTracer!\n") sys.exit(1) - CTracer = None + HAS_CTRACER = False +T = TypeVar("T") class Collector: """Collects trace data. @@ -53,15 +68,22 @@ # The stack of active Collectors. Collectors are added here when started, # and popped when stopped. Collectors on the stack are paused when not # the top, and resumed when they become the top again. - _collectors = [] + _collectors: List[Collector] = [] # The concurrency settings we support here. LIGHT_THREADS = {"greenlet", "eventlet", "gevent"} def __init__( - self, should_trace, check_include, should_start_context, file_mapper, - timid, branch, warn, concurrency, - ): + self, + should_trace: Callable[[str, FrameType], TFileDisposition], + check_include: Callable[[str, FrameType], bool], + should_start_context: Optional[Callable[[FrameType], Optional[str]]], + file_mapper: Callable[[str], str], + timid: bool, + branch: bool, + warn: TWarnFn, + concurrency: List[str], + ) -> None: """Create a collector. `should_trace` is a function, taking a file name and a frame, and @@ -107,28 +129,29 @@ self.concurrency = concurrency assert isinstance(self.concurrency, list), f"Expected a list: {self.concurrency!r}" + self.covdata: CoverageData self.threading = None - self.covdata = None - self.static_context = None + self.static_context: Optional[str] = None self.origin = short_stack() self.concur_id_func = None - self.mapped_file_cache = {} - if timid: - # Being timid: use the simple Python trace function. - self._trace_class = PyTracer - else: - # Being fast: use the C Tracer if it is available, else the Python - # trace function. - self._trace_class = CTracer or PyTracer + self._trace_class: Type[TTracer] + self.file_disposition_class: Type[TFileDisposition] - if self._trace_class is CTracer: + use_ctracer = False + if HAS_CTRACER and not timid: + use_ctracer = True + + #if HAS_CTRACER and self._trace_class is CTracer: + if use_ctracer: + self._trace_class = CTracer self.file_disposition_class = CFileDisposition self.supports_plugins = True self.packed_arcs = True else: + self._trace_class = PyTracer self.file_disposition_class = FileDisposition self.supports_plugins = False self.packed_arcs = False @@ -182,22 +205,22 @@ self.reset() - def __repr__(self): + def __repr__(self) -> str: return f"" - def use_data(self, covdata, context): + def use_data(self, covdata: CoverageData, context: Optional[str]) -> None: """Use `covdata` for recording data.""" self.covdata = covdata self.static_context = context self.covdata.set_context(self.static_context) - def tracer_name(self): + def tracer_name(self) -> str: """Return the class name of the tracer we're using.""" return self._trace_class.__name__ - def _clear_data(self): + def _clear_data(self) -> None: """Clear out existing data, but stay ready for more collection.""" - # We used to used self.data.clear(), but that would remove filename + # We used to use self.data.clear(), but that would remove filename # keys and data values that were still in use higher up the stack # when we are called as part of switch_context. for d in self.data.values(): @@ -206,18 +229,16 @@ for tracer in self.tracers: tracer.reset_activity() - def reset(self): + def reset(self) -> None: """Clear collected data, and prepare to collect more.""" - # A dictionary mapping file names to dicts with line number keys (if not - # branch coverage), or mapping file names to dicts with line number - # pairs as keys (if branch coverage). - self.data = {} + # The trace data we are collecting. + self.data: TTraceData = {} # A dictionary mapping file names to file tracer plugin names that will # handle them. - self.file_tracers = {} + self.file_tracers: Dict[str, str] = {} - self.disabled_plugins = set() + self.disabled_plugins: Set[str] = set() # The .should_trace_cache attribute is a cache from file names to # coverage.FileDisposition objects, or None. When a file is first @@ -248,11 +269,11 @@ self.should_trace_cache = {} # Our active Tracers. - self.tracers = [] + self.tracers: List[TTracer] = [] self._clear_data() - def _start_tracer(self): + def _start_tracer(self) -> TTraceFn: """Start a new Tracer object, and store it in self.tracers.""" tracer = self._trace_class() tracer.data = self.data @@ -271,6 +292,7 @@ tracer.check_include = self.check_include if hasattr(tracer, 'should_start_context'): tracer.should_start_context = self.should_start_context + if hasattr(tracer, 'switch_context'): tracer.switch_context = self.switch_context if hasattr(tracer, 'disable_plugin'): tracer.disable_plugin = self.disable_plugin @@ -288,12 +310,12 @@ # # New in 3.12: threading.settrace_all_threads: https://github.com/python/cpython/pull/96681 - def _installation_trace(self, frame, event, arg): + def _installation_trace(self, frame: FrameType, event: str, arg: Any) -> Optional[TTraceFn]: """Called on new threads, installs the real tracer.""" # Remove ourselves as the trace function. sys.settrace(None) # Install the real tracer. - fn = self._start_tracer() + fn: Optional[TTraceFn] = self._start_tracer() # Invoke the real trace function with the current event, to be sure # not to lose an event. if fn: @@ -301,7 +323,7 @@ # Return the new trace function to continue tracing in this scope. return fn - def start(self): + def start(self) -> None: """Start collecting trace information.""" if self._collectors: self._collectors[-1].pause() @@ -310,7 +332,7 @@ # Check to see whether we had a fullcoverage tracer installed. If so, # get the stack frames it stashed away for us. - traces0 = [] + traces0: List[Tuple[Tuple[FrameType, str, Any], TLineNo]] = [] fn0 = sys.gettrace() if fn0: tracer0 = getattr(fn0, '__self__', None) @@ -334,14 +356,14 @@ try: fn(frame, event, arg, lineno=lineno) except TypeError as ex: - raise Exception("fullcoverage must be run with the C trace function.") from ex + raise RuntimeError("fullcoverage must be run with the C trace function.") from ex # Install our installation tracer in threading, to jump-start other # threads. if self.threading: self.threading.settrace(self._installation_trace) - def stop(self): + def stop(self) -> None: """Stop collecting trace information.""" assert self._collectors if self._collectors[-1] is not self: @@ -360,19 +382,19 @@ if self._collectors: self._collectors[-1].resume() - def pause(self): + def pause(self) -> None: """Pause tracing, but be prepared to `resume`.""" for tracer in self.tracers: tracer.stop() stats = tracer.get_stats() if stats: print("\nCoverage.py tracer stats:") - for k in human_sorted(stats.keys()): - print(f"{k:>20}: {stats[k]}") + for k, v in human_sorted_items(stats.items()): + print(f"{k:>20}: {v}") if self.threading: self.threading.settrace(None) - def resume(self): + def resume(self) -> None: """Resume tracing after a `pause`.""" for tracer in self.tracers: tracer.start() @@ -381,7 +403,7 @@ else: self._start_tracer() - def _activity(self): + def _activity(self) -> bool: """Has any activity been traced? Returns a boolean, True if any trace function was invoked. @@ -389,8 +411,9 @@ """ return any(tracer.activity() for tracer in self.tracers) - def switch_context(self, new_context): + def switch_context(self, new_context: Optional[str]) -> None: """Switch to a new dynamic context.""" + context: Optional[str] self.flush_data() if self.static_context: context = self.static_context @@ -400,24 +423,22 @@ context = new_context self.covdata.set_context(context) - def disable_plugin(self, disposition): + def disable_plugin(self, disposition: TFileDisposition) -> None: """Disable the plugin mentioned in `disposition`.""" file_tracer = disposition.file_tracer + assert file_tracer is not None plugin = file_tracer._coverage_plugin plugin_name = plugin._coverage_plugin_name self.warn(f"Disabling plug-in {plugin_name!r} due to previous exception") plugin._coverage_enabled = False disposition.trace = False - def cached_mapped_file(self, filename): + @functools.lru_cache(maxsize=None) # pylint: disable=method-cache-max-size-none + def cached_mapped_file(self, filename: str) -> str: """A locally cached version of file names mapped through file_mapper.""" - key = (type(filename), filename) - try: - return self.mapped_file_cache[key] - except KeyError: - return self.mapped_file_cache.setdefault(key, self.file_mapper(filename)) + return self.file_mapper(filename) - def mapped_file_dict(self, d): + def mapped_file_dict(self, d: Mapping[str, T]) -> Dict[str, T]: """Return a dict like d, but with keys modified by file_mapper.""" # The call to list(items()) ensures that the GIL protects the dictionary # iterator against concurrent modifications by tracers running @@ -431,16 +452,17 @@ runtime_err = ex else: break - else: - raise runtime_err # pragma: cant happen + else: # pragma: cant happen + assert isinstance(runtime_err, Exception) + raise runtime_err - return {self.cached_mapped_file(k): v for k, v in items} + return {self.cached_mapped_file(k): v for k, v in items if v} - def plugin_was_disabled(self, plugin): + def plugin_was_disabled(self, plugin: CoveragePlugin) -> None: """Record that `plugin` was disabled during the run.""" self.disabled_plugins.add(plugin._coverage_plugin_name) - def flush_data(self): + def flush_data(self) -> bool: """Save the collected data to our associated `CoverageData`. Data may have also been saved along the way. This forces the @@ -456,8 +478,9 @@ # Unpack the line number pairs packed into integers. See # tracer.c:CTracer_record_pair for the C code that creates # these packed ints. - data = {} - for fname, packeds in self.data.items(): + arc_data: Dict[str, List[TArc]] = {} + packed_data = cast(Dict[str, Set[int]], self.data) + for fname, packeds in packed_data.items(): tuples = [] for packed in packeds: l1 = packed & 0xFFFFF @@ -467,12 +490,13 @@ if packed & (1 << 41): l2 *= -1 tuples.append((l1, l2)) - data[fname] = tuples + arc_data[fname] = tuples else: - data = self.data - self.covdata.add_arcs(self.mapped_file_dict(data)) + arc_data = cast(Dict[str, List[TArc]], self.data) + self.covdata.add_arcs(self.mapped_file_dict(arc_data)) else: - self.covdata.add_lines(self.mapped_file_dict(self.data)) + line_data = cast(Dict[str, Set[int]], self.data) + self.covdata.add_lines(self.mapped_file_dict(line_data)) file_tracers = { k: v for k, v in self.file_tracers.items() diff -Nru python-coverage-6.5.0+dfsg1/coverage/config.py python-coverage-7.2.7+dfsg1/coverage/config.py --- python-coverage-6.5.0+dfsg1/coverage/config.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/config.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Config file for coverage.py""" +from __future__ import annotations + import collections import configparser import copy @@ -10,18 +12,25 @@ import os.path import re -from coverage.exceptions import ConfigError -from coverage.misc import contract, isolate_module, human_sorted_items, substitute_variables +from typing import ( + Any, Callable, Dict, Iterable, List, Optional, Tuple, Union, +) +from coverage.exceptions import ConfigError +from coverage.misc import isolate_module, human_sorted_items, substitute_variables from coverage.tomlconfig import TomlConfigParser, TomlDecodeError +from coverage.types import ( + TConfigurable, TConfigSectionIn, TConfigValueIn, TConfigSectionOut, + TConfigValueOut, TPluginConfig, +) os = isolate_module(os) -class HandyConfigParser(configparser.RawConfigParser): +class HandyConfigParser(configparser.ConfigParser): """Our specialization of ConfigParser.""" - def __init__(self, our_file): + def __init__(self, our_file: bool) -> None: """Create the HandyConfigParser. `our_file` is True if this config file is specifically for coverage, @@ -29,49 +38,54 @@ for possible settings. """ - configparser.RawConfigParser.__init__(self) + super().__init__(interpolation=None) self.section_prefixes = ["coverage:"] if our_file: self.section_prefixes.append("") - def read(self, filenames, encoding_unused=None): + def read( # type: ignore[override] + self, + filenames: Iterable[str], + encoding_unused: Optional[str] = None, + ) -> List[str]: """Read a file name as UTF-8 configuration data.""" - return configparser.RawConfigParser.read(self, filenames, encoding="utf-8") - - def has_option(self, section, option): - for section_prefix in self.section_prefixes: - real_section = section_prefix + section - has = configparser.RawConfigParser.has_option(self, real_section, option) - if has: - return has - return False + return super().read(filenames, encoding="utf-8") - def has_section(self, section): + def real_section(self, section: str) -> Optional[str]: + """Get the actual name of a section.""" for section_prefix in self.section_prefixes: real_section = section_prefix + section - has = configparser.RawConfigParser.has_section(self, real_section) + has = super().has_section(real_section) if has: return real_section + return None + + def has_option(self, section: str, option: str) -> bool: + real_section = self.real_section(section) + if real_section is not None: + return super().has_option(real_section, option) return False - def options(self, section): - for section_prefix in self.section_prefixes: - real_section = section_prefix + section - if configparser.RawConfigParser.has_section(self, real_section): - return configparser.RawConfigParser.options(self, real_section) + def has_section(self, section: str) -> bool: + return bool(self.real_section(section)) + + def options(self, section: str) -> List[str]: + real_section = self.real_section(section) + if real_section is not None: + return super().options(real_section) raise ConfigError(f"No section: {section!r}") - def get_section(self, section): + def get_section(self, section: str) -> TConfigSectionOut: """Get the contents of a section, as a dictionary.""" - d = {} + d: Dict[str, TConfigValueOut] = {} for opt in self.options(section): d[opt] = self.get(section, opt) return d - def get(self, section, option, *args, **kwargs): + def get(self, section: str, option: str, *args: Any, **kwargs: Any) -> str: # type: ignore """Get a value, replacing environment variables also. - The arguments are the same as `RawConfigParser.get`, but in the found + The arguments are the same as `ConfigParser.get`, but in the found value, ``$WORD`` or ``${WORD}`` are replaced by the value of the environment variable ``WORD``. @@ -80,38 +94,38 @@ """ for section_prefix in self.section_prefixes: real_section = section_prefix + section - if configparser.RawConfigParser.has_option(self, real_section, option): + if super().has_option(real_section, option): break else: raise ConfigError(f"No option {option!r} in section: {section!r}") - v = configparser.RawConfigParser.get(self, real_section, option, *args, **kwargs) + v: str = super().get(real_section, option, *args, **kwargs) v = substitute_variables(v, os.environ) return v - def getlist(self, section, option): + def getlist(self, section: str, option: str) -> List[str]: """Read a list of strings. The value of `section` and `option` is treated as a comma- and newline- - separated list of strings. Each value is stripped of whitespace. + separated list of strings. Each value is stripped of white space. Returns the list of strings. """ value_list = self.get(section, option) values = [] - for value_line in value_list.split('\n'): - for value in value_line.split(','): + for value_line in value_list.split("\n"): + for value in value_line.split(","): value = value.strip() if value: values.append(value) return values - def getregexlist(self, section, option): + def getregexlist(self, section: str, option: str) -> List[str]: """Read a list of full-line regexes. The value of `section` and `option` is treated as a newline-separated - list of regexes. Each value is stripped of whitespace. + list of regexes. Each value is stripped of white space. Returns the list of strings. @@ -131,26 +145,29 @@ return value_list +TConfigParser = Union[HandyConfigParser, TomlConfigParser] + + # The default line exclusion regexes. DEFAULT_EXCLUDE = [ - r'#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER)', + r"#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER)", ] # The default partial branch regexes, to be modified by the user. DEFAULT_PARTIAL = [ - r'#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(branch|BRANCH)', + r"#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(branch|BRANCH)", ] # The default partial branch regexes, based on Python semantics. # These are any Python branching constructs that can't actually execute all # their branches. DEFAULT_PARTIAL_ALWAYS = [ - 'while (True|1|False|0):', - 'if (True|1|False|0):', + "while (True|1|False|0):", + "if (True|1|False|0):", ] -class CoverageConfig: +class CoverageConfig(TConfigurable, TPluginConfig): """Coverage.py configuration. The attributes of this class are the various settings that control the @@ -159,16 +176,16 @@ """ # pylint: disable=too-many-instance-attributes - def __init__(self): + def __init__(self) -> None: """Initialize the configuration attributes to their defaults.""" # Metadata about the config. # We tried to read these config files. - self.attempted_config_files = [] + self.attempted_config_files: List[str] = [] # We did read these config files, but maybe didn't find any content for us. - self.config_files_read = [] + self.config_files_read: List[str] = [] # The file that gave us our configuration. - self.config_file = None - self._config_contents = None + self.config_file: Optional[str] = None + self._config_contents: Optional[bytes] = None # Defaults for [run] and [report] self._include = None @@ -176,46 +193,49 @@ # Defaults for [run] self.branch = False - self.command_line = None - self.concurrency = None - self.context = None + self.command_line: Optional[str] = None + self.concurrency: List[str] = [] + self.context: Optional[str] = None self.cover_pylib = False self.data_file = ".coverage" - self.debug = [] - self.disable_warnings = [] - self.dynamic_context = None - self.note = None + self.debug: List[str] = [] + self.debug_file: Optional[str] = None + self.disable_warnings: List[str] = [] + self.dynamic_context: Optional[str] = None self.parallel = False - self.plugins = [] + self.plugins: List[str] = [] self.relative_files = False - self.run_include = None - self.run_omit = None + self.run_include: List[str] = [] + self.run_omit: List[str] = [] self.sigterm = False - self.source = None - self.source_pkgs = [] + self.source: Optional[List[str]] = None + self.source_pkgs: List[str] = [] self.timid = False - self._crash = None + self._crash: Optional[str] = None # Defaults for [report] self.exclude_list = DEFAULT_EXCLUDE[:] + self.exclude_also: List[str] = [] self.fail_under = 0.0 + self.format: Optional[str] = None self.ignore_errors = False - self.report_include = None - self.report_omit = None + self.include_namespace_packages = False + self.report_include: Optional[List[str]] = None + self.report_omit: Optional[List[str]] = None self.partial_always_list = DEFAULT_PARTIAL_ALWAYS[:] self.partial_list = DEFAULT_PARTIAL[:] self.precision = 0 - self.report_contexts = None + self.report_contexts: Optional[List[str]] = None self.show_missing = False self.skip_covered = False self.skip_empty = False - self.sort = None + self.sort: Optional[str] = None # Defaults for [html] - self.extra_css = None + self.extra_css: Optional[str] = None self.html_dir = "htmlcov" - self.html_skip_covered = None - self.html_skip_empty = None + self.html_skip_covered: Optional[bool] = None + self.html_skip_empty: Optional[bool] = None self.html_title = "Coverage report" self.show_contexts = False @@ -232,10 +252,10 @@ self.lcov_output = "coverage.lcov" # Defaults for [paths] - self.paths = collections.OrderedDict() + self.paths: Dict[str, List[str]] = {} # Options for plugins - self.plugin_options = {} + self.plugin_options: Dict[str, TConfigSectionOut] = {} MUST_BE_LIST = { "debug", "concurrency", "plugins", @@ -243,7 +263,7 @@ "run_omit", "run_include", } - def from_args(self, **kwargs): + def from_args(self, **kwargs: TConfigValueIn) -> None: """Read config values from `kwargs`.""" for k, v in kwargs.items(): if v is not None: @@ -251,8 +271,7 @@ v = [v] setattr(self, k, v) - @contract(filename=str) - def from_file(self, filename, warn, our_file): + def from_file(self, filename: str, warn: Callable[[str], None], our_file: bool) -> bool: """Read configuration from a .rc file. `filename` is a file name to read. @@ -266,7 +285,8 @@ """ _, ext = os.path.splitext(filename) - if ext == '.toml': + cp: TConfigParser + if ext == ".toml": cp = TomlConfigParser(our_file) else: cp = HandyConfigParser(our_file) @@ -298,7 +318,7 @@ all_options[section].add(option) for section, options in all_options.items(): - real_section = cp.has_section(section) + real_section = cp.real_section(section) if real_section: for unknown in set(cp.options(section)) - options: warn( @@ -308,9 +328,9 @@ ) # [paths] is special - if cp.has_section('paths'): - for option in cp.options('paths'): - self.paths[option] = cp.getlist('paths', option) + if cp.has_section("paths"): + for option in cp.options("paths"): + self.paths[option] = cp.getlist("paths", option) any_set = True # plugins can have options @@ -334,7 +354,7 @@ return used - def copy(self): + def copy(self) -> CoverageConfig: """Return a copy of the configuration.""" return copy.deepcopy(self) @@ -350,64 +370,73 @@ # configuration value from the file. # [run] - ('branch', 'run:branch', 'boolean'), - ('command_line', 'run:command_line'), - ('concurrency', 'run:concurrency', 'list'), - ('context', 'run:context'), - ('cover_pylib', 'run:cover_pylib', 'boolean'), - ('data_file', 'run:data_file'), - ('debug', 'run:debug', 'list'), - ('disable_warnings', 'run:disable_warnings', 'list'), - ('dynamic_context', 'run:dynamic_context'), - ('note', 'run:note'), - ('parallel', 'run:parallel', 'boolean'), - ('plugins', 'run:plugins', 'list'), - ('relative_files', 'run:relative_files', 'boolean'), - ('run_include', 'run:include', 'list'), - ('run_omit', 'run:omit', 'list'), - ('sigterm', 'run:sigterm', 'boolean'), - ('source', 'run:source', 'list'), - ('source_pkgs', 'run:source_pkgs', 'list'), - ('timid', 'run:timid', 'boolean'), - ('_crash', 'run:_crash'), + ("branch", "run:branch", "boolean"), + ("command_line", "run:command_line"), + ("concurrency", "run:concurrency", "list"), + ("context", "run:context"), + ("cover_pylib", "run:cover_pylib", "boolean"), + ("data_file", "run:data_file"), + ("debug", "run:debug", "list"), + ("debug_file", "run:debug_file"), + ("disable_warnings", "run:disable_warnings", "list"), + ("dynamic_context", "run:dynamic_context"), + ("parallel", "run:parallel", "boolean"), + ("plugins", "run:plugins", "list"), + ("relative_files", "run:relative_files", "boolean"), + ("run_include", "run:include", "list"), + ("run_omit", "run:omit", "list"), + ("sigterm", "run:sigterm", "boolean"), + ("source", "run:source", "list"), + ("source_pkgs", "run:source_pkgs", "list"), + ("timid", "run:timid", "boolean"), + ("_crash", "run:_crash"), # [report] - ('exclude_list', 'report:exclude_lines', 'regexlist'), - ('fail_under', 'report:fail_under', 'float'), - ('ignore_errors', 'report:ignore_errors', 'boolean'), - ('partial_always_list', 'report:partial_branches_always', 'regexlist'), - ('partial_list', 'report:partial_branches', 'regexlist'), - ('precision', 'report:precision', 'int'), - ('report_contexts', 'report:contexts', 'list'), - ('report_include', 'report:include', 'list'), - ('report_omit', 'report:omit', 'list'), - ('show_missing', 'report:show_missing', 'boolean'), - ('skip_covered', 'report:skip_covered', 'boolean'), - ('skip_empty', 'report:skip_empty', 'boolean'), - ('sort', 'report:sort'), + ("exclude_list", "report:exclude_lines", "regexlist"), + ("exclude_also", "report:exclude_also", "regexlist"), + ("fail_under", "report:fail_under", "float"), + ("format", "report:format", "boolean"), + ("ignore_errors", "report:ignore_errors", "boolean"), + ("include_namespace_packages", "report:include_namespace_packages", "boolean"), + ("partial_always_list", "report:partial_branches_always", "regexlist"), + ("partial_list", "report:partial_branches", "regexlist"), + ("precision", "report:precision", "int"), + ("report_contexts", "report:contexts", "list"), + ("report_include", "report:include", "list"), + ("report_omit", "report:omit", "list"), + ("show_missing", "report:show_missing", "boolean"), + ("skip_covered", "report:skip_covered", "boolean"), + ("skip_empty", "report:skip_empty", "boolean"), + ("sort", "report:sort"), # [html] - ('extra_css', 'html:extra_css'), - ('html_dir', 'html:directory'), - ('html_skip_covered', 'html:skip_covered', 'boolean'), - ('html_skip_empty', 'html:skip_empty', 'boolean'), - ('html_title', 'html:title'), - ('show_contexts', 'html:show_contexts', 'boolean'), + ("extra_css", "html:extra_css"), + ("html_dir", "html:directory"), + ("html_skip_covered", "html:skip_covered", "boolean"), + ("html_skip_empty", "html:skip_empty", "boolean"), + ("html_title", "html:title"), + ("show_contexts", "html:show_contexts", "boolean"), # [xml] - ('xml_output', 'xml:output'), - ('xml_package_depth', 'xml:package_depth', 'int'), + ("xml_output", "xml:output"), + ("xml_package_depth", "xml:package_depth", "int"), # [json] - ('json_output', 'json:output'), - ('json_pretty_print', 'json:pretty_print', 'boolean'), - ('json_show_contexts', 'json:show_contexts', 'boolean'), + ("json_output", "json:output"), + ("json_pretty_print", "json:pretty_print", "boolean"), + ("json_show_contexts", "json:show_contexts", "boolean"), # [lcov] - ('lcov_output', 'lcov:output'), + ("lcov_output", "lcov:output"), ] - def _set_attr_from_config_option(self, cp, attr, where, type_=''): + def _set_attr_from_config_option( + self, + cp: TConfigParser, + attr: str, + where: str, + type_: str = "", + ) -> bool: """Set an attribute on self if it exists in the ConfigParser. Returns True if the attribute was set. @@ -415,16 +444,16 @@ """ section, option = where.split(":") if cp.has_option(section, option): - method = getattr(cp, 'get' + type_) + method = getattr(cp, "get" + type_) setattr(self, attr, method(section, option)) return True return False - def get_plugin_options(self, plugin): + def get_plugin_options(self, plugin: str) -> TConfigSectionOut: """Get a dictionary of options for the plugin named `plugin`.""" return self.plugin_options.get(plugin, {}) - def set_option(self, option_name, value): + def set_option(self, option_name: str, value: Union[TConfigValueIn, TConfigSectionIn]) -> None: """Set an option in the configuration. `option_name` is a colon-separated string indicating the section and @@ -436,7 +465,7 @@ """ # Special-cased options. if option_name == "paths": - self.paths = value + self.paths = value # type: ignore return # Check all the hard-coded options. @@ -449,13 +478,13 @@ # See if it's a plugin option. plugin_name, _, key = option_name.partition(":") if key and plugin_name in self.plugins: - self.plugin_options.setdefault(plugin_name, {})[key] = value + self.plugin_options.setdefault(plugin_name, {})[key] = value # type: ignore return # If we get here, we didn't find the option. raise ConfigError(f"No such option: {option_name!r}") - def get_option(self, option_name): + def get_option(self, option_name: str) -> Optional[TConfigValueOut]: """Get an option from the configuration. `option_name` is a colon-separated string indicating the section and @@ -467,13 +496,13 @@ """ # Special-cased options. if option_name == "paths": - return self.paths + return self.paths # type: ignore # Check all the hard-coded options. for option_spec in self.CONFIG_FILE_OPTIONS: attr, where = option_spec[:2] if where == option_name: - return getattr(self, attr) + return getattr(self, attr) # type: ignore # See if it's a plugin option. plugin_name, _, key = option_name.partition(":") @@ -483,28 +512,29 @@ # If we get here, we didn't find the option. raise ConfigError(f"No such option: {option_name!r}") - def post_process_file(self, path): + def post_process_file(self, path: str) -> str: """Make final adjustments to a file path to make it usable.""" return os.path.expanduser(path) - def post_process(self): + def post_process(self) -> None: """Make final adjustments to settings to make them usable.""" self.data_file = self.post_process_file(self.data_file) self.html_dir = self.post_process_file(self.html_dir) self.xml_output = self.post_process_file(self.xml_output) - self.paths = collections.OrderedDict( + self.paths = dict( (k, [self.post_process_file(f) for f in v]) for k, v in self.paths.items() ) + self.exclude_list += self.exclude_also - def debug_info(self): + def debug_info(self) -> List[Tuple[str, Any]]: """Make a list of (name, value) pairs for writing debug info.""" return human_sorted_items( (k, v) for k, v in self.__dict__.items() if not k.startswith("_") ) -def config_files_to_try(config_file): +def config_files_to_try(config_file: Union[bool, str]) -> List[Tuple[str, bool, bool]]: """What config files should we try to read? Returns a list of tuples: @@ -518,12 +548,14 @@ specified_file = (config_file is not True) if not specified_file: # No file was specified. Check COVERAGE_RCFILE. - config_file = os.environ.get('COVERAGE_RCFILE') - if config_file: + rcfile = os.environ.get("COVERAGE_RCFILE") + if rcfile: + config_file = rcfile specified_file = True if not specified_file: # Still no file specified. Default to .coveragerc config_file = ".coveragerc" + assert isinstance(config_file, str) files_to_try = [ (config_file, True, specified_file), ("setup.cfg", False, False), @@ -533,7 +565,11 @@ return files_to_try -def read_coverage_config(config_file, warn, **kwargs): +def read_coverage_config( + config_file: Union[bool, str], + warn: Callable[[str], None], + **kwargs: TConfigValueIn, +) -> CoverageConfig: """Read the coverage.py configuration. Arguments: @@ -566,10 +602,10 @@ # $set_env.py: COVERAGE_DEBUG - Options for --debug. # 3) from environment variables: - env_data_file = os.environ.get('COVERAGE_FILE') + env_data_file = os.environ.get("COVERAGE_FILE") if env_data_file: config.data_file = env_data_file - debugs = os.environ.get('COVERAGE_DEBUG') + debugs = os.environ.get("COVERAGE_DEBUG") if debugs: config.debug.extend(d.strip() for d in debugs.split(",")) diff -Nru python-coverage-6.5.0+dfsg1/coverage/context.py python-coverage-7.2.7+dfsg1/coverage/context.py --- python-coverage-6.5.0+dfsg1/coverage/context.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/context.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,8 +3,15 @@ """Determine contexts for coverage.py""" +from __future__ import annotations -def combine_context_switchers(context_switchers): +from types import FrameType +from typing import cast, Callable, Optional, Sequence + + +def combine_context_switchers( + context_switchers: Sequence[Callable[[FrameType], Optional[str]]], +) -> Optional[Callable[[FrameType], Optional[str]]]: """Create a single context switcher from multiple switchers. `context_switchers` is a list of functions that take a frame as an @@ -23,7 +30,7 @@ if len(context_switchers) == 1: return context_switchers[0] - def should_start_context(frame): + def should_start_context(frame: FrameType) -> Optional[str]: """The combiner for multiple context switchers.""" for switcher in context_switchers: new_context = switcher(frame) @@ -34,7 +41,7 @@ return should_start_context -def should_start_context_test_function(frame): +def should_start_context_test_function(frame: FrameType) -> Optional[str]: """Is this frame calling a test_* function?""" co_name = frame.f_code.co_name if co_name.startswith("test") or co_name == "runTest": @@ -42,7 +49,7 @@ return None -def qualname_from_frame(frame): +def qualname_from_frame(frame: FrameType) -> Optional[str]: """Get a qualified name for the code running in `frame`.""" co = frame.f_code fname = co.co_name @@ -55,11 +62,11 @@ func = frame.f_globals.get(fname) if func is None: return None - return func.__module__ + "." + fname + return cast(str, func.__module__ + "." + fname) func = getattr(method, "__func__", None) if func is None: cls = self.__class__ - return cls.__module__ + "." + cls.__name__ + "." + fname + return cast(str, cls.__module__ + "." + cls.__name__ + "." + fname) - return func.__module__ + "." + func.__qualname__ + return cast(str, func.__module__ + "." + func.__qualname__) diff -Nru python-coverage-6.5.0+dfsg1/coverage/control.py python-coverage-7.2.7+dfsg1/coverage/control.py --- python-coverage-6.5.0+dfsg1/coverage/control.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/control.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Core control stuff for coverage.py.""" +from __future__ import annotations + import atexit import collections import contextlib @@ -15,13 +17,21 @@ import time import warnings +from types import FrameType +from typing import ( + cast, + Any, Callable, Dict, IO, Iterable, Iterator, List, Optional, Tuple, Union, +) + from coverage import env from coverage.annotate import AnnotateReporter -from coverage.collector import Collector, CTracer -from coverage.config import read_coverage_config +from coverage.collector import Collector, HAS_CTRACER +from coverage.config import CoverageConfig, 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.debug import ( + DebugControl, NoDebugging, short_stack, write_formatted_info, relevant_environment_display +) from coverage.disposition import disposition_debug_msg from coverage.exceptions import ConfigError, CoverageException, CoverageWarning, PluginError from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory @@ -29,26 +39,25 @@ from coverage.inorout import InOrOut from coverage.jsonreport import JsonReporter from coverage.lcovreport import LcovReporter -from coverage.misc import bool_or_none, join_regex, human_sorted +from coverage.misc import bool_or_none, join_regex from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module +from coverage.multiproc import patch_multiprocessing from coverage.plugin import FileReporter from coverage.plugin_support import Plugins from coverage.python import PythonFileReporter -from coverage.report import render_report +from coverage.report import SummaryReporter +from coverage.report_core import render_report from coverage.results import Analysis -from coverage.summary import SummaryReporter +from coverage.types import ( + FilePath, TConfigurable, TConfigSectionIn, TConfigValueIn, TConfigValueOut, + TFileDisposition, TLineNo, TMorf, +) from coverage.xmlreport import XmlReporter -try: - from coverage.multiproc import patch_multiprocessing -except ImportError: # pragma: only jython - # Jython has no multiprocessing module. - patch_multiprocessing = None - os = isolate_module(os) @contextlib.contextmanager -def override_config(cov, **kwargs): +def override_config(cov: Coverage, **kwargs: TConfigValueIn) -> Iterator[None]: """Temporarily tweak the configuration of `cov`. The arguments are applied to `cov.config` with the `from_args` method. @@ -66,7 +75,7 @@ DEFAULT_DATAFILE = DefaultValue("MISSING") _DEFAULT_DATAFILE = DEFAULT_DATAFILE # Just in case, for backwards compatibility -class Coverage: +class Coverage(TConfigurable): """Programmatic access to coverage.py. To use:: @@ -77,7 +86,7 @@ cov.start() #.. call your code .. cov.stop() - cov.html_report(directory='covhtml') + cov.html_report(directory="covhtml") Note: in keeping with Python custom, names starting with underscore are not part of the public API. They might stop working at any point. Please @@ -88,10 +97,10 @@ """ # The stack of started Coverage instances. - _instances = [] + _instances: List[Coverage] = [] @classmethod - def current(cls): + def current(cls) -> Optional[Coverage]: """Get the latest started `Coverage` instance, if any. Returns: a `Coverage` instance, or None. @@ -104,13 +113,25 @@ else: return None - def __init__( - self, data_file=DEFAULT_DATAFILE, data_suffix=None, cover_pylib=None, - 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 + def __init__( # pylint: disable=too-many-arguments + self, + data_file: Optional[Union[FilePath, DefaultValue]] = DEFAULT_DATAFILE, + data_suffix: Optional[Union[str, bool]] = None, + cover_pylib: Optional[bool] = None, + auto_data: bool = False, + timid: Optional[bool] = None, + branch: Optional[bool] = None, + config_file: Union[FilePath, bool] = True, + source: Optional[Iterable[str]] = None, + source_pkgs: Optional[Iterable[str]] = None, + omit: Optional[Union[str, Iterable[str]]] = None, + include: Optional[Union[str, Iterable[str]]] = None, + debug: Optional[Iterable[str]] = None, + concurrency: Optional[Union[str, Iterable[str]]] = None, + check_preimported: bool = False, + context: Optional[str] = None, + messages: bool = False, + ) -> None: """ Many of these arguments duplicate and override values that can be provided in a configuration file. Parameters that are missing here @@ -199,16 +220,20 @@ The `messages` parameter. """ + # Start self.config as a usable default configuration. It will soon be + # replaced with the real configuration. + self.config = CoverageConfig() + # data_file=None means no disk file at all. data_file missing means # use the value from the config file. self._no_disk = data_file is None - if data_file is DEFAULT_DATAFILE: + if isinstance(data_file, DefaultValue): data_file = None - - self.config = None + if data_file is not None: + data_file = os.fspath(data_file) # This is injectable by tests. - self._debug_file = None + self._debug_file: Optional[IO[str]] = None self._auto_load = self._auto_save = auto_data self._data_suffix_specified = data_suffix @@ -217,21 +242,24 @@ self._warn_no_data = True self._warn_unimported_source = True self._warn_preimported_source = check_preimported - self._no_warn_slugs = None + self._no_warn_slugs: List[str] = [] self._messages = messages # A record of all the warnings that have been issued. - self._warnings = [] + self._warnings: List[str] = [] - # Other instance attributes, set later. - self._data = self._collector = None - self._plugins = None - self._inorout = None + # Other instance attributes, set with placebos or placeholders. + # More useful objects will be created later. + self._debug: DebugControl = NoDebugging() + self._inorout: Optional[InOrOut] = None + self._plugins: Plugins = Plugins() + self._data: Optional[CoverageData] = None + self._collector: Optional[Collector] = None + + self._file_mapper: Callable[[str], str] = abs_file self._data_suffix = self._run_suffix = None - self._exclude_re = None - self._debug = None - self._file_mapper = None - self._old_sigterm = None + self._exclude_re: Dict[str, str] = {} + self._old_sigterm: Optional[Callable[[int, Optional[FrameType]], Any]] = None # State machine variables: # Have we initialized everything? @@ -243,13 +271,25 @@ self._should_write_debug = True # Build our configuration from a number of sources. + if not isinstance(config_file, bool): + config_file = os.fspath(config_file) 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, + 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 @@ -260,7 +300,7 @@ if not env.METACOV: _prevent_sub_process_measurement() - def _init(self): + def _init(self) -> None: """Set all the initial state. This is called by the public methods to initialize state. This lets us @@ -273,10 +313,8 @@ self._inited = True - # Create and configure the debugging controller. COVERAGE_DEBUG_FILE - # is an environment variable, the name of a file to append debug logs - # to. - self._debug = DebugControl(self.config.debug, self._debug_file) + # Create and configure the debugging controller. + self._debug = DebugControl(self.config.debug, self._debug_file, self.config.debug_file) if "multiprocessing" in (self.config.concurrency or ()): # Multi-processing uses parallel for the subprocesses, so also use @@ -287,7 +325,8 @@ self._exclude_re = {} set_relative_directory() - self._file_mapper = relative_filename if self.config.relative_files else abs_file + if self.config.relative_files: + self._file_mapper = relative_filename # Load plugins self._plugins = Plugins.load_plugins(self.config.plugins, self.config, self._debug) @@ -300,18 +339,18 @@ # this is a bit childish. :) plugin.configure([self, self.config][int(time.time()) % 2]) - def _post_init(self): + def _post_init(self) -> None: """Stuff to do after everything is initialized.""" if self._should_write_debug: self._should_write_debug = False self._write_startup_debug() - # '[run] _crash' will raise an exception if the value is close by in + # "[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(f"Crashing because called by {self.config._crash}") + raise RuntimeError(f"Crashing because called by {self.config._crash}") - def _write_startup_debug(self): + def _write_startup_debug(self) -> None: """Write out debug info at startup if needed.""" wrote_any = False with self._debug.without_callers(): @@ -335,25 +374,27 @@ if wrote_any: write_formatted_info(self._debug.write, "end", ()) - def _should_trace(self, filename, frame): + def _should_trace(self, filename: str, frame: FrameType) -> TFileDisposition: """Decide whether to trace execution in `filename`. Calls `_should_trace_internal`, and returns the FileDisposition. """ + assert self._inorout is not None disp = self._inorout.should_trace(filename, frame) - if self._debug.should('trace'): + if self._debug.should("trace"): self._debug.write(disposition_debug_msg(disp)) return disp - def _check_include_omit_etc(self, filename, frame): + def _check_include_omit_etc(self, filename: str, frame: FrameType) -> bool: """Check a file name against the include/omit/etc, rules, verbosely. Returns a boolean: True if the file should be traced, False if not. """ + assert self._inorout is not None reason = self._inorout.check_include_omit_etc(filename, frame) - if self._debug.should('trace'): + if self._debug.should("trace"): if not reason: msg = f"Including {filename!r}" else: @@ -362,7 +403,7 @@ return not reason - def _warn(self, msg, slug=None, once=False): + def _warn(self, msg: str, slug: Optional[str] = None, once: bool = False) -> None: """Use `msg` as a warning. For warning suppression, use `slug` as the shorthand. @@ -371,31 +412,30 @@ slug.) """ - if self._no_warn_slugs is None: - if self.config is not None: - self._no_warn_slugs = list(self.config.disable_warnings) - - if self._no_warn_slugs is not None: - if slug in self._no_warn_slugs: - # Don't issue the warning - return + if not self._no_warn_slugs: + self._no_warn_slugs = list(self.config.disable_warnings) + + if slug in self._no_warn_slugs: + # Don't issue the warning + return self._warnings.append(msg) if slug: msg = f"{msg} ({slug})" - if self._debug is not None and self._debug.should('pid'): + if self._debug.should("pid"): msg = f"[{os.getpid()}] {msg}" warnings.warn(msg, category=CoverageWarning, stacklevel=2) if once: + assert slug is not None self._no_warn_slugs.append(slug) - def _message(self, msg): + def _message(self, msg: str) -> None: """Write a message to the user, if configured to do so.""" if self._messages: print(msg) - def get_option(self, option_name): + def get_option(self, option_name: str) -> Optional[TConfigValueOut]: """Get an option from the configuration. `option_name` is a colon-separated string indicating the section and @@ -406,14 +446,14 @@ selected. As a special case, an `option_name` of ``"paths"`` will return an - OrderedDict with the entire ``[paths]`` section value. + dictionary with the entire ``[paths]`` section value. .. versionadded:: 4.0 """ return self.config.get_option(option_name) - def set_option(self, option_name, value): + def set_option(self, option_name: str, value: Union[TConfigValueIn, TConfigSectionIn]) -> None: """Set an option in the configuration. `option_name` is a colon-separated string indicating the section and @@ -438,34 +478,31 @@ branch = True As a special case, an `option_name` of ``"paths"`` will replace the - entire ``[paths]`` section. The value should be an OrderedDict. + entire ``[paths]`` section. The value should be a dictionary. .. versionadded:: 4.0 """ self.config.set_option(option_name, value) - def load(self): + def load(self) -> None: """Load previously-collected coverage data from the data file.""" self._init() - if self._collector: + if self._collector is not None: self._collector.reset() should_skip = self.config.parallel and not os.path.exists(self.config.data_file) if not should_skip: self._init_data(suffix=None) self._post_init() if not should_skip: + assert self._data is not None self._data.read() - def _init_for_start(self): + def _init_for_start(self) -> None: """Initialization for start()""" # Construct the collector. - concurrency = self.config.concurrency or [] + concurrency: List[str] = self.config.concurrency or [] if "multiprocessing" in concurrency: - if not patch_multiprocessing: - raise ConfigError( # pragma: only jython - "multiprocessing is not supported on this Python" - ) if self.config.config_file is None: raise ConfigError("multiprocessing requires a configuration file") patch_multiprocessing(rcfile=self.config.config_file) @@ -510,6 +547,7 @@ self._init_data(suffix) + assert self._data is not None self._collector.use_data(self._data, self.config.context) # Early warning if we aren't going to be able to support plugins. @@ -528,10 +566,11 @@ # Create the file classifying substructure. self._inorout = InOrOut( + config=self.config, warn=self._warn, - debug=(self._debug if self._debug.should('trace') else None), + debug=(self._debug if self._debug.should("trace") else None), + include_namespace_packages=self.config.include_namespace_packages, ) - self._inorout.configure(self.config) self._inorout.plugins = self._plugins self._inorout.disp_class = self._collector.file_disposition_class @@ -546,9 +585,11 @@ # The Python docs seem to imply that SIGTERM works uniformly even # on Windows, but that's not my experience, and this agrees: # https://stackoverflow.com/questions/35772001/x/35792192#35792192 - self._old_sigterm = signal.signal(signal.SIGTERM, self._on_sigterm) + self._old_sigterm = signal.signal( # type: ignore[assignment] + signal.SIGTERM, self._on_sigterm, + ) - def _init_data(self, suffix): + def _init_data(self, suffix: Optional[Union[str, bool]]) -> None: """Create a data file if we don't have one yet.""" if self._data is None: # Create the data file. We do this at construction time so that the @@ -563,7 +604,7 @@ no_disk=self._no_disk, ) - def start(self): + def start(self) -> None: """Start measuring code coverage. Coverage measurement only occurs in functions called after @@ -580,6 +621,9 @@ self._init_for_start() self._post_init() + assert self._collector is not None + assert self._inorout is not None + # Issue warnings for possible problems. self._inorout.warn_conflicting_settings() @@ -595,25 +639,26 @@ self._started = True self._instances.append(self) - def stop(self): + def stop(self) -> None: """Stop measuring code coverage.""" if self._instances: if self._instances[-1] is self: self._instances.pop() if self._started: + assert self._collector is not None self._collector.stop() self._started = False - def _atexit(self, event="atexit"): + def _atexit(self, event: str = "atexit") -> None: """Clean up on process shutdown.""" if self._debug.should("process"): self._debug.write(f"{event}: pid: {os.getpid()}, instance: {self!r}") if self._started: self.stop() - if self._auto_save: + if self._auto_save or event == "sigterm": self.save() - def _on_sigterm(self, signum_unused, frame_unused): + def _on_sigterm(self, signum_unused: int, frame_unused: Optional[FrameType]) -> None: """A handler for signal.SIGTERM.""" self._atexit("sigterm") # Statements after here won't be seen by metacov because we just wrote @@ -621,7 +666,7 @@ signal.signal(signal.SIGTERM, self._old_sigterm) # pragma: not covered os.kill(os.getpid(), signal.SIGTERM) # pragma: not covered - def erase(self): + def erase(self) -> None: """Erase previously collected coverage data. This removes the in-memory data collected in this session as well as @@ -630,14 +675,15 @@ """ self._init() self._post_init() - if self._collector: + if self._collector is not None: self._collector.reset() self._init_data(suffix=None) + assert self._data is not None self._data.erase(parallel=self.config.parallel) self._data = None self._inited_for_start = False - def switch_context(self, new_context): + def switch_context(self, new_context: str) -> None: """Switch to a new dynamic context. `new_context` is a string to use as the :ref:`dynamic context @@ -653,18 +699,19 @@ if not self._started: # pragma: part started raise CoverageException("Cannot switch context, coverage is not started") + assert self._collector is not None if self._collector.should_start_context: self._warn("Conflicting dynamic contexts", slug="dynamic-conflict", once=True) self._collector.switch_context(new_context) - def clear_exclude(self, which='exclude'): + def clear_exclude(self, which: str = "exclude") -> None: """Clear the exclude list.""" self._init() setattr(self.config, which + "_list", []) self._exclude_regex_stale() - def exclude(self, regex, which='exclude'): + def exclude(self, regex: str, which: str = "exclude") -> None: """Exclude source lines from execution consideration. A number of lists of regular expressions are maintained. Each list @@ -684,33 +731,50 @@ excl_list.append(regex) self._exclude_regex_stale() - def _exclude_regex_stale(self): + def _exclude_regex_stale(self) -> None: """Drop all the compiled exclusion regexes, a list was modified.""" self._exclude_re.clear() - def _exclude_regex(self, which): - """Return a compiled regex for the given exclusion list.""" + def _exclude_regex(self, which: str) -> str: + """Return a regex string for the given exclusion list.""" if which not in self._exclude_re: excl_list = getattr(self.config, which + "_list") self._exclude_re[which] = join_regex(excl_list) return self._exclude_re[which] - def get_exclude_list(self, which='exclude'): - """Return a list of excluded regex patterns. + def get_exclude_list(self, which: str = "exclude") -> List[str]: + """Return a list of excluded regex strings. `which` indicates which list is desired. See :meth:`exclude` for the lists that are available, and their meaning. """ self._init() - return getattr(self.config, which + "_list") + return cast(List[str], getattr(self.config, which + "_list")) - def save(self): + def save(self) -> None: """Save the collected coverage data to the data file.""" data = self.get_data() data.write() - def combine(self, data_paths=None, strict=False, keep=False): + def _make_aliases(self) -> PathAliases: + """Create a PathAliases from our configuration.""" + aliases = PathAliases( + debugfn=(self._debug.write if self._debug.should("pathmap") else None), + relative=self.config.relative_files, + ) + for paths in self.config.paths.values(): + result = paths[0] + for pattern in paths[1:]: + aliases.add(pattern, result) + return aliases + + def combine( + self, + data_paths: Optional[Iterable[str]] = None, + strict: bool = False, + keep: bool = False + ) -> None: """Combine together a number of similarly-named coverage data files. All coverage data files whose name starts with `data_file` (from the @@ -741,27 +805,17 @@ self._post_init() self.get_data() - aliases = None - if self.config.paths: - aliases = PathAliases( - debugfn=(self._debug.write if self._debug.should("pathmap") else None), - relative=self.config.relative_files, - ) - for paths in self.config.paths.values(): - result = paths[0] - for pattern in paths[1:]: - aliases.add(pattern, result) - + assert self._data is not None combine_parallel_data( self._data, - aliases=aliases, + aliases=self._make_aliases(), data_paths=data_paths, strict=strict, keep=keep, message=self._message, ) - def get_data(self): + def get_data(self) -> CoverageData: """Get the collected data. Also warn about various problems collecting data. @@ -775,22 +829,27 @@ self._init_data(suffix=None) self._post_init() - for plugin in self._plugins: - if not plugin._coverage_enabled: - self._collector.plugin_was_disabled(plugin) + if self._collector is not None: + for plugin in self._plugins: + if not plugin._coverage_enabled: + self._collector.plugin_was_disabled(plugin) - if self._collector and self._collector.flush_data(): - self._post_save_work() + if self._collector.flush_data(): + self._post_save_work() + assert self._data is not None return self._data - def _post_save_work(self): + def _post_save_work(self) -> None: """After saving data, look for warnings, post-work, etc. Warn about things that should have happened but didn't. - Look for unexecuted files. + Look for un-executed files. """ + assert self._data is not None + assert self._inorout is not None + # If there are still entries in the source_pkgs_unmatched list, # then we never encountered those packages. if self._warn_unimported_source: @@ -801,25 +860,24 @@ self._warn("No data was collected.", slug="no-data-collected") # Touch all the files that could have executed, so that we can - # mark completely unexecuted files as 0% covered. - if self._data is not None: - file_paths = collections.defaultdict(list) - for file_path, plugin_name in self._inorout.find_possibly_unexecuted_files(): - file_path = self._file_mapper(file_path) - file_paths[plugin_name].append(file_path) - for plugin_name, paths in file_paths.items(): - self._data.touch_files(paths, plugin_name) - - if self.config.note: - self._warn("The '[run] note' setting is no longer supported.") + # mark completely un-executed files as 0% covered. + file_paths = collections.defaultdict(list) + for file_path, plugin_name in self._inorout.find_possibly_unexecuted_files(): + file_path = self._file_mapper(file_path) + file_paths[plugin_name].append(file_path) + for plugin_name, paths in file_paths.items(): + self._data.touch_files(paths, plugin_name) # Backward compatibility with version 1. - def analysis(self, morf): + def analysis(self, morf: TMorf) -> Tuple[str, List[TLineNo], List[TLineNo], str]: """Like `analysis2` but doesn't return excluded line numbers.""" f, s, _, m, mf = self.analysis2(morf) return f, s, m, mf - def analysis2(self, morf): + def analysis2( + self, + morf: TMorf, + ) -> Tuple[str, List[TLineNo], List[TLineNo], List[TLineNo], str]: """Analyze a module. `morf` is a module or a file name. It will be analyzed to determine @@ -845,7 +903,7 @@ analysis.missing_formatted(), ) - def _analyze(self, it): + def _analyze(self, it: Union[FileReporter, TMorf]) -> Analysis: """Analyze a single morf or code unit. Returns an `Analysis` object. @@ -856,15 +914,18 @@ self._post_init() data = self.get_data() - if not isinstance(it, FileReporter): - it = self._get_file_reporter(it) + if isinstance(it, FileReporter): + fr = it + else: + fr = self._get_file_reporter(it) - return Analysis(data, self.config.precision, it, self._file_mapper) + return Analysis(data, self.config.precision, fr, self._file_mapper) - def _get_file_reporter(self, morf): + def _get_file_reporter(self, morf: TMorf) -> FileReporter: """Get a FileReporter for a module or file name.""" + assert self._data is not None plugin = None - file_reporter = "python" + file_reporter: Union[str, FileReporter] = "python" if isinstance(morf, str): mapped_morf = self._file_mapper(morf) @@ -884,9 +945,10 @@ if file_reporter == "python": file_reporter = PythonFileReporter(morf, self) + assert isinstance(file_reporter, FileReporter) return file_reporter - def _get_file_reporters(self, morfs=None): + def _get_file_reporters(self, morfs: Optional[Iterable[TMorf]] = None) -> List[FileReporter]: """Get a list of FileReporters for a list of modules or file names. For each module or file name in `morfs`, find a FileReporter. Return @@ -897,21 +959,40 @@ measured is used to find the FileReporters. """ + assert self._data is not None if not morfs: morfs = self._data.measured_files() # Be sure we have a collection. if not isinstance(morfs, (list, tuple, set)): - morfs = [morfs] + morfs = [morfs] # type: ignore[list-item] file_reporters = [self._get_file_reporter(morf) for morf in morfs] return file_reporters + def _prepare_data_for_reporting(self) -> None: + """Re-map data before reporting, to get implicit "combine" behavior.""" + if self.config.paths: + mapped_data = CoverageData(warn=self._warn, debug=self._debug, no_disk=True) + if self._data is not None: + mapped_data.update(self._data, aliases=self._make_aliases()) + self._data = mapped_data + def report( - self, morfs=None, show_missing=None, ignore_errors=None, - file=None, omit=None, include=None, skip_covered=None, - contexts=None, skip_empty=None, precision=None, sort=None - ): + self, + morfs: Optional[Iterable[TMorf]] = None, + show_missing: Optional[bool] = None, + ignore_errors: Optional[bool] = None, + file: Optional[IO[str]] = None, + omit: Optional[Union[str, List[str]]] = None, + include: Optional[Union[str, List[str]]] = None, + skip_covered: Optional[bool] = None, + contexts: Optional[List[str]] = None, + skip_empty: Optional[bool] = None, + precision: Optional[int] = None, + sort: Optional[str] = None, + output_format: Optional[str] = None, + ) -> float: """Write a textual summary report to `file`. Each module in `morfs` is listed, with counts of statements, executed @@ -924,6 +1005,9 @@ `file` is a file-like object, suitable for writing. + `output_format` determines the format, either "text" (the default), + "markdown", or "total". + `include` is a list of file name patterns. Files that match will be included in the report. Files matching `omit` will not be included in the report. @@ -933,7 +1017,7 @@ If `skip_empty` is true, don't report on empty files (those that have no statements). - `contexts` is a list of regular expressions. Only data from + `contexts` is a list of regular expression strings. Only data from :ref:`dynamic contexts ` that match one of those expressions (using :func:`re.search `) will be included in the report. @@ -955,21 +1039,36 @@ .. versionadded:: 5.2 The `precision` parameter. + .. versionadded:: 7.0 + The `format` parameter. + """ + self._prepare_data_for_reporting() with override_config( self, - ignore_errors=ignore_errors, report_omit=omit, report_include=include, - show_missing=show_missing, skip_covered=skip_covered, - report_contexts=contexts, skip_empty=skip_empty, precision=precision, - sort=sort + ignore_errors=ignore_errors, + report_omit=omit, + report_include=include, + show_missing=show_missing, + skip_covered=skip_covered, + report_contexts=contexts, + skip_empty=skip_empty, + precision=precision, + sort=sort, + format=output_format, ): reporter = SummaryReporter(self) return reporter.report(morfs, outfile=file) def annotate( - self, morfs=None, directory=None, ignore_errors=None, - omit=None, include=None, contexts=None, - ): + self, + morfs: Optional[Iterable[TMorf]] = None, + directory: Optional[str] = None, + ignore_errors: Optional[bool] = None, + omit: Optional[Union[str, List[str]]] = None, + include: Optional[Union[str, List[str]]] = None, + contexts: Optional[List[str]] = None, + ) -> None: """Annotate a list of modules. .. note:: @@ -989,19 +1088,32 @@ print("The annotate command will be removed in a future version.") print("Get in touch if you still use it: ned@nedbatchelder.com") - with override_config(self, - ignore_errors=ignore_errors, report_omit=omit, - report_include=include, report_contexts=contexts, + self._prepare_data_for_reporting() + with override_config( + self, + ignore_errors=ignore_errors, + report_omit=omit, + report_include=include, + report_contexts=contexts, ): reporter = AnnotateReporter(self) reporter.report(morfs, directory=directory) def html_report( - self, morfs=None, directory=None, ignore_errors=None, - omit=None, include=None, extra_css=None, title=None, - skip_covered=None, show_contexts=None, contexts=None, - skip_empty=None, precision=None, - ): + self, + morfs: Optional[Iterable[TMorf]] = None, + directory: Optional[str] = None, + ignore_errors: Optional[bool] = None, + omit: Optional[Union[str, List[str]]] = None, + include: Optional[Union[str, List[str]]] = None, + extra_css: Optional[str] = None, + title: Optional[str] = None, + skip_covered: Optional[bool] = None, + show_contexts: Optional[bool] = None, + contexts: Optional[List[str]] = None, + skip_empty: Optional[bool] = None, + precision: Optional[int] = None, + ) -> float: """Generate an HTML report. The HTML is written to `directory`. The file "index.html" is the @@ -1026,20 +1138,35 @@ changing the files in the report folder. """ - with override_config(self, - ignore_errors=ignore_errors, report_omit=omit, report_include=include, - html_dir=directory, extra_css=extra_css, html_title=title, - html_skip_covered=skip_covered, show_contexts=show_contexts, report_contexts=contexts, - html_skip_empty=skip_empty, precision=precision, + self._prepare_data_for_reporting() + with override_config( + self, + ignore_errors=ignore_errors, + report_omit=omit, + report_include=include, + html_dir=directory, + extra_css=extra_css, + html_title=title, + html_skip_covered=skip_covered, + show_contexts=show_contexts, + report_contexts=contexts, + html_skip_empty=skip_empty, + precision=precision, ): reporter = HtmlReporter(self) ret = reporter.report(morfs) return ret def xml_report( - self, morfs=None, outfile=None, ignore_errors=None, - omit=None, include=None, contexts=None, skip_empty=None, - ): + self, + morfs: Optional[Iterable[TMorf]] = None, + outfile: Optional[str] = None, + ignore_errors: Optional[bool] = None, + omit: Optional[Union[str, List[str]]] = None, + include: Optional[Union[str, List[str]]] = None, + contexts: Optional[List[str]] = None, + skip_empty: Optional[bool] = None, + ) -> float: """Generate an XML report of coverage results. The report is compatible with Cobertura reports. @@ -1052,22 +1179,36 @@ Returns a float, the total percentage covered. """ - with override_config(self, - ignore_errors=ignore_errors, report_omit=omit, report_include=include, - xml_output=outfile, report_contexts=contexts, skip_empty=skip_empty, + self._prepare_data_for_reporting() + with override_config( + self, + ignore_errors=ignore_errors, + report_omit=omit, + report_include=include, + xml_output=outfile, + report_contexts=contexts, + skip_empty=skip_empty, ): return render_report(self.config.xml_output, XmlReporter(self), morfs, self._message) def json_report( - self, morfs=None, outfile=None, ignore_errors=None, - omit=None, include=None, contexts=None, pretty_print=None, - show_contexts=None - ): + self, + morfs: Optional[Iterable[TMorf]] = None, + outfile: Optional[str] = None, + ignore_errors: Optional[bool] = None, + omit: Optional[Union[str, List[str]]] = None, + include: Optional[Union[str, List[str]]] = None, + contexts: Optional[List[str]] = None, + pretty_print: Optional[bool] = None, + show_contexts: Optional[bool] = None, + ) -> float: """Generate a JSON report of coverage results. Each module in `morfs` is included in the report. `outfile` is the path to write the file to, "-" will write to stdout. + `pretty_print` is a boolean, whether to pretty-print the JSON output or not. + See :meth:`report` for other arguments. Returns a float, the total percentage covered. @@ -1075,33 +1216,49 @@ .. versionadded:: 5.0 """ - with override_config(self, - ignore_errors=ignore_errors, report_omit=omit, report_include=include, - json_output=outfile, report_contexts=contexts, json_pretty_print=pretty_print, - json_show_contexts=show_contexts + self._prepare_data_for_reporting() + with override_config( + self, + ignore_errors=ignore_errors, + report_omit=omit, + report_include=include, + json_output=outfile, + report_contexts=contexts, + json_pretty_print=pretty_print, + json_show_contexts=show_contexts, ): return render_report(self.config.json_output, JsonReporter(self), morfs, self._message) def lcov_report( - self, morfs=None, outfile=None, ignore_errors=None, - omit=None, include=None, contexts=None, - ): + self, + morfs: Optional[Iterable[TMorf]] = None, + outfile: Optional[str] = None, + ignore_errors: Optional[bool] = None, + omit: Optional[Union[str, List[str]]] = None, + include: Optional[Union[str, List[str]]] = None, + contexts: Optional[List[str]] = None, + ) -> float: """Generate an LCOV report of coverage results. - Each module in 'morfs' is included in the report. 'outfile' is the + Each module in `morfs` is included in the report. `outfile` is the path to write the file to, "-" will write to stdout. - See :meth 'report' for other arguments. + See :meth:`report` for other arguments. .. versionadded:: 6.3 """ - with override_config(self, - ignore_errors=ignore_errors, report_omit=omit, report_include=include, - lcov_output=outfile, report_contexts=contexts, + self._prepare_data_for_reporting() + with override_config( + self, + ignore_errors=ignore_errors, + report_omit=omit, + report_include=include, + lcov_output=outfile, + report_contexts=contexts, ): return render_report(self.config.lcov_output, LcovReporter(self), morfs, self._message) - def sys_info(self): + def sys_info(self) -> Iterable[Tuple[str, Any]]: """Return a list of (key, value) pairs showing internal information.""" import coverage as covmod @@ -1109,7 +1266,7 @@ self._init() self._post_init() - def plugin_info(plugins): + def plugin_info(plugins: List[Any]) -> List[str]: """Make an entry for the sys_info from a list of plug-ins.""" entries = [] for plugin in plugins: @@ -1120,43 +1277,34 @@ return entries info = [ - ('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)), - ('plugins.configurers', plugin_info(self._plugins.configurers)), - ('plugins.context_switchers', plugin_info(self._plugins.context_switchers)), - ('configs_attempted', self.config.attempted_config_files), - ('configs_read', self.config.config_files_read), - ('config_file', self.config.config_file), - ('config_contents', - repr(self.config._config_contents) - if self.config._config_contents - else '-none-' + ("coverage_version", covmod.__version__), + ("coverage_module", covmod.__file__), + ("tracer", self._collector.tracer_name() if self._collector is not None else "-none-"), + ("CTracer", "available" if HAS_CTRACER else "unavailable"), + ("plugins.file_tracers", plugin_info(self._plugins.file_tracers)), + ("plugins.configurers", plugin_info(self._plugins.configurers)), + ("plugins.context_switchers", plugin_info(self._plugins.context_switchers)), + ("configs_attempted", self.config.attempted_config_files), + ("configs_read", self.config.config_files_read), + ("config_file", self.config.config_file), + ("config_contents", + repr(self.config._config_contents) if self.config._config_contents else "-none-" ), - ('data_file', self._data.data_filename() if self._data is not None else "-none-"), - ('python', sys.version.replace('\n', '')), - ('platform', platform.platform()), - ('implementation', platform.python_implementation()), - ('executable', sys.executable), - ('def_encoding', sys.getdefaultencoding()), - ('fs_encoding', sys.getfilesystemencoding()), - ('pid', os.getpid()), - ('cwd', os.getcwd()), - ('path', sys.path), - ('environment', human_sorted( - 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-']))), + ("data_file", self._data.data_filename() if self._data is not None else "-none-"), + ("python", sys.version.replace("\n", "")), + ("platform", platform.platform()), + ("implementation", platform.python_implementation()), + ("executable", sys.executable), + ("def_encoding", sys.getdefaultencoding()), + ("fs_encoding", sys.getfilesystemencoding()), + ("pid", os.getpid()), + ("cwd", os.getcwd()), + ("path", sys.path), + ("environment", [f"{k} = {v}" for k, v in relevant_environment_display(os.environ)]), + ("command_line", " ".join(getattr(sys, "argv", ["-none-"]))), ] - if self._inorout: + if self._inorout is not None: info.extend(self._inorout.sys_info()) info.extend(CoverageData.sys_info()) @@ -1169,10 +1317,13 @@ if int(os.environ.get("COVERAGE_DEBUG_CALLS", 0)): # pragma: debugging from coverage.debug import decorate_methods, show_calls - Coverage = decorate_methods(show_calls(show_args=True), butnot=['get_data'])(Coverage) + Coverage = decorate_methods( # type: ignore[misc] + show_calls(show_args=True), + butnot=["get_data"] + )(Coverage) -def process_startup(): +def process_startup() -> Optional[Coverage]: """Call this at Python start-up to perhaps measure coverage. If the environment variable COVERAGE_PROCESS_START is defined, coverage @@ -1215,7 +1366,7 @@ return None cov = Coverage(config_file=cps) - process_startup.coverage = cov + process_startup.coverage = cov # type: ignore[attr-defined] cov._warn_no_data = False cov._warn_unimported_source = False cov._warn_preimported_source = False @@ -1225,7 +1376,7 @@ return cov -def _prevent_sub_process_measurement(): +def _prevent_sub_process_measurement() -> None: """Stop any subprocess auto-measurement from writing data.""" auto_created_coverage = getattr(process_startup, "coverage", None) if auto_created_coverage is not None: diff -Nru python-coverage-6.5.0+dfsg1/coverage/data.py python-coverage-7.2.7+dfsg1/coverage/data.py --- python-coverage-6.5.0+dfsg1/coverage/data.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/data.py 2023-05-29 19:46:30.000000000 +0000 @@ -10,15 +10,21 @@ """ +from __future__ import annotations + import glob +import hashlib import os.path +from typing import Callable, Dict, Iterable, List, Optional + from coverage.exceptions import CoverageException, NoDataError -from coverage.misc import file_be_gone, human_sorted, plural +from coverage.files import PathAliases +from coverage.misc import Hasher, file_be_gone, human_sorted, plural from coverage.sqldata import CoverageData -def line_counts(data, fullpath=False): +def line_counts(data: CoverageData, fullpath: bool = False) -> Dict[str, int]: """Return a dict summarizing the line coverage data. Keys are based on the file names, and values are the number of executed @@ -29,17 +35,20 @@ """ summ = {} + filename_fn: Callable[[str], str] if fullpath: # pylint: disable=unnecessary-lambda-assignment filename_fn = lambda f: f else: filename_fn = os.path.basename for filename in data.measured_files(): - summ[filename_fn(filename)] = len(data.lines(filename)) + lines = data.lines(filename) + assert lines is not None + summ[filename_fn(filename)] = len(lines) return summ -def add_data_to_hash(data, filename, hasher): +def add_data_to_hash(data: CoverageData, filename: str, hasher: Hasher) -> None: """Contribute `filename`'s data to the `hasher`. `hasher` is a `coverage.misc.Hasher` instance to be updated with @@ -50,11 +59,11 @@ if data.has_arcs(): hasher.update(sorted(data.arcs(filename) or [])) else: - hasher.update(sorted(data.lines(filename) or [])) + hasher.update(sorted_lines(data, filename)) hasher.update(data.file_tracer(filename)) -def combinable_files(data_file, data_paths=None): +def combinable_files(data_file: str, data_paths: Optional[Iterable[str]] = None) -> List[str]: """Make a list of data files to be combined. `data_file` is a path to a data file. `data_paths` is a list of files or @@ -78,8 +87,13 @@ def combine_parallel_data( - data, aliases=None, data_paths=None, strict=False, keep=False, message=None, -): + data: CoverageData, + aliases: Optional[PathAliases] = None, + data_paths: Optional[Iterable[str]] = None, + strict: bool = False, + keep: bool = False, + message: Optional[Callable[[str], None]] = None, +) -> None: """Combine a number of data files together. `data` is a CoverageData. @@ -97,59 +111,81 @@ If `data_paths` is not provided, then the directory portion of `data.filename` is used as the directory to search for data files. - Unless `keep` is True every data file found and combined is then deleted from disk. If a file - cannot be read, a warning will be issued, and the file will not be - deleted. + Unless `keep` is True every data file found and combined is then deleted + from disk. If a file cannot be read, a warning will be issued, and the + file will not be deleted. If `strict` is true, and no files are found to combine, an error is raised. + `message` is a function to use for printing messages to the user. + """ files_to_combine = combinable_files(data.base_filename(), data_paths) if strict and not files_to_combine: raise NoDataError("No data to combine") - files_combined = 0 + file_hashes = set() + combined_any = False + for f in files_to_combine: if f == data.data_filename(): # Sometimes we are combining into a file which is one of the # parallel files. Skip that file. - if data._debug.should('dataio'): + if data._debug.should("dataio"): data._debug.write(f"Skipping combining ourself: {f!r}") continue - if data._debug.should('dataio'): - data._debug.write(f"Combining data file {f!r}") + try: - new_data = CoverageData(f, debug=data._debug) - new_data.read() - except CoverageException as exc: - if data._warn: - # The CoverageException has the file name in it, so just - # use the message as the warning. - data._warn(str(exc)) + rel_file_name = os.path.relpath(f) + except ValueError: + # ValueError can be raised under Windows when os.getcwd() returns a + # folder from a different drive than the drive of f, in which case + # we print the original value of f instead of its relative path + rel_file_name = f + + with open(f, "rb") as fobj: + hasher = hashlib.new("sha3_256") + hasher.update(fobj.read()) + sha = hasher.digest() + combine_this_one = sha not in file_hashes + + delete_this_one = not keep + if combine_this_one: + if data._debug.should("dataio"): + data._debug.write(f"Combining data file {f!r}") + file_hashes.add(sha) + try: + new_data = CoverageData(f, debug=data._debug) + new_data.read() + except CoverageException as exc: + if data._warn: + # The CoverageException has the file name in it, so just + # use the message as the warning. + data._warn(str(exc)) + if message: + message(f"Couldn't combine data file {rel_file_name}: {exc}") + delete_this_one = False + else: + data.update(new_data, aliases=aliases) + combined_any = True + if message: + message(f"Combined data file {rel_file_name}") else: - data.update(new_data, aliases=aliases) - files_combined += 1 if message: - try: - file_name = os.path.relpath(f) - except ValueError: - # ValueError can be raised under Windows when os.getcwd() returns a - # folder from a different drive than the drive of f, in which case - # we print the original value of f instead of its relative path - file_name = f - message(f"Combined data file {file_name}") - if not keep: - if data._debug.should('dataio'): - data._debug.write(f"Deleting combined data file {f!r}") - file_be_gone(f) + message(f"Skipping duplicate data {rel_file_name}") + + if delete_this_one: + if data._debug.should("dataio"): + data._debug.write(f"Deleting data file {f!r}") + file_be_gone(f) - if strict and not files_combined: + if strict and not combined_any: raise NoDataError("No usable data files") -def debug_data_file(filename): +def debug_data_file(filename: str) -> None: """Implementation of 'coverage debug data'.""" data = CoverageData(filename) filename = data.data_filename() @@ -169,3 +205,9 @@ if plugin: line += f" [{plugin}]" print(line) + + +def sorted_lines(data: CoverageData, filename: str) -> List[int]: + """Get the sorted lines for a file, for tests.""" + lines = data.lines(filename) + return sorted(lines or []) diff -Nru python-coverage-6.5.0+dfsg1/coverage/debug.py python-coverage-7.2.7+dfsg1/coverage/debug.py --- python-coverage-6.5.0+dfsg1/coverage/debug.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/debug.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Control of and utilities for debugging.""" +from __future__ import annotations + import contextlib import functools import inspect @@ -10,12 +12,19 @@ import itertools import os import pprint +import re import reprlib import sys import types import _thread -from coverage.misc import isolate_module +from typing import ( + cast, + Any, Callable, IO, Iterable, Iterator, Mapping, Optional, List, Tuple, +) + +from coverage.misc import human_sorted_items, isolate_module +from coverage.types import TWritable os = isolate_module(os) @@ -23,41 +32,47 @@ # When debugging, it can be helpful to force some options, especially when # debugging the configuration mechanisms you usually use to control debugging! # This is a list of forced debugging options. -FORCED_DEBUG = [] +FORCED_DEBUG: List[str] = [] FORCED_DEBUG_FILE = None class DebugControl: """Control and output for debugging.""" - show_repr_attr = False # For SimpleReprMixin + show_repr_attr = False # For AutoReprMixin - def __init__(self, options, output): + def __init__( + self, + options: Iterable[str], + output: Optional[IO[str]], + file_name: Optional[str] = None, + ) -> None: """Configure the options and output file for debugging.""" self.options = list(options) + FORCED_DEBUG self.suppress_callers = False filters = [] - if self.should('pid'): + if self.should("pid"): filters.append(add_pid_and_tid) self.output = DebugOutputFile.get_one( output, - show_process=self.should('process'), + file_name=file_name, + show_process=self.should("process"), filters=filters, ) self.raw_output = self.output.outfile - def __repr__(self): + def __repr__(self) -> str: return f"" - def should(self, option): + def should(self, option: str) -> bool: """Decide whether to output debug information in category `option`.""" if option == "callers" and self.suppress_callers: return False return (option in self.options) @contextlib.contextmanager - def without_callers(self): + def without_callers(self) -> Iterator[None]: """A context manager to prevent call stacks from being logged.""" old = self.suppress_callers self.suppress_callers = True @@ -66,45 +81,53 @@ finally: self.suppress_callers = old - def write(self, msg): + def write(self, msg: str) -> None: """Write a line of debug output. `msg` is the line to write. A newline will be appended. """ self.output.write(msg+"\n") - if self.should('self'): - caller_self = inspect.stack()[1][0].f_locals.get('self') + if self.should("self"): + caller_self = inspect.stack()[1][0].f_locals.get("self") if caller_self is not None: self.output.write(f"self: {caller_self!r}\n") - if self.should('callers'): + if self.should("callers"): dump_stack_frames(out=self.output, skip=1) self.output.flush() class DebugControlString(DebugControl): """A `DebugControl` that writes to a StringIO, for testing.""" - def __init__(self, options): + def __init__(self, options: Iterable[str]) -> None: super().__init__(options, io.StringIO()) - def get_output(self): + def get_output(self) -> str: """Get the output text from the `DebugControl`.""" - return self.raw_output.getvalue() + return cast(str, self.raw_output.getvalue()) # type: ignore -class NoDebugging: +class NoDebugging(DebugControl): """A replacement for DebugControl that will never try to do anything.""" - def should(self, option): # pylint: disable=unused-argument + def __init__(self) -> None: + # pylint: disable=super-init-not-called + ... + + def should(self, option: str) -> bool: """Should we write debug messages? Never.""" return False + def write(self, msg: str) -> None: + """This will never be called.""" + raise AssertionError("NoDebugging.write should never be called.") + -def info_header(label): +def info_header(label: str) -> str: """Make a nice header string.""" return "--{:-<60s}".format(" "+label+" ") -def info_formatter(info): +def info_formatter(info: Iterable[Tuple[str, Any]]) -> Iterator[str]: """Produce a sequence of formatted lines from info. `info` is a sequence of pairs (label, data). The produced lines are @@ -131,7 +154,11 @@ yield "%*s: %s" % (label_len, label, data) -def write_formatted_info(write, header, info): +def write_formatted_info( + write: Callable[[str], None], + header: str, + info: Iterable[Tuple[str, Any]], +) -> None: """Write a sequence of (label,data) pairs nicely. `write` is a function write(str) that accepts each line of output. @@ -145,7 +172,7 @@ write(f" {line}") -def short_stack(limit=None, skip=0): +def short_stack(limit: Optional[int] = None, skip: int = 0) -> str: """Return a string summarizing the call stack. The string is multi-line, with one line per stack frame. Each line shows @@ -167,21 +194,25 @@ return "\n".join("%30s : %s:%d" % (t[3], t[1], t[2]) for t in stack) -def dump_stack_frames(limit=None, out=None, skip=0): +def dump_stack_frames( + limit: Optional[int] = None, + out: Optional[TWritable] = None, + skip: int = 0 +) -> None: """Print a summary of the stack to stdout, or someplace else.""" - out = out or sys.stdout - out.write(short_stack(limit=limit, skip=skip+1)) - out.write("\n") + fout = out or sys.stdout + fout.write(short_stack(limit=limit, skip=skip+1)) + fout.write("\n") -def clipped_repr(text, numchars=50): +def clipped_repr(text: str, numchars: int = 50) -> str: """`repr(text)`, but limited to `numchars`.""" r = reprlib.Repr() r.maxstring = numchars return r.repr(text) -def short_id(id64): +def short_id(id64: int) -> int: """Given a 64-bit id, make a shorter 16-bit one.""" id16 = 0 for offset in range(0, 64, 16): @@ -189,7 +220,7 @@ return id16 & 0xFFFF -def add_pid_and_tid(text): +def add_pid_and_tid(text: str) -> str: """A filter to add pid and tid to debug messages.""" # Thread ids are useful, but too long. Make a shorter one. tid = f"{short_id(_thread.get_ident()):04x}" @@ -197,16 +228,16 @@ return text -class SimpleReprMixin: - """A mixin implementing a simple __repr__.""" - simple_repr_ignore = ['simple_repr_ignore', '$coverage.object_id'] +class AutoReprMixin: + """A mixin implementing an automatic __repr__ for debugging.""" + auto_repr_ignore = ["auto_repr_ignore", "$coverage.object_id"] - def __repr__(self): + def __repr__(self) -> str: show_attrs = ( (k, v) for k, v in self.__dict__.items() if getattr(v, "show_repr_attr", True) and not callable(v) - and k not in self.simple_repr_ignore + and k not in self.auto_repr_ignore ) return "<{klass} @0x{id:x} {attrs}>".format( klass=self.__class__.__name__, @@ -215,25 +246,25 @@ ) -def simplify(v): # pragma: debugging +def simplify(v: Any) -> Any: # pragma: debugging """Turn things which are nearly dict/list/etc into dict/list/etc.""" if isinstance(v, dict): return {k:simplify(vv) for k, vv in v.items()} elif isinstance(v, (list, tuple)): return type(v)(simplify(vv) for vv in v) elif hasattr(v, "__dict__"): - return simplify({'.'+k: v for k, v in v.__dict__.items()}) + return simplify({"."+k: v for k, v in v.__dict__.items()}) else: return v -def pp(v): # pragma: debugging +def pp(v: Any) -> None: # pragma: debugging """Debug helper to pretty-print data, including SimpleNamespace objects.""" # Might not be needed in 3.9+ pprint.pprint(simplify(v)) -def filter_text(text, filters): +def filter_text(text: str, filters: Iterable[Callable[[str], str]]) -> str: """Run `text` through a series of filters. `filters` is a list of functions. Each takes a string and returns a @@ -254,12 +285,12 @@ return text + ending -class CwdTracker: # pragma: debugging +class CwdTracker: """A class to add cwd info to debug messages.""" - def __init__(self): - self.cwd = None + def __init__(self) -> None: + self.cwd: Optional[str] = None - def filter(self, text): + def filter(self, text: str) -> str: """Add a cwd message for each new cwd.""" cwd = os.getcwd() if cwd != self.cwd: @@ -268,9 +299,14 @@ return text -class DebugOutputFile: # pragma: debugging +class DebugOutputFile: """A file-like object that includes pid and cwd information.""" - def __init__(self, outfile, show_process, filters): + def __init__( + self, + outfile: Optional[IO[str]], + show_process: bool, + filters: Iterable[Callable[[str], str]], + ): self.outfile = outfile self.show_process = show_process self.filters = list(filters) @@ -278,22 +314,26 @@ if self.show_process: self.filters.insert(0, CwdTracker().filter) 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: cmd: {!r}\n".format(getattr(sys, "argv", None))) + if hasattr(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' - SINGLETON_ATTR = 'the_one_and_is_interim' - @classmethod - def get_one(cls, fileobj=None, show_process=True, filters=(), interim=False): + def get_one( + cls, + fileobj: Optional[IO[str]] = None, + file_name: Optional[str] = None, + show_process: bool = True, + filters: Iterable[Callable[[str], str]] = (), + interim: bool = False, + ) -> DebugOutputFile: """Get a DebugOutputFile. If `fileobj` is provided, then a new DebugOutputFile is made with it. - If `fileobj` isn't provided, then a file is chosen - (COVERAGE_DEBUG_FILE, or stderr), and a process-wide singleton - DebugOutputFile is made. + If `fileobj` isn't provided, then a file is chosen (`file_name` if + provided, or COVERAGE_DEBUG_FILE, or stderr), and a process-wide + singleton DebugOutputFile is made. `show_process` controls whether the debug file adds process-level information, and filters is a list of other message filters to apply. @@ -308,38 +348,62 @@ # Make DebugOutputFile around the fileobj passed. return cls(fileobj, show_process, filters) - # Because of the way igor.py deletes and re-imports modules, - # this class can be defined more than once. But we really want - # a process-wide singleton. So stash it in sys.modules instead of - # on a class attribute. Yes, this is aggressively gross. - singleton_module = sys.modules.get(cls.SYS_MOD_NAME) - the_one, is_interim = getattr(singleton_module, cls.SINGLETON_ATTR, (None, True)) + the_one, is_interim = cls._get_singleton_data() 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 in ("stdout", "stderr"): - fileobj = getattr(sys, debug_file_name) - elif debug_file_name: - fileobj = open(debug_file_name, "a") + if file_name is not None: + fileobj = open(file_name, "a", encoding="utf-8") + else: + file_name = os.environ.get("COVERAGE_DEBUG_FILE", FORCED_DEBUG_FILE) + if file_name in ("stdout", "stderr"): + fileobj = getattr(sys, file_name) + elif file_name: + fileobj = open(file_name, "a", encoding="utf-8") else: fileobj = sys.stderr the_one = cls(fileobj, show_process, filters) - singleton_module = types.ModuleType(cls.SYS_MOD_NAME) - setattr(singleton_module, cls.SINGLETON_ATTR, (the_one, interim)) - sys.modules[cls.SYS_MOD_NAME] = singleton_module + cls._set_singleton_data(the_one, interim) return the_one - def write(self, text): + # Because of the way igor.py deletes and re-imports modules, + # this class can be defined more than once. But we really want + # a process-wide singleton. So stash it in sys.modules instead of + # on a class attribute. Yes, this is aggressively gross. + + SYS_MOD_NAME = "$coverage.debug.DebugOutputFile.the_one" + SINGLETON_ATTR = "the_one_and_is_interim" + + @classmethod + def _set_singleton_data(cls, the_one: DebugOutputFile, interim: bool) -> None: + """Set the one DebugOutputFile to rule them all.""" + singleton_module = types.ModuleType(cls.SYS_MOD_NAME) + setattr(singleton_module, cls.SINGLETON_ATTR, (the_one, interim)) + sys.modules[cls.SYS_MOD_NAME] = singleton_module + + @classmethod + def _get_singleton_data(cls) -> Tuple[Optional[DebugOutputFile], bool]: + """Get the one DebugOutputFile.""" + singleton_module = sys.modules.get(cls.SYS_MOD_NAME) + return getattr(singleton_module, cls.SINGLETON_ATTR, (None, True)) + + @classmethod + def _del_singleton_data(cls) -> None: + """Delete the one DebugOutputFile, just for tests to use.""" + if cls.SYS_MOD_NAME in sys.modules: + del sys.modules[cls.SYS_MOD_NAME] + + def write(self, text: str) -> None: """Just like file.write, but filter through all our filters.""" + assert self.outfile is not None self.outfile.write(filter_text(text, self.filters)) self.outfile.flush() - def flush(self): + def flush(self) -> None: """Flush our file.""" + assert self.outfile is not None self.outfile.flush() -def log(msg, stack=False): # pragma: debugging +def log(msg: str, stack: bool = False) -> None: # pragma: debugging """Write a log message as forcefully as possible.""" out = DebugOutputFile.get_one(interim=True) out.write(msg+"\n") @@ -347,9 +411,13 @@ dump_stack_frames(out=out, skip=1) -def decorate_methods(decorator, butnot=(), private=False): # pragma: debugging +def decorate_methods( + decorator: Callable[..., Any], + butnot: Iterable[str] = (), + private: bool = False, +) -> Callable[..., Any]: # pragma: debugging """A class decorator to apply a decorator to methods.""" - def _decorator(cls): + def _decorator(cls): # type: ignore for name, meth in inspect.getmembers(cls, inspect.isroutine): if name not in cls.__dict__: continue @@ -363,10 +431,10 @@ return _decorator -def break_in_pudb(func): # pragma: debugging +def break_in_pudb(func: Callable[..., Any]) -> Callable[..., Any]: # pragma: debugging """A function decorator to stop in the debugger for each call.""" @functools.wraps(func) - def _wrapper(*args, **kwargs): + def _wrapper(*args: Any, **kwargs: Any) -> Any: import pudb sys.stdout = sys.__stdout__ pudb.set_trace() @@ -378,11 +446,15 @@ CALLS = itertools.count() OBJ_ID_ATTR = "$coverage.object_id" -def show_calls(show_args=True, show_stack=False, show_return=False): # pragma: debugging +def show_calls( + show_args: bool = True, + show_stack: bool = False, + show_return: bool = False, +) -> Callable[..., Any]: # pragma: debugging """A method decorator to debug-log each call to the function.""" - def _decorator(func): + def _decorator(func: Callable[..., Any]) -> Callable[..., Any]: @functools.wraps(func) - def _wrapper(self, *args, **kwargs): + def _wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: oid = getattr(self, OBJ_ID_ATTR, None) if oid is None: oid = f"{os.getpid():08d} {next(OBJ_IDS):04d}" @@ -412,10 +484,41 @@ return _decorator -def _clean_stack_line(s): # pragma: debugging +def _clean_stack_line(s: str) -> str: # pragma: debugging """Simplify some paths in a stack trace, for compactness.""" s = s.strip() - s = s.replace(os.path.dirname(__file__) + '/', '') - s = s.replace(os.path.dirname(os.__file__) + '/', '') - s = s.replace(sys.prefix + '/', '') + s = s.replace(os.path.dirname(__file__) + "/", "") + s = s.replace(os.path.dirname(os.__file__) + "/", "") + s = s.replace(sys.prefix + "/", "") return s + + +def relevant_environment_display(env: Mapping[str, str]) -> List[Tuple[str, str]]: + """Filter environment variables for a debug display. + + Select variables to display (with COV or PY in the name, or HOME, TEMP, or + TMP), and also cloak sensitive values with asterisks. + + Arguments: + env: a dict of environment variable names and values. + + Returns: + A list of pairs (name, value) to show. + + """ + slugs = {"COV", "PY"} + include = {"HOME", "TEMP", "TMP"} + cloak = {"API", "TOKEN", "KEY", "SECRET", "PASS", "SIGNATURE"} + + to_show = [] + for name, val in env.items(): + keep = False + if name in include: + keep = True + elif any(slug in name for slug in slugs): + keep = True + if keep: + if any(slug in name for slug in cloak): + val = re.sub(r"\w", "*", val) + to_show.append((name, val)) + return human_sorted_items(to_show) diff -Nru python-coverage-6.5.0+dfsg1/coverage/disposition.py python-coverage-7.2.7+dfsg1/coverage/disposition.py --- python-coverage-6.5.0+dfsg1/coverage/disposition.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/disposition.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,11 +3,28 @@ """Simple value objects for tracking what to do with files.""" +from __future__ import annotations + +from typing import Optional, Type, TYPE_CHECKING + +from coverage.types import TFileDisposition + +if TYPE_CHECKING: + from coverage.plugin import FileTracer + class FileDisposition: """A simple value type for recording what to do with a file.""" - def __repr__(self): + original_filename: str + canonical_filename: str + source_filename: Optional[str] + trace: bool + reason: str + file_tracer: Optional[FileTracer] + has_dynamic_filename: bool + + def __repr__(self) -> str: return f"" @@ -15,7 +32,7 @@ # be implemented in either C or Python. Acting on them is done with these # functions. -def disposition_init(cls, original_filename): +def disposition_init(cls: Type[TFileDisposition], original_filename: str) -> TFileDisposition: """Construct and initialize a new FileDisposition object.""" disp = cls() disp.original_filename = original_filename @@ -28,7 +45,7 @@ return disp -def disposition_debug_msg(disp): +def disposition_debug_msg(disp: TFileDisposition) -> str: """Make a nice debug message of what the FileDisposition is doing.""" if disp.trace: msg = f"Tracing {disp.original_filename!r}" diff -Nru python-coverage-6.5.0+dfsg1/coverage/env.py python-coverage-7.2.7+dfsg1/coverage/env.py --- python-coverage-6.5.0+dfsg1/coverage/env.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/env.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,10 +3,21 @@ """Determine facts about the environment.""" +from __future__ import annotations + import os import platform import sys +from typing import Any, Iterable, Tuple + +# debug_info() at the bottom wants to show all the globals, but not imports. +# Grab the global names here to know which names to not show. Nothing defined +# above this line will be in the output. +_UNINTERESTING_GLOBALS = list(globals()) +# These names also shouldn't be shown. +_UNINTERESTING_GLOBALS += ["PYBEHAVIOR", "debug_info"] + # Operating systems. WINDOWS = sys.platform == "win32" LINUX = sys.platform.startswith("linux") @@ -15,15 +26,13 @@ # Python implementations. CPYTHON = (platform.python_implementation() == "CPython") PYPY = (platform.python_implementation() == "PyPy") -JYTHON = (platform.python_implementation() == "Jython") -IRONPYTHON = (platform.python_implementation() == "IronPython") # 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] == "+"),) if PYPY: - PYPYVERSION = sys.pypy_version_info + PYPYVERSION = sys.pypy_version_info # type: ignore[attr-defined] # Python behavior. class PYBEHAVIOR: @@ -31,13 +40,10 @@ # 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)) + pep626 = (PYVERSION > (3, 10, 0, "alpha", 4)) # Is "if __debug__" optimized away? - if PYPY: - optimize_if_debug = True - else: - optimize_if_debug = not pep626 + optimize_if_debug = not pep626 # Is "if not __debug__" optimized away? The exact details have changed # across versions. @@ -51,7 +57,7 @@ else: optimize_if_not_debug = 1 else: - if PYVERSION >= (3, 8, 0, 'beta', 1): + if PYVERSION >= (3, 8, 0, "beta", 1): optimize_if_not_debug = 2 else: optimize_if_not_debug = 1 @@ -60,18 +66,23 @@ negative_lnotab = not (PYPY and PYPYVERSION < (7, 2)) # 3.7 changed how functions with only docstrings are numbered. - docstring_only_function = (not PYPY) and ((3, 7, 0, 'beta', 5) <= PYVERSION <= (3, 10)) + docstring_only_function = (not PYPY) and ((3, 7, 0, "beta", 5) <= PYVERSION <= (3, 10)) # When a break/continue/return statement in a try block jumps to a finally # block, does the finally block do the break/continue/return (pre-3.8), or # does the finally jump back to the break/continue/return (3.8) to do the # work? finally_jumps_back = ((3, 8) <= PYVERSION < (3, 10)) + if PYPY and PYPYVERSION < (7, 3, 7): + finally_jumps_back = False # When a function is decorated, does the trace function get called for the # @-line and also the def-line (new behavior in 3.8)? Or just the @-line # (old behavior)? - trace_decorated_def = (CPYTHON and PYVERSION >= (3, 8)) or (PYPY and PYVERSION >= (3, 9)) + trace_decorated_def = ( + (PYVERSION >= (3, 8)) and + (CPYTHON or (PYVERSION > (3, 8)) or (PYPYVERSION > (7, 3, 9))) + ) # Functions are no longer claimed to start at their earliest decorator even though # the decorators are traced? @@ -79,21 +90,30 @@ # CPython 3.11 now jumps to the decorator line again while executing # the decorator. - trace_decorator_line_again = (CPYTHON and PYVERSION > (3, 11, 0, 'alpha', 3, 0)) + trace_decorator_line_again = (CPYTHON and PYVERSION > (3, 11, 0, "alpha", 3, 0)) # Are while-true loops optimized into absolute jumps with no loop setup? nix_while_true = (PYVERSION >= (3, 8)) # CPython 3.9a1 made sys.argv[0] and other reported files absolute paths. - report_absolute_files = ((CPYTHON or (PYPYVERSION >= (7, 3, 10))) and PYVERSION >= (3, 9)) + report_absolute_files = ( + (CPYTHON or (PYPY and PYPYVERSION >= (7, 3, 10))) + and PYVERSION >= (3, 9) + ) # Lines after break/continue/return/raise are no longer compiled into the # bytecode. They used to be marked as missing, now they aren't executable. - omit_after_jump = pep626 + omit_after_jump = ( + pep626 + or (PYPY and PYVERSION >= (3, 9) and PYPYVERSION >= (7, 3, 12)) + ) # PyPy has always omitted statements after return. omit_after_return = omit_after_jump or PYPY + # Optimize away unreachable try-else clauses. + optimize_unreachable_try_else = pep626 + # Modules used to have firstlineno equal to the line number of the first # real line of code. Now they always start at 1. module_firstline_1 = pep626 @@ -102,7 +122,7 @@ 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')) + exit_through_with = (PYVERSION >= (3, 10, 0, "beta")) # Match-case construct. match_case = (PYVERSION >= (3, 10)) @@ -112,37 +132,31 @@ # Modules start with a line numbered zero. This means empty modules have # only a 0-number line, which is ignored, giving a truly empty module. - empty_is_empty = (PYVERSION >= (3, 11, 0, 'beta', 4)) + empty_is_empty = (PYVERSION >= (3, 11, 0, "beta", 4)) + + # Are comprehensions inlined (new) or compiled as called functions (old)? + # Changed in https://github.com/python/cpython/pull/101441 + comprehensions_are_functions = (PYVERSION <= (3, 12, 0, "alpha", 7, 0)) # Coverage.py specifics. # Are we using the C-implemented trace function? -C_TRACER = os.getenv('COVERAGE_TEST_TRACER', 'c') == 'c' +C_TRACER = os.getenv("COVERAGE_TEST_TRACER", "c") == "c" # Are we coverage-measuring ourselves? -METACOV = os.getenv('COVERAGE_COVERAGE', '') != '' +METACOV = os.getenv("COVERAGE_COVERAGE", "") != "" # Are we running our test suite? # Even when running tests, you can use COVERAGE_TESTING=0 to disable the -# test-specific behavior like contracts. -TESTING = os.getenv('COVERAGE_TESTING', '') == 'True' +# test-specific behavior like AST checking. +TESTING = os.getenv("COVERAGE_TESTING", "") == "True" -# Environment COVERAGE_NO_CONTRACTS=1 can turn off contracts while debugging -# tests to remove noise from stack traces. -# $set_env.py: COVERAGE_NO_CONTRACTS - Disable PyContracts to simplify stack traces. -USE_CONTRACTS = ( - TESTING - and not bool(int(os.environ.get("COVERAGE_NO_CONTRACTS", 0))) - and (PYVERSION < (3, 11)) -) -def debug_info(): +def debug_info() -> Iterable[Tuple[str, Any]]: """Return a list of (name, value) pairs for printing debug information.""" info = [ (name, value) for name, value in globals().items() - if not name.startswith("_") and - name not in {"PYBEHAVIOR", "debug_info"} and - not isinstance(value, type(os)) + if not name.startswith("_") and name not in _UNINTERESTING_GLOBALS ] info += [ (name, value) for name, value in PYBEHAVIOR.__dict__.items() diff -Nru python-coverage-6.5.0+dfsg1/coverage/exceptions.py python-coverage-7.2.7+dfsg1/coverage/exceptions.py --- python-coverage-6.5.0+dfsg1/coverage/exceptions.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/exceptions.py 2023-05-29 19:46:30.000000000 +0000 @@ -57,16 +57,6 @@ 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 -Nru python-coverage-6.5.0+dfsg1/coverage/execfile.py python-coverage-7.2.7+dfsg1/coverage/execfile.py --- python-coverage-6.5.0+dfsg1/coverage/execfile.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/execfile.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Execute files of Python code.""" +from __future__ import annotations + import importlib.machinery import importlib.util import inspect @@ -10,13 +12,15 @@ import os import struct import sys -import types + +from importlib.machinery import ModuleSpec +from types import CodeType, ModuleType +from typing import Any, List, Optional, Tuple from coverage import env from coverage.exceptions import CoverageException, _ExceptionDuringRun, NoCode, NoSource from coverage.files import canonical_filename, python_reported_file from coverage.misc import isolate_module -from coverage.phystokens import compile_unicode from coverage.python import get_python_source os = isolate_module(os) @@ -29,11 +33,13 @@ Currently only implements the .fullname attribute """ - def __init__(self, fullname, *_args): + def __init__(self, fullname: str, *_args: Any) -> None: self.fullname = fullname -def find_module(modulename): +def find_module( + modulename: str, +) -> Tuple[Optional[str], str, ModuleSpec]: """Find the module named `modulename`. Returns the file path of the module, the name of the enclosing @@ -67,18 +73,23 @@ This is meant to emulate real Python execution as closely as possible. """ - def __init__(self, args, as_module=False): + def __init__(self, args: List[str], as_module: bool = False) -> None: self.args = args self.as_module = as_module self.arg0 = args[0] - self.package = self.modulename = self.pathname = self.loader = self.spec = None + self.package: Optional[str] = None + self.modulename: Optional[str] = None + self.pathname: Optional[str] = None + self.loader: Optional[DummyLoader] = None + self.spec: Optional[ModuleSpec] = None - def prepare(self): + def prepare(self) -> None: """Set sys.path properly. This needs to happen before any importing, and without importing anything. """ + path0: Optional[str] if self.as_module: path0 = os.getcwd() elif os.path.isdir(self.arg0): @@ -112,7 +123,7 @@ if path0 is not None: sys.path[0] = python_reported_file(path0) - def _prepare2(self): + def _prepare2(self) -> None: """Do more preparation to run Python code. Includes finding the module to run and adjusting sys.argv[0]. @@ -125,6 +136,7 @@ if self.spec is not None: self.modulename = self.spec.name self.loader = DummyLoader(self.modulename) + assert pathname is not None self.pathname = os.path.abspath(pathname) self.args[0] = self.arg0 = self.pathname elif os.path.isdir(self.arg0): @@ -154,13 +166,13 @@ self.arg0 = python_reported_file(self.arg0) - def run(self): + def run(self) -> None: """Run the Python code!""" self._prepare2() # Create a module to serve as __main__ - main_mod = types.ModuleType('__main__') + main_mod = ModuleType("__main__") from_pyc = self.arg0.endswith((".pyc", ".pyo")) main_mod.__file__ = self.arg0 @@ -168,13 +180,13 @@ main_mod.__file__ = main_mod.__file__[:-1] if self.package is not None: main_mod.__package__ = self.package - main_mod.__loader__ = self.loader + main_mod.__loader__ = self.loader # type: ignore[assignment] if self.spec is not None: main_mod.__spec__ = self.spec - main_mod.__builtins__ = sys.modules['builtins'] + main_mod.__builtins__ = sys.modules["builtins"] # type: ignore[attr-defined] - sys.modules['__main__'] = main_mod + sys.modules["__main__"] = main_mod # Set sys.argv properly. sys.argv = self.args @@ -208,15 +220,19 @@ # so that the coverage.py code doesn't appear in the final printed # traceback. typ, err, tb = sys.exc_info() + assert typ is not None + assert err is not None + assert tb is not None # PyPy3 weirdness. If I don't access __context__, then somehow it # is non-None when the exception is reported at the upper layer, # and a nested exception is shown to the user. This getattr fixes # it somehow? https://bitbucket.org/pypy/pypy/issue/1903 - getattr(err, '__context__', None) + getattr(err, "__context__", None) # Call the excepthook. try: + assert err.__traceback__ is not None err.__traceback__ = err.__traceback__.tb_next sys.excepthook(typ, err, tb.tb_next) except SystemExit: # pylint: disable=try-except-raise @@ -226,7 +242,11 @@ # shenanigans is kind of involved. sys.stderr.write("Error in sys.excepthook:\n") typ2, err2, tb2 = sys.exc_info() + assert typ2 is not None + assert err2 is not None + assert tb2 is not None err2.__suppress_context__ = True + assert err2.__traceback__ is not None err2.__traceback__ = err2.__traceback__.tb_next sys.__excepthook__(typ2, err2, tb2.tb_next) sys.stderr.write("\nOriginal exception was:\n") @@ -237,7 +257,7 @@ os.chdir(cwd) -def run_python_module(args): +def run_python_module(args: List[str]) -> None: """Run a Python module, as though with ``python -m name args...``. `args` is the argument array to present as sys.argv, including the first @@ -251,7 +271,7 @@ runner.run() -def run_python_file(args): +def run_python_file(args: List[str]) -> None: """Run a Python file as if it were the main program on the command line. `args` is the argument array to present as sys.argv, including the first @@ -266,7 +286,7 @@ runner.run() -def make_code_from_py(filename): +def make_code_from_py(filename: str) -> CodeType: """Get source from `filename` and make a code object of it.""" # Open the source file. try: @@ -274,11 +294,10 @@ except (OSError, NoSource) as exc: raise NoSource(f"No file to run: '{filename}'") from exc - code = compile_unicode(source, filename, "exec") - return code + return compile(source, filename, "exec", dont_inherit=True) -def make_code_from_pyc(filename): +def make_code_from_pyc(filename: str) -> CodeType: """Get a code object from a .pyc file.""" try: fpyc = open(filename, "rb") @@ -292,7 +311,7 @@ if magic != PYC_MAGIC_NUMBER: raise NoCode(f"Bad magic number in .pyc file: {magic!r} != {PYC_MAGIC_NUMBER!r}") - flags = struct.unpack(' None: """Set the directory that `relative_filename` will be relative to.""" global RELATIVE_DIR, CANONICAL_FILENAME_CACHE @@ -38,13 +44,12 @@ CANONICAL_FILENAME_CACHE = {} -def relative_directory(): +def relative_directory() -> str: """Return the directory that `relative_filename` is relative to.""" return RELATIVE_DIR -@contract(returns='unicode') -def relative_filename(filename): +def relative_filename(filename: str) -> str: """Return the relative form of `filename`. The file name will be relative to the current directory when the @@ -57,8 +62,7 @@ return filename -@contract(returns='unicode') -def canonical_filename(filename): +def canonical_filename(filename: str) -> str: """Return a canonical file name for `filename`. An absolute path with no redundant components and normalized case. @@ -69,7 +73,7 @@ if not os.path.isabs(filename): for path in [os.curdir] + sys.path: if path is None: - continue + continue # type: ignore f = os.path.join(path, filename) try: exists = os.path.exists(f) @@ -85,8 +89,7 @@ MAX_FLAT = 100 -@contract(filename='unicode', returns='unicode') -def flat_rootname(filename): +def flat_rootname(filename: str) -> str: """A base for a flat file name to correspond to this file. Useful for writing files about the code where you want all the files in @@ -107,10 +110,10 @@ if env.WINDOWS: - _ACTUAL_PATH_CACHE = {} - _ACTUAL_PATH_LIST_CACHE = {} + _ACTUAL_PATH_CACHE: Dict[str, str] = {} + _ACTUAL_PATH_LIST_CACHE: Dict[str, List[str]] = {} - def actual_path(path): + def actual_path(path: str) -> str: """Get the actual path of `path`, including the correct case.""" if path in _ACTUAL_PATH_CACHE: return _ACTUAL_PATH_CACHE[path] @@ -143,36 +146,59 @@ return actpath else: - def actual_path(path): + def actual_path(path: str) -> str: """The actual path for non-Windows platforms.""" return path -@contract(returns='unicode') -def abs_file(path): +def abs_file(path: str) -> str: """Return the absolute normalized form of `path`.""" return actual_path(os.path.abspath(os.path.realpath(path))) -def python_reported_file(filename): +def zip_location(filename: str) -> Optional[Tuple[str, str]]: + """Split a filename into a zipfile / inner name pair. + + Only return a pair if the zipfile exists. No check is made if the inner + name is in the zipfile. + + """ + for ext in [".zip", ".whl", ".egg", ".pex"]: + zipbase, extension, inner = filename.partition(ext + sep(filename)) + if extension: + zipfile = zipbase + ext + if os.path.exists(zipfile): + return zipfile, inner + return None + + +def source_exists(path: str) -> bool: + """Determine if a source file path exists.""" + if os.path.exists(path): + return True + + if zip_location(path): + # If zip_location returns anything, then it's a zipfile that + # exists. That's good enough for us. + return True + + return False + + +def python_reported_file(filename: str) -> str: """Return the string as Python would describe this file name.""" if env.PYBEHAVIOR.report_absolute_files: filename = os.path.abspath(filename) return filename -RELATIVE_DIR = None -CANONICAL_FILENAME_CACHE = None -set_relative_directory() - - -def isabs_anywhere(filename): +def isabs_anywhere(filename: str) -> bool: """Is `filename` an absolute path on any OS?""" return ntpath.isabs(filename) or posixpath.isabs(filename) -def prep_patterns(patterns): - """Prepare the file patterns for use in a `FnmatchMatcher`. +def prep_patterns(patterns: Iterable[str]) -> List[str]: + """Prepare the file patterns for use in a `GlobMatcher`. If a pattern starts with a wildcard, it is used as a pattern as-is. If it does not start with a wildcard, then it is made @@ -183,9 +209,8 @@ """ prepped = [] for p in patterns or []: - if p.startswith(("*", "?")): - prepped.append(p) - else: + prepped.append(p) + if not p.startswith(("*", "?")): prepped.append(abs_file(p)) return prepped @@ -198,19 +223,20 @@ somewhere in a subtree rooted at one of the directories. """ - def __init__(self, paths, name="unknown"): - self.original_paths = human_sorted(paths) - self.paths = list(map(os.path.normcase, paths)) + def __init__(self, paths: Iterable[str], name: str = "unknown") -> None: + self.original_paths: List[str] = human_sorted(paths) + #self.paths = list(map(os.path.normcase, paths)) + self.paths = [os.path.normcase(p) for p in paths] self.name = name - def __repr__(self): + def __repr__(self) -> str: return f"" - def info(self): + def info(self) -> List[str]: """A list of strings for displaying when dumping state.""" return self.original_paths - def match(self, fpath): + def match(self, fpath: str) -> bool: """Does `fpath` indicate a file in one of our trees?""" fpath = os.path.normcase(fpath) for p in self.paths: @@ -226,18 +252,18 @@ class ModuleMatcher: """A matcher for modules in a tree.""" - def __init__(self, module_names, name="unknown"): + def __init__(self, module_names: Iterable[str], name:str = "unknown") -> None: self.modules = list(module_names) self.name = name - def __repr__(self): + def __repr__(self) -> str: return f"" - def info(self): + def info(self) -> List[str]: """A list of strings for displaying when dumping state.""" return self.modules - def match(self, module_name): + def match(self, module_name: str) -> bool: """Does `module_name` indicate a module in one of our packages?""" if not module_name: return False @@ -246,33 +272,33 @@ if module_name.startswith(m): if module_name == m: return True - if module_name[len(m)] == '.': + if module_name[len(m)] == ".": # This is a module in the package return True return False -class FnmatchMatcher: +class GlobMatcher: """A matcher for files by file name pattern.""" - def __init__(self, pats, name="unknown"): + def __init__(self, pats: Iterable[str], name: str = "unknown") -> None: self.pats = list(pats) - self.re = fnmatches_to_regex(self.pats, case_insensitive=env.WINDOWS) + self.re = globs_to_regex(self.pats, case_insensitive=env.WINDOWS) self.name = name - def __repr__(self): - return f"" + def __repr__(self) -> str: + return f"" - def info(self): + def info(self) -> List[str]: """A list of strings for displaying when dumping state.""" return self.pats - def match(self, fpath): + def match(self, fpath: str) -> bool: """Does `fpath` match one of our file name patterns?""" return self.re.match(fpath) is not None -def sep(s): +def sep(s: str) -> str: """Find the path separator used in this string, or os.sep if none.""" sep_match = re.search(r"[\\/]", s) if sep_match: @@ -282,12 +308,59 @@ return the_sep -def fnmatches_to_regex(patterns, case_insensitive=False, partial=False): - """Convert fnmatch patterns to a compiled regex that matches any of them. +# Tokenizer for _glob_to_regex. +# None as a sub means disallowed. +G2RX_TOKENS = [(re.compile(rx), sub) for rx, sub in [ + (r"\*\*\*+", None), # Can't have *** + (r"[^/]+\*\*+", None), # Can't have x** + (r"\*\*+[^/]+", None), # Can't have **x + (r"\*\*/\*\*", None), # Can't have **/** + (r"^\*+/", r"(.*[/\\\\])?"), # ^*/ matches any prefix-slash, or nothing. + (r"/\*+$", r"[/\\\\].*"), # /*$ matches any slash-suffix. + (r"\*\*/", r"(.*[/\\\\])?"), # **/ matches any subdirs, including none + (r"/", r"[/\\\\]"), # / matches either slash or backslash + (r"\*", r"[^/\\\\]*"), # * matches any number of non slash-likes + (r"\?", r"[^/\\\\]"), # ? matches one non slash-like + (r"\[.*?\]", r"\g<0>"), # [a-f] matches [a-f] + (r"[a-zA-Z0-9_-]+", r"\g<0>"), # word chars match themselves + (r"[\[\]]", None), # Can't have single square brackets + (r".", r"\\\g<0>"), # Anything else is escaped to be safe +]] + +def _glob_to_regex(pattern: str) -> str: + """Convert a file-path glob pattern into a regex.""" + # Turn all backslashes into slashes to simplify the tokenizer. + pattern = pattern.replace("\\", "/") + if "/" not in pattern: + pattern = "**/" + pattern + path_rx = [] + pos = 0 + while pos < len(pattern): + for rx, sub in G2RX_TOKENS: # pragma: always breaks + m = rx.match(pattern, pos=pos) + if m: + if sub is None: + raise ConfigError(f"File pattern can't include {m[0]!r}") + path_rx.append(m.expand(sub)) + pos = m.end() + break + return "".join(path_rx) + + +def globs_to_regex( + patterns: Iterable[str], + case_insensitive: bool = False, + partial: bool = False, +) -> re.Pattern[str]: + """Convert glob patterns to a compiled regex that matches any of them. Slashes are always converted to match either slash or backslash, for Windows support, even when running elsewhere. + If the pattern has no slash or backslash, then it is interpreted as + matching a file name anywhere it appears in the tree. Otherwise, the glob + pattern must match the whole file path. + If `partial` is true, then the pattern will match if the target string starts with the pattern. Otherwise, it must match the entire string. @@ -295,23 +368,13 @@ strings. """ - regexes = (fnmatch.translate(pattern) for pattern in patterns) - # Python3.7 fnmatch translates "/" as "/". Before that, it translates as "\/", - # so we have to deal with maybe a backslash. - regexes = (re.sub(r"\\?/", r"[\\\\/]", regex) for regex in regexes) - - if partial: - # fnmatch always adds a \Z to match the whole string, which we don't - # want, so we remove the \Z. While removing it, we only replace \Z if - # followed by paren (introducing flags), or at end, to keep from - # destroying a literal \Z in the pattern. - regexes = (re.sub(r'\\Z(\(\?|$)', r'\1', regex) for regex in regexes) - flags = 0 if case_insensitive: flags |= re.IGNORECASE - compiled = re.compile(join_regex(regexes), flags=flags) - + rx = join_regex(map(_glob_to_regex, patterns)) + if not partial: + rx = rf"(?:{rx})\Z" + compiled = re.compile(rx, flags=flags) return compiled @@ -326,22 +389,27 @@ map a path through those aliases to produce a unified path. """ - def __init__(self, debugfn=None, relative=False): - self.aliases = [] # A list of (original_pattern, regex, result) + def __init__( + self, + debugfn: Optional[Callable[[str], None]] = None, + relative: bool = False, + ) -> None: + # A list of (original_pattern, regex, result) + self.aliases: List[Tuple[str, re.Pattern[str], str]] = [] self.debugfn = debugfn or (lambda msg: 0) self.relative = relative self.pprinted = False - def pprint(self): + def pprint(self) -> None: """Dump the important parts of the PathAliases, for debugging.""" self.debugfn(f"Aliases (relative={self.relative}):") for original_pattern, regex, result in self.aliases: self.debugfn(f" Rule: {original_pattern!r} -> {result!r} using regex {regex.pattern!r}") - def add(self, pattern, result): + def add(self, pattern: str, result: str) -> None: """Add the `pattern`/`result` pair to the list of aliases. - `pattern` is an `fnmatch`-style pattern. `result` is a simple + `pattern` is an `glob`-style pattern. `result` is a simple string. When mapping paths, if a path starts with a match against `pattern`, then that match is replaced with `result`. This models isomorphic source trees being rooted at different places on two @@ -361,22 +429,23 @@ if pattern.endswith("*"): raise ConfigError("Pattern must not end with wildcards.") - # The pattern is meant to match a filepath. Let's make it absolute + # The pattern is meant to match a file path. Let's make it absolute # unless it already is, or is meant to match any prefix. - if not pattern.startswith('*') and not isabs_anywhere(pattern + pattern_sep): - pattern = abs_file(pattern) + if not self.relative: + if not pattern.startswith("*") and not isabs_anywhere(pattern + pattern_sep): + pattern = abs_file(pattern) if not pattern.endswith(pattern_sep): pattern += pattern_sep # Make a regex from the pattern. - regex = fnmatches_to_regex([pattern], case_insensitive=True, partial=True) + regex = globs_to_regex([pattern], case_insensitive=True, partial=True) # Normalize the result: it must end with a path separator. result_sep = sep(result) result = result.rstrip(r"\/") + result_sep self.aliases.append((original_pattern, regex, result)) - def map(self, path): + def map(self, path: str, exists:Callable[[str], bool] = source_exists) -> str: """Map `path` through the aliases. `path` is checked against all of the patterns. The first pattern to @@ -387,6 +456,9 @@ The separator style in the result is made to match that of the result in the alias. + `exists` is a function to determine if the resulting path actually + exists. + Returns the mapped path. If a mapping has happened, this is a canonical path. If no mapping has happened, it is the original value of `path` unchanged. @@ -403,16 +475,44 @@ new = new.replace(sep(path), sep(result)) if not self.relative: new = canonical_filename(new) + dot_start = result.startswith(("./", ".\\")) and len(result) > 2 + if new.startswith(("./", ".\\")) and not dot_start: + new = new[2:] + if not exists(new): + self.debugfn( + f"Rule {original_pattern!r} changed {path!r} to {new!r} " + + "which doesn't exist, continuing" + ) + continue self.debugfn( f"Matched path {path!r} to rule {original_pattern!r} -> {result!r}, " + f"producing {new!r}" ) return new + + # If we get here, no pattern matched. + + if self.relative and not isabs_anywhere(path): + # Auto-generate a pattern to implicitly match relative files + parts = re.split(r"[/\\]", path) + if len(parts) > 1: + dir1 = parts[0] + pattern = f"*/{dir1}" + regex_pat = rf"^(.*[\\/])?{re.escape(dir1)}[\\/]" + result = f"{dir1}{os.sep}" + # Only add a new pattern if we don't already have this pattern. + if not any(p == pattern for p, _, _ in self.aliases): + self.debugfn( + f"Generating rule: {pattern!r} -> {result!r} using regex {regex_pat!r}" + ) + self.aliases.append((pattern, re.compile(regex_pat), result)) + return self.map(path, exists=exists) + self.debugfn(f"No rules match, path {path!r} is unchanged") return path -def find_python_files(dirname): +def find_python_files(dirname: str, include_namespace_packages: bool) -> Iterable[str]: """Yield all of the importable Python files in `dirname`, recursively. To be importable, the files have to be in a directory with a __init__.py, @@ -421,16 +521,27 @@ best, but sub-directories are checked for a __init__.py to be sure we only find the importable files. + If `include_namespace_packages` is True, then the check for __init__.py + files is skipped. + + Files with strange characters are skipped, since they couldn't have been + imported, and are probably editor side-files. + """ for i, (dirpath, dirnames, filenames) in enumerate(os.walk(dirname)): - if i > 0 and '__init__.py' not in filenames: - # If a directory doesn't have __init__.py, then it isn't - # importable and neither are its files - del dirnames[:] - continue + if not include_namespace_packages: + if i > 0 and "__init__.py" not in filenames: + # If a directory doesn't have __init__.py, then it isn't + # importable and neither are its files + del dirnames[:] + continue for filename in filenames: # We're only interested in files that look like reasonable Python # files: Must end with .py or .pyw, and must not have certain funny # characters that probably mean they are editor junk. if re.match(r"^[^.#~!$@%^&*()+=,]+\.pyw?$", filename): yield os.path.join(dirpath, filename) + + +# Globally set the relative directory. +set_relative_directory() diff -Nru python-coverage-6.5.0+dfsg1/coverage/fullcoverage/encodings.py python-coverage-7.2.7+dfsg1/coverage/fullcoverage/encodings.py --- python-coverage-6.5.0+dfsg1/coverage/fullcoverage/encodings.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/fullcoverage/encodings.py 2023-05-29 19:46:30.000000000 +0000 @@ -14,6 +14,9 @@ a problem with coverage.py - that it starts too late to trace the coverage of many of the most fundamental modules in the Standard Library. +DO NOT import other modules into here, it will interfere with the goal of this +code executing before all imports. This is why this file isn't type-checked. + """ import sys diff -Nru python-coverage-6.5.0+dfsg1/coverage/htmlfiles/coverage_html.js python-coverage-7.2.7+dfsg1/coverage/htmlfiles/coverage_html.js --- python-coverage-6.5.0+dfsg1/coverage/htmlfiles/coverage_html.js 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/htmlfiles/coverage_html.js 2023-05-29 19:46:30.000000000 +0000 @@ -214,7 +214,7 @@ coverage.pyfile_ready = function () { // If we're directed to a particular line number, highlight the line. var frag = location.hash; - if (frag.length > 2 && frag[1] === 't') { + if (frag.length > 2 && frag[1] === "t") { document.querySelector(frag).closest(".n").classList.add("highlight"); coverage.set_sel(parseInt(frag.substr(2), 10)); } else { @@ -257,6 +257,10 @@ coverage.init_scroll_markers(); coverage.wire_up_sticky_header(); + document.querySelectorAll("[id^=ctxs]").forEach( + cbox => cbox.addEventListener("click", coverage.expand_contexts) + ); + // Rebuild scroll markers when the window height changes. window.addEventListener("resize", coverage.build_scroll_markers); }; @@ -528,14 +532,14 @@ coverage.init_scroll_markers = function () { // Init some variables - coverage.lines_len = document.querySelectorAll('#source > p').length; + coverage.lines_len = document.querySelectorAll("#source > p").length; // Build html coverage.build_scroll_markers(); }; coverage.build_scroll_markers = function () { - const temp_scroll_marker = document.getElementById('scroll_marker') + const temp_scroll_marker = document.getElementById("scroll_marker") if (temp_scroll_marker) temp_scroll_marker.remove(); // Don't build markers if the window has no scroll bar. if (document.body.scrollHeight <= window.innerHeight) { @@ -549,8 +553,8 @@ const scroll_marker = document.createElement("div"); scroll_marker.id = "scroll_marker"; - document.getElementById('source').querySelectorAll( - 'p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par' + document.getElementById("source").querySelectorAll( + "p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par" ).forEach(element => { const line_top = Math.floor(element.offsetTop * marker_scale); const line_number = parseInt(element.querySelector(".n a").id.substr(1)); @@ -577,24 +581,40 @@ }; coverage.wire_up_sticky_header = function () { - const header = document.querySelector('header'); + const header = document.querySelector("header"); const header_bottom = ( - header.querySelector('.content h2').getBoundingClientRect().top - + header.querySelector(".content h2").getBoundingClientRect().top - header.getBoundingClientRect().top ); function updateHeader() { if (window.scrollY > header_bottom) { - header.classList.add('sticky'); + header.classList.add("sticky"); } else { - header.classList.remove('sticky'); + header.classList.remove("sticky"); } } - window.addEventListener('scroll', updateHeader); + window.addEventListener("scroll", updateHeader); updateHeader(); }; +coverage.expand_contexts = function (e) { + var ctxs = e.target.parentNode.querySelector(".ctxs"); + + if (!ctxs.classList.contains("expanded")) { + var ctxs_text = ctxs.textContent; + var width = Number(ctxs_text[0]); + ctxs.textContent = ""; + for (var i = 1; i < ctxs_text.length; i += width) { + key = ctxs_text.substring(i, i + width).trim(); + ctxs.appendChild(document.createTextNode(contexts[key])); + ctxs.appendChild(document.createElement("br")); + } + ctxs.classList.add("expanded"); + } +}; + document.addEventListener("DOMContentLoaded", () => { if (document.body.classList.contains("indexfile")) { coverage.index_ready(); diff -Nru python-coverage-6.5.0+dfsg1/coverage/htmlfiles/pyfile.html python-coverage-7.2.7+dfsg1/coverage/htmlfiles/pyfile.html --- python-coverage-6.5.0+dfsg1/coverage/htmlfiles/pyfile.html 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/htmlfiles/pyfile.html 2023-05-29 19:46:30.000000000 +0000 @@ -11,6 +11,13 @@ {% if extra_css %} {% endif %} + + {% if contexts_json %} + + {% endif %} + @@ -117,12 +124,8 @@ {% endif %} {# Things that should appear below the line. #} - {% if line.context_list %} - - {% for context in line.context_list %} - {{context}} - {% endfor %} - + {% if line.context_str %} + {{ line.context_str }} {% endif %}

{% endjoined %} diff -Nru python-coverage-6.5.0+dfsg1/coverage/htmlfiles/style.css python-coverage-7.2.7+dfsg1/coverage/htmlfiles/style.css --- python-coverage-6.5.0+dfsg1/coverage/htmlfiles/style.css 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/htmlfiles/style.css 2023-05-29 19:46:30.000000000 +0000 @@ -258,12 +258,10 @@ @media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } -#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; } +#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; text-align: right; } @media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } -#source p .ctxs span { display: block; text-align: right; } - #index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } #index table.index { margin-left: -.5em; } diff -Nru python-coverage-6.5.0+dfsg1/coverage/htmlfiles/style.scss python-coverage-7.2.7+dfsg1/coverage/htmlfiles/style.scss --- python-coverage-6.5.0+dfsg1/coverage/htmlfiles/style.scss 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/htmlfiles/style.scss 2023-05-29 19:46:30.000000000 +0000 @@ -622,10 +622,7 @@ @include background-dark($dark-context-bg-color); border-radius: .25em; margin-right: 1.75em; - span { - display: block; - text-align: right; - } + text-align: right; } } } diff -Nru python-coverage-6.5.0+dfsg1/coverage/html.py python-coverage-7.2.7+dfsg1/coverage/html.py --- python-coverage-6.5.0+dfsg1/coverage/html.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/html.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,27 +3,57 @@ """HTML reporting for coverage.py.""" +from __future__ import annotations + +import collections import datetime +import functools import json import os import re import shutil -import types +import string # pylint: disable=deprecated-module + +from dataclasses import dataclass +from typing import Any, Dict, Iterable, List, Optional, Tuple, TYPE_CHECKING, cast import coverage -from coverage.data import add_data_to_hash +from coverage.data import CoverageData, add_data_to_hash from coverage.exceptions import NoDataError from coverage.files import flat_rootname from coverage.misc import ensure_dir, file_be_gone, Hasher, isolate_module, format_local_datetime -from coverage.misc import human_sorted, plural -from coverage.report import get_analysis_to_report -from coverage.results import Numbers +from coverage.misc import human_sorted, plural, stdout_link +from coverage.report_core import get_analysis_to_report +from coverage.results import Analysis, Numbers from coverage.templite import Templite +from coverage.types import TLineNo, TMorf +from coverage.version import __url__ + + +if TYPE_CHECKING: + # To avoid circular imports: + from coverage import Coverage + from coverage.plugins import FileReporter + + # To be able to use 3.8 typing features, and still run on 3.7: + from typing import TypedDict + + class IndexInfoDict(TypedDict): + """Information for each file, to render the index file.""" + nums: Numbers + html_filename: str + relative_filename: str + + class FileInfoDict(TypedDict): + """Summary of the information from last rendering, to avoid duplicate work.""" + hash: str + index: IndexInfoDict + os = isolate_module(os) -def data_filename(fname): +def data_filename(fname: str) -> str: """Return the path to an "htmlfiles" data file of ours. """ static_dir = os.path.join(os.path.dirname(__file__), "htmlfiles") @@ -31,17 +61,44 @@ return static_filename -def read_data(fname): +def read_data(fname: str) -> str: """Return the contents of a data file of ours.""" with open(data_filename(fname)) as data_file: return data_file.read() -def write_html(fname, html): +def write_html(fname: str, html: str) -> None: """Write `html` to `fname`, properly encoded.""" html = re.sub(r"(\A\s+)|(\s+$)", "", html, flags=re.MULTILINE) + "\n" with open(fname, "wb") as fout: - fout.write(html.encode('ascii', 'xmlcharrefreplace')) + fout.write(html.encode("ascii", "xmlcharrefreplace")) + + +@dataclass +class LineData: + """The data for each source line of HTML output.""" + tokens: List[Tuple[str, str]] + number: TLineNo + category: str + statement: bool + contexts: List[str] + contexts_label: str + context_list: List[str] + short_annotations: List[str] + long_annotations: List[str] + html: str = "" + context_str: Optional[str] = None + annotate: Optional[str] = None + annotate_long: Optional[str] = None + css_class: str = "" + + +@dataclass +class FileData: + """The data for each source file of HTML output.""" + relative_filename: str + nums: Numbers + lines: List[LineData] class HtmlDataGeneration: @@ -49,7 +106,7 @@ EMPTY = "(empty)" - def __init__(self, cov): + def __init__(self, cov: Coverage) -> None: self.coverage = cov self.config = self.coverage.config data = self.coverage.get_data() @@ -59,7 +116,7 @@ self.coverage._warn("No contexts were measured") data.set_query_contexts(self.config.report_contexts) - def data_for_file(self, fr, analysis): + def data_for_file(self, fr: FileReporter, analysis: Analysis) -> FileData: """Produce the data needed for one file's report.""" if self.has_arcs: missing_branch_arcs = analysis.missing_branch_arcs() @@ -72,27 +129,28 @@ for lineno, tokens in enumerate(fr.source_token_lines(), start=1): # Figure out how to mark this line. - category = None + category = "" short_annotations = [] long_annotations = [] if lineno in analysis.excluded: - category = 'exc' + category = "exc" elif lineno in analysis.missing: - category = 'mis' + category = "mis" elif self.has_arcs and lineno in missing_branch_arcs: - category = 'par' + category = "par" for b in missing_branch_arcs[lineno]: if b < 0: short_annotations.append("exit") else: - short_annotations.append(b) + short_annotations.append(str(b)) long_annotations.append(fr.missing_arc_description(lineno, b, arcs_executed)) elif lineno in analysis.statements: - category = 'run' + category = "run" - contexts = contexts_label = None - context_list = None + contexts = [] + contexts_label = "" + context_list = [] if category and self.config.show_contexts: contexts = human_sorted(c or self.EMPTY for c in contexts_by_lineno.get(lineno, ())) if contexts == [self.EMPTY]: @@ -101,7 +159,7 @@ contexts_label = f"{len(contexts)} ctx" context_list = contexts - lines.append(types.SimpleNamespace( + lines.append(LineData( tokens=tokens, number=lineno, category=category, @@ -113,7 +171,7 @@ long_annotations=long_annotations, )) - file_data = types.SimpleNamespace( + file_data = FileData( relative_filename=fr.relative_filename(), nums=analysis.numbers, lines=lines, @@ -124,13 +182,28 @@ class FileToReport: """A file we're considering reporting.""" - def __init__(self, fr, analysis): + def __init__(self, fr: FileReporter, analysis: Analysis) -> None: self.fr = fr self.analysis = analysis self.rootname = flat_rootname(fr.relative_filename()) self.html_filename = self.rootname + ".html" +HTML_SAFE = string.ascii_letters + string.digits + "!#$%'()*+,-./:;=?@[]^_`{|}~" + +@functools.lru_cache(maxsize=None) +def encode_int(n: int) -> str: + """Create a short HTML-safe string from an integer, using HTML_SAFE.""" + if n == 0: + return HTML_SAFE[0] + + r = [] + while n: + n, t = divmod(n, len(HTML_SAFE)) + r.append(HTML_SAFE[t]) + return "".join(r) + + class HtmlReporter: """HTML reporting.""" @@ -144,7 +217,7 @@ "favicon_32.png", ] - def __init__(self, cov): + def __init__(self, cov: Coverage) -> None: self.coverage = cov self.config = self.coverage.config self.directory = self.config.html_dir @@ -160,6 +233,7 @@ title = self.config.html_title + self.extra_css: Optional[str] if self.config.extra_css: self.extra_css = os.path.basename(self.config.extra_css) else: @@ -168,8 +242,8 @@ self.data = self.coverage.get_data() self.has_arcs = self.data.has_arcs() - self.file_summaries = [] - self.all_files_nums = [] + self.file_summaries: List[IndexInfoDict] = [] + self.all_files_nums: List[Numbers] = [] self.incr = IncrementalChecker(self.directory) self.datagen = HtmlDataGeneration(self.coverage) self.totals = Numbers(precision=self.config.precision) @@ -179,32 +253,32 @@ self.template_globals = { # Functions available in the templates. - 'escape': escape, - 'pair': pair, - 'len': len, + "escape": escape, + "pair": pair, + "len": len, # Constants for this report. - '__url__': coverage.__url__, - '__version__': coverage.__version__, - 'title': title, - 'time_stamp': format_local_datetime(datetime.datetime.now()), - 'extra_css': self.extra_css, - 'has_arcs': self.has_arcs, - 'show_contexts': self.config.show_contexts, + "__url__": __url__, + "__version__": coverage.__version__, + "title": title, + "time_stamp": format_local_datetime(datetime.datetime.now()), + "extra_css": self.extra_css, + "has_arcs": self.has_arcs, + "show_contexts": self.config.show_contexts, # Constants for all reports. # These css classes determine which lines are highlighted by default. - 'category': { - 'exc': 'exc show_exc', - 'mis': 'mis show_mis', - 'par': 'par run show_par', - 'run': 'run', + "category": { + "exc": "exc show_exc", + "mis": "mis show_mis", + "par": "par run show_par", + "run": "run", }, } self.pyfile_html_source = read_data("pyfile.html") self.source_tmpl = Templite(self.pyfile_html_source, self.template_globals) - def report(self, morfs): + def report(self, morfs: Optional[Iterable[TMorf]]) -> float: """Generate an HTML report for `morfs`. `morfs` is a list of modules or file names. @@ -241,7 +315,7 @@ if not self.all_files_nums: raise NoDataError("No data to report.") - self.totals = sum(self.all_files_nums) + self.totals = cast(Numbers, sum(self.all_files_nums)) # Write the index file. if files_to_report: @@ -254,13 +328,13 @@ self.make_local_static_report_files() return self.totals.n_statements and self.totals.pc_covered - def make_directory(self): + def make_directory(self) -> None: """Make sure our htmlcov directory exists.""" ensure_dir(self.directory) if not os.listdir(self.directory): self.directory_was_empty = True - def make_local_static_report_files(self): + def make_local_static_report_files(self) -> None: """Make local instances of static files for HTML report.""" # The files we provide must always be copied. for static in self.STATIC_FILES: @@ -275,9 +349,10 @@ # The user may have extra CSS they want copied. if self.extra_css: + assert self.config.extra_css is not None shutil.copyfile(self.config.extra_css, os.path.join(self.directory, self.extra_css)) - def should_report_file(self, ftr): + def should_report_file(self, ftr: FileToReport) -> bool: """Determine if we'll report this file.""" # Get the numbers for this file. nums = ftr.analysis.numbers @@ -300,7 +375,7 @@ return True - def write_html_file(self, ftr, prev_html, next_html): + def write_html_file(self, ftr: FileToReport, prev_html: str, next_html: str) -> None: """Generate an HTML file for one source file.""" self.make_directory() @@ -311,18 +386,38 @@ # Write the HTML page for this file. file_data = self.datagen.data_for_file(ftr.fr, ftr.analysis) + + contexts = collections.Counter(c for cline in file_data.lines for c in cline.contexts) + context_codes = {y: i for (i, y) in enumerate(x[0] for x in contexts.most_common())} + if context_codes: + contexts_json = json.dumps( + {encode_int(v): k for (k, v) in context_codes.items()}, + indent=2, + ) + else: + contexts_json = None + for ldata in file_data.lines: # Build the HTML for the line. - html = [] + html_parts = [] for tok_type, tok_text in ldata.tokens: if tok_type == "ws": - html.append(escape(tok_text)) + html_parts.append(escape(tok_text)) else: - tok_html = escape(tok_text) or ' ' - html.append( - f'{tok_html}' - ) - ldata.html = ''.join(html) + tok_html = escape(tok_text) or " " + html_parts.append(f'{tok_html}') + ldata.html = "".join(html_parts) + if ldata.context_list: + encoded_contexts = [ + encode_int(context_codes[c_context]) for c_context in ldata.context_list + ] + code_width = max(len(ec) for ec in encoded_contexts) + ldata.context_str = ( + str(code_width) + + "".join(ec.ljust(code_width) for ec in encoded_contexts) + ) + else: + ldata.context_str = "" if ldata.short_annotations: # 202F is NARROW NO-BREAK SPACE. @@ -351,27 +446,30 @@ css_classes = [] if ldata.category: - css_classes.append(self.template_globals['category'][ldata.category]) - ldata.css_class = ' '.join(css_classes) or "pln" + css_classes.append( + self.template_globals["category"][ldata.category] # type: ignore[index] + ) + ldata.css_class = " ".join(css_classes) or "pln" html_path = os.path.join(self.directory, ftr.html_filename) html = self.source_tmpl.render({ **file_data.__dict__, - 'prev_html': prev_html, - 'next_html': next_html, + "contexts_json": contexts_json, + "prev_html": prev_html, + "next_html": next_html, }) write_html(html_path, html) # Save this file's information for the index file. - index_info = { - 'nums': ftr.analysis.numbers, - 'html_filename': ftr.html_filename, - 'relative_filename': ftr.fr.relative_filename(), + index_info: IndexInfoDict = { + "nums": ftr.analysis.numbers, + "html_filename": ftr.html_filename, + "relative_filename": ftr.fr.relative_filename(), } self.file_summaries.append(index_info) self.incr.set_index_info(ftr.rootname, index_info) - def index_file(self, first_html, final_html): + def index_file(self, first_html: str, final_html: str) -> None: """Write the index.html file for this report.""" self.make_directory() index_tmpl = Templite(read_data("index.html"), self.template_globals) @@ -385,17 +483,19 @@ skipped_empty_msg = f"{n} empty file{plural(n)} skipped." html = index_tmpl.render({ - 'files': self.file_summaries, - 'totals': self.totals, - 'skipped_covered_msg': skipped_covered_msg, - 'skipped_empty_msg': skipped_empty_msg, - 'first_html': first_html, - 'final_html': final_html, + "files": self.file_summaries, + "totals": self.totals, + "skipped_covered_msg": skipped_covered_msg, + "skipped_empty_msg": skipped_empty_msg, + "first_html": first_html, + "final_html": final_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}") + + print_href = stdout_link(index_file, f"file://{os.path.abspath(index_file)}") + self.coverage._message(f"Wrote HTML report to {print_href}") # Write the latest hashes for next time. self.incr.write() @@ -407,7 +507,6 @@ STATUS_FILE = "status.json" STATUS_FORMAT = 2 - # pylint: disable=wrong-spelling-in-comment,useless-suppression # The data looks like: # # { @@ -435,16 +534,16 @@ # } # } - def __init__(self, directory): + def __init__(self, directory: str) -> None: self.directory = directory self.reset() - def reset(self): + def reset(self) -> None: """Initialize to empty. Causes all files to be reported.""" - self.globals = '' - self.files = {} + self.globals = "" + self.files: Dict[str, FileInfoDict] = {} - def read(self): + def read(self) -> None: """Read the information we stored last time.""" usable = False try: @@ -455,38 +554,39 @@ usable = False else: usable = True - if status['format'] != self.STATUS_FORMAT: + if status["format"] != self.STATUS_FORMAT: usable = False - elif status['version'] != coverage.__version__: + elif status["version"] != coverage.__version__: usable = False if usable: self.files = {} - for filename, fileinfo in status['files'].items(): - fileinfo['index']['nums'] = Numbers(*fileinfo['index']['nums']) + for filename, fileinfo in status["files"].items(): + fileinfo["index"]["nums"] = Numbers(*fileinfo["index"]["nums"]) self.files[filename] = fileinfo - self.globals = status['globals'] + self.globals = status["globals"] else: self.reset() - def write(self): + def write(self) -> None: """Write the current status.""" status_file = os.path.join(self.directory, self.STATUS_FILE) files = {} for filename, fileinfo in self.files.items(): - fileinfo['index']['nums'] = fileinfo['index']['nums'].init_args() + index = fileinfo["index"] + index["nums"] = index["nums"].init_args() # type: ignore[typeddict-item] files[filename] = fileinfo status = { - 'format': self.STATUS_FORMAT, - 'version': coverage.__version__, - 'globals': self.globals, - 'files': files, + "format": self.STATUS_FORMAT, + "version": coverage.__version__, + "globals": self.globals, + "files": files, } with open(status_file, "w") as fout: - json.dump(status, fout, separators=(',', ':')) + json.dump(status, fout, separators=(",", ":")) - def check_global_data(self, *data): + def check_global_data(self, *data: Any) -> None: """Check the global data that can affect incremental reporting.""" m = Hasher() for d in data: @@ -496,14 +596,14 @@ self.reset() self.globals = these_globals - def can_skip_file(self, data, fr, rootname): + def can_skip_file(self, data: CoverageData, fr: FileReporter, rootname: str) -> bool: """Can we skip reporting this file? `data` is a CoverageData object, `fr` is a `FileReporter`, and `rootname` is the name being used for the file. """ m = Hasher() - m.update(fr.source().encode('utf-8')) + m.update(fr.source().encode("utf-8")) add_data_to_hash(data, fr.filename, m) this_hash = m.hexdigest() @@ -516,26 +616,26 @@ self.set_file_hash(rootname, this_hash) return False - def file_hash(self, fname): + def file_hash(self, fname: str) -> str: """Get the hash of `fname`'s contents.""" - return self.files.get(fname, {}).get('hash', '') + return self.files.get(fname, {}).get("hash", "") # type: ignore[call-overload] - def set_file_hash(self, fname, val): + def set_file_hash(self, fname: str, val: str) -> None: """Set the hash of `fname`'s contents.""" - self.files.setdefault(fname, {})['hash'] = val + self.files.setdefault(fname, {})["hash"] = val # type: ignore[typeddict-item] - def index_info(self, fname): + def index_info(self, fname: str) -> IndexInfoDict: """Get the information for index.html for `fname`.""" - return self.files.get(fname, {}).get('index', {}) + return self.files.get(fname, {}).get("index", {}) # type: ignore - def set_index_info(self, fname, info): + def set_index_info(self, fname: str, info: IndexInfoDict) -> None: """Set the information for index.html for `fname`.""" - self.files.setdefault(fname, {})['index'] = info + self.files.setdefault(fname, {})["index"] = info # type: ignore[typeddict-item] # Helpers for templates and generating HTML -def escape(t): +def escape(t: str) -> str: """HTML-escape the text in `t`. This is only suitable for HTML text, not attributes. @@ -545,6 +645,6 @@ return t.replace("&", "&").replace("<", "<") -def pair(ratio): +def pair(ratio: Tuple[int, int]) -> str: """Format a pair of numbers so JavaScript can read them in an attribute.""" return "%s %s" % ratio diff -Nru python-coverage-6.5.0+dfsg1/coverage/__init__.py python-coverage-7.2.7+dfsg1/coverage/__init__.py --- python-coverage-6.5.0+dfsg1/coverage/__init__.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/__init__.py 2023-05-29 19:46:30.000000000 +0000 @@ -1,22 +1,35 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt -"""Code coverage measurement for Python. +""" +Code coverage measurement for Python. Ned Batchelder -https://nedbatchelder.com/code/coverage +https://coverage.readthedocs.io """ -import sys - -from coverage.version import __version__, __url__, version_info - -from coverage.control import Coverage, process_startup -from coverage.data import CoverageData -from coverage.exceptions import CoverageException -from coverage.plugin import CoveragePlugin, FileTracer, FileReporter -from coverage.pytracer import PyTracer +# mypy's convention is that "import as" names are public from the module. +# We import names as themselves to indicate that. Pylint sees it as pointless, +# so disable its warning. +# pylint: disable=useless-import-alias + +from coverage.version import ( + __version__ as __version__, + version_info as version_info, +) + +from coverage.control import ( + Coverage as Coverage, + process_startup as process_startup, +) +from coverage.data import CoverageData as CoverageData +from coverage.exceptions import CoverageException as CoverageException +from coverage.plugin import ( + CoveragePlugin as CoveragePlugin, + FileReporter as FileReporter, + FileTracer as FileTracer, +) # Backward compatibility. coverage = Coverage @@ -25,12 +38,3 @@ # the encodings.utf_8 module is loaded and then unloaded, I don't know why. # Adding a reference here prevents it from being unloaded. Yuk. import encodings.utf_8 # pylint: disable=wrong-import-position, wrong-import-order - -# Because of the "from coverage.control import fooey" lines at the top of the -# file, there's an entry for coverage.coverage in sys.modules, mapped to None. -# This makes some inspection tools (like pydoc) unable to find the class -# coverage.coverage. So remove that entry. -try: - del sys.modules['coverage.coverage'] -except KeyError: - pass diff -Nru python-coverage-6.5.0+dfsg1/coverage/inorout.py python-coverage-7.2.7+dfsg1/coverage/inorout.py --- python-coverage-6.5.0+dfsg1/coverage/inorout.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/inorout.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Determining whether files are being measured/reported or not.""" +from __future__ import annotations + import importlib.util import inspect import itertools @@ -13,33 +15,48 @@ import sysconfig import traceback +from types import FrameType, ModuleType +from typing import ( + cast, Any, Iterable, List, Optional, Set, Tuple, Type, TYPE_CHECKING, +) + from coverage import env from coverage.disposition import FileDisposition, disposition_init from coverage.exceptions import CoverageException, PluginError -from coverage.files import TreeMatcher, FnmatchMatcher, ModuleMatcher +from coverage.files import TreeMatcher, GlobMatcher, ModuleMatcher from coverage.files import prep_patterns, find_python_files, canonical_filename from coverage.misc import sys_modules_saved from coverage.python import source_for_file, source_for_morf +from coverage.types import TFileDisposition, TMorf, TWarnFn, TDebugCtl + +if TYPE_CHECKING: + from coverage.config import CoverageConfig + from coverage.plugin_support import Plugins # Pypy has some unusual stuff in the "stdlib". Consider those locations # when deciding where the stdlib is. These modules are not used for anything, # they are modules importable from the pypy lib directories, so that we can # find those directories. -_structseq = _pypy_irc_topic = None +modules_we_happen_to_have: List[ModuleType] = [ + inspect, itertools, os, platform, re, sysconfig, traceback, +] + if env.PYPY: try: import _structseq + modules_we_happen_to_have.append(_structseq) except ImportError: pass try: import _pypy_irc_topic + modules_we_happen_to_have.append(_pypy_irc_topic) except ImportError: pass -def canonical_path(morf, directory=False): +def canonical_path(morf: TMorf, directory: bool = False) -> str: """Return the canonical path of the module or file `morf`. If the module is a package, then return its directory. If it is a @@ -53,7 +70,7 @@ return morf_path -def name_for_module(filename, frame): +def name_for_module(filename: str, frame: Optional[FrameType]) -> str: """Get the name of the module for a filename and frame. For configurability's sake, we allow __main__ modules to be matched by @@ -66,24 +83,20 @@ """ module_globals = frame.f_globals if frame is not None else {} - if module_globals is None: # pragma: only ironpython - # IronPython doesn't provide globals: https://github.com/IronLanguages/main/issues/1296 - module_globals = {} + dunder_name: str = module_globals.get("__name__", None) - dunder_name = module_globals.get('__name__', None) - - if isinstance(dunder_name, str) and dunder_name != '__main__': + if isinstance(dunder_name, str) and dunder_name != "__main__": # This is the usual case: an imported module. return dunder_name - loader = module_globals.get('__loader__', None) - for attrname in ('fullname', 'name'): # attribute renamed in py3.2 + loader = module_globals.get("__loader__", None) + for attrname in ("fullname", "name"): # attribute renamed in py3.2 if hasattr(loader, attrname): fullname = getattr(loader, attrname) else: continue - if isinstance(fullname, str) and fullname != '__main__': + if isinstance(fullname, str) and fullname != "__main__": # Module loaded via: runpy -m return fullname @@ -95,20 +108,20 @@ return dunder_name -def module_is_namespace(mod): +def module_is_namespace(mod: ModuleType) -> bool: """Is the module object `mod` a PEP420 namespace module?""" - return hasattr(mod, '__path__') and getattr(mod, '__file__', None) is None + return hasattr(mod, "__path__") and getattr(mod, "__file__", None) is None -def module_has_file(mod): +def module_has_file(mod: ModuleType) -> bool: """Does the module object `mod` have an existing __file__ ?""" - mod__file__ = getattr(mod, '__file__', None) + mod__file__ = getattr(mod, "__file__", None) if mod__file__ is None: return False return os.path.exists(mod__file__) -def file_and_path_for_module(modulename): +def file_and_path_for_module(modulename: str) -> Tuple[Optional[str], List[str]]: """Find the file and search path for `modulename`. Returns: @@ -129,32 +142,19 @@ return filename, path -def add_stdlib_paths(paths): +def add_stdlib_paths(paths: Set[str]) -> None: """Add paths where the stdlib can be found to the set `paths`.""" # Look at where some standard modules are located. That's the # indication for "installed with the interpreter". In some # environments (virtualenv, for example), these modules may be # spread across a few locations. Look at all the candidate modules # we've imported, and take all the different ones. - modules_we_happen_to_have = [ - inspect, itertools, os, platform, re, sysconfig, traceback, - _pypy_irc_topic, _structseq, - ] for m in modules_we_happen_to_have: - if m is not None and hasattr(m, "__file__"): + if hasattr(m, "__file__"): paths.add(canonical_path(m, directory=True)) - if _structseq and not hasattr(_structseq, '__file__'): - # PyPy 2.4 has no __file__ in the builtin modules, but the code - # objects still have the file names. So dig into one to find - # the path to exclude. The "filename" might be synthetic, - # don't be fooled by those. - structseq_file = _structseq.structseq_new.__code__.co_filename - if not structseq_file.startswith("<"): - paths.add(canonical_path(structseq_file)) - -def add_third_party_paths(paths): +def add_third_party_paths(paths: Set[str]) -> None: """Add locations for third-party packages to the set `paths`.""" # Get the paths that sysconfig knows about. scheme_names = set(sysconfig.get_scheme_names()) @@ -168,7 +168,7 @@ paths.add(config_paths[path_name]) -def add_coverage_paths(paths): +def add_coverage_paths(paths: Set[str]) -> None: """Add paths where coverage.py code can be found to the set `paths`.""" cover_path = canonical_path(__file__, directory=True) paths.add(cover_path) @@ -176,43 +176,23 @@ # Don't include our own test code. paths.add(os.path.join(cover_path, "tests")) - # When testing, we use PyContracts, which should be considered - # part of coverage.py, and it uses six. Exclude those directories - # just as we exclude ourselves. - if env.USE_CONTRACTS: - import contracts - import six - for mod in [contracts, six]: - paths.add(canonical_path(mod)) - class InOrOut: """Machinery for determining what files to measure.""" - def __init__(self, warn, debug): + def __init__( + self, + config: CoverageConfig, + warn: TWarnFn, + debug: Optional[TDebugCtl], + include_namespace_packages: bool, + ) -> None: self.warn = warn self.debug = debug + self.include_namespace_packages = include_namespace_packages - # The matchers for should_trace. - self.source_match = None - self.source_pkgs_match = None - self.pylib_paths = self.cover_paths = self.third_paths = None - self.pylib_match = self.cover_match = self.third_match = None - self.include_match = self.omit_match = None - self.plugins = [] - self.disp_class = FileDisposition - - # The source argument can be directories or package names. - self.source = [] - self.source_pkgs = [] - self.source_pkgs_unmatched = [] - self.omit = self.include = None - - # Is the source inside a third-party area? - self.source_in_third = False - - def configure(self, config): - """Apply the configuration to get ready for decision-time.""" + self.source: List[str] = [] + self.source_pkgs: List[str] = [] self.source_pkgs.extend(config.source_pkgs) for src in config.source or []: if os.path.isdir(src): @@ -221,31 +201,38 @@ self.source_pkgs.append(src) self.source_pkgs_unmatched = self.source_pkgs[:] - self.omit = prep_patterns(config.run_omit) self.include = prep_patterns(config.run_include) + self.omit = prep_patterns(config.run_omit) # The directories for files considered "installed with the interpreter". - self.pylib_paths = set() + self.pylib_paths: Set[str] = set() if not config.cover_pylib: add_stdlib_paths(self.pylib_paths) # To avoid tracing the coverage.py code itself, we skip anything # located where we are. - self.cover_paths = set() + self.cover_paths: Set[str] = set() add_coverage_paths(self.cover_paths) # Find where third-party packages are installed. - self.third_paths = set() + self.third_paths: Set[str] = set() add_third_party_paths(self.third_paths) - def debug(msg): + def _debug(msg: str) -> None: if self.debug: self.debug.write(msg) + # The matchers for should_trace. + # Generally useful information - debug("sys.path:" + "".join(f"\n {p}" for p in sys.path)) + _debug("sys.path:" + "".join(f"\n {p}" for p in sys.path)) # Create the matchers we need for should_trace + self.source_match = None + self.source_pkgs_match = None + self.pylib_match = None + self.include_match = self.omit_match = None + if self.source or self.source_pkgs: against = [] if self.source: @@ -254,55 +241,61 @@ if self.source_pkgs: self.source_pkgs_match = ModuleMatcher(self.source_pkgs, "source_pkgs") against.append(f"modules {self.source_pkgs_match!r}") - debug("Source matching against " + " and ".join(against)) + _debug("Source matching against " + " and ".join(against)) else: if self.pylib_paths: self.pylib_match = TreeMatcher(self.pylib_paths, "pylib") - debug(f"Python stdlib matching: {self.pylib_match!r}") + _debug(f"Python stdlib matching: {self.pylib_match!r}") if self.include: - self.include_match = FnmatchMatcher(self.include, "include") - debug(f"Include matching: {self.include_match!r}") + self.include_match = GlobMatcher(self.include, "include") + _debug(f"Include matching: {self.include_match!r}") if self.omit: - self.omit_match = FnmatchMatcher(self.omit, "omit") - debug(f"Omit matching: {self.omit_match!r}") + self.omit_match = GlobMatcher(self.omit, "omit") + _debug(f"Omit matching: {self.omit_match!r}") self.cover_match = TreeMatcher(self.cover_paths, "coverage") - debug(f"Coverage code matching: {self.cover_match!r}") + _debug(f"Coverage code matching: {self.cover_match!r}") self.third_match = TreeMatcher(self.third_paths, "third") - debug(f"Third-party lib matching: {self.third_match!r}") + _debug(f"Third-party lib matching: {self.third_match!r}") # Check if the source we want to measure has been installed as a # third-party package. + # Is the source inside a third-party area? + self.source_in_third_paths = set() with sys_modules_saved(): for pkg in self.source_pkgs: try: modfile, path = file_and_path_for_module(pkg) - debug(f"Imported source package {pkg!r} as {modfile!r}") + _debug(f"Imported source package {pkg!r} as {modfile!r}") except CoverageException as exc: - debug(f"Couldn't import source package {pkg!r}: {exc}") + _debug(f"Couldn't import source package {pkg!r}: {exc}") continue if modfile: if self.third_match.match(modfile): - debug( - f"Source is in third-party because of source_pkg {pkg!r} at {modfile!r}" + _debug( + f"Source in third-party: source_pkg {pkg!r} at {modfile!r}" ) - self.source_in_third = True + self.source_in_third_paths.add(canonical_path(source_for_file(modfile))) else: for pathdir in path: if self.third_match.match(pathdir): - debug( - f"Source is in third-party because of {pkg!r} path directory " + - f"at {pathdir!r}" + _debug( + f"Source in third-party: {pkg!r} path directory at {pathdir!r}" ) - self.source_in_third = True + self.source_in_third_paths.add(pathdir) for src in self.source: if self.third_match.match(src): - debug(f"Source is in third-party because of source directory {src!r}") - self.source_in_third = True + _debug(f"Source in third-party: source directory {src!r}") + self.source_in_third_paths.add(src) + self.source_in_third_match = TreeMatcher(self.source_in_third_paths, "source_in_third") + _debug(f"Source in third-party matching: {self.source_in_third_match}") + + self.plugins: Plugins + self.disp_class: Type[TFileDisposition] = FileDisposition - def should_trace(self, filename, frame=None): + def should_trace(self, filename: str, frame: Optional[FrameType] = None) -> TFileDisposition: """Decide whether to trace execution in `filename`, with a reason. This function is called from the trace function. As each new file name @@ -314,14 +307,14 @@ original_filename = filename disp = disposition_init(self.disp_class, filename) - def nope(disp, reason): + def nope(disp: TFileDisposition, reason: str) -> TFileDisposition: """Simple helper to make it easy to return NO.""" disp.trace = False disp.reason = reason return disp - if original_filename.startswith('<'): - return nope(disp, "not a real original file name") + if original_filename.startswith("<"): + return nope(disp, "original file name is not real") if frame is not None: # Compiled Python files have two file names: frame.f_code.co_filename is @@ -330,10 +323,10 @@ # .pyc files can be moved after compilation (for example, by being # installed), we look for __file__ in the frame and prefer it to the # co_filename value. - dunder_file = frame.f_globals and frame.f_globals.get('__file__') + dunder_file = frame.f_globals and frame.f_globals.get("__file__") if dunder_file: filename = source_for_file(dunder_file) - if original_filename and not original_filename.startswith('<'): + if original_filename and not original_filename.startswith("<"): orig = os.path.basename(original_filename) if orig != os.path.basename(filename): # Files shouldn't be renamed when moved. This happens when @@ -345,19 +338,15 @@ # Empty string is pretty useless. return nope(disp, "empty string isn't a file name") - if filename.startswith('memory:'): + if filename.startswith("memory:"): return nope(disp, "memory isn't traceable") - if filename.startswith('<'): + if filename.startswith("<"): # Lots of non-file execution is represented with artificial # file names like "", "", or # "". Don't ever trace these executions, since we # can't do anything with the data later anyway. - return nope(disp, "not a real file name") - - # Jython reports the .class file to the tracer, use the source file. - if filename.endswith("$py.class"): - filename = filename[:-9] + ".py" + return nope(disp, "file name is not real") canonical = canonical_filename(filename) disp.canonical_filename = canonical @@ -403,7 +392,7 @@ return disp - def check_include_omit_etc(self, filename, frame): + def check_include_omit_etc(self, filename: str, frame: Optional[FrameType]) -> Optional[str]: """Check a file name against the include, omit, etc, rules. Returns a string or None. String means, don't trace, and is the reason @@ -431,9 +420,8 @@ ok = True if not ok: return extra + "falls outside the --source spec" - if not self.source_in_third: - if self.third_match.match(filename): - return "inside --source, but is third-party" + if self.third_match.match(filename) and not self.source_in_third_match.match(filename): + return "inside --source, but is third-party" elif self.include_match: if not self.include_match.match(filename): return "falls outside the --include trees" @@ -465,13 +453,13 @@ # No reason found to skip this file. return None - def warn_conflicting_settings(self): + def warn_conflicting_settings(self) -> None: """Warn if there are settings that conflict.""" if self.include: if self.source or self.source_pkgs: self.warn("--include is ignored because --source is set", slug="include-ignored") - def warn_already_imported_files(self): + def warn_already_imported_files(self) -> None: """Warn if files have already been imported that we will be measuring.""" if self.include or self.source or self.source_pkgs: warned = set() @@ -496,19 +484,19 @@ msg = f"Already imported a file that will be measured: {filename}" self.warn(msg, slug="already-imported") warned.add(filename) - elif self.debug and self.debug.should('trace'): + elif self.debug and self.debug.should("trace"): self.debug.write( "Didn't trace already imported file {!r}: {}".format( disp.original_filename, disp.reason ) ) - def warn_unimported_source(self): + def warn_unimported_source(self) -> None: """Warn about source packages that were of interest, but never traced.""" for pkg in self.source_pkgs_unmatched: self._warn_about_unmeasured_code(pkg) - def _warn_about_unmeasured_code(self, pkg): + def _warn_about_unmeasured_code(self, pkg: str) -> None: """Warn about a package or module that we never traced. `pkg` is a string, the name of the package or module. @@ -534,28 +522,28 @@ msg = f"Module {pkg} was previously imported, but not measured" self.warn(msg, slug="module-not-measured") - def find_possibly_unexecuted_files(self): + def find_possibly_unexecuted_files(self) -> Iterable[Tuple[str, Optional[str]]]: """Find files in the areas of interest that might be untraced. Yields pairs: file path, and responsible plug-in name. """ for pkg in self.source_pkgs: - if (not pkg in sys.modules or + if (pkg not in sys.modules or not module_has_file(sys.modules[pkg])): continue - pkg_file = source_for_file(sys.modules[pkg].__file__) + pkg_file = source_for_file(cast(str, sys.modules[pkg].__file__)) yield from self._find_executable_files(canonical_path(pkg_file)) for src in self.source: yield from self._find_executable_files(src) - def _find_plugin_files(self, src_dir): + def _find_plugin_files(self, src_dir: str) -> Iterable[Tuple[str, str]]: """Get executable files from the plugins.""" for plugin in self.plugins.file_tracers: for x_file in plugin.find_executable_files(src_dir): yield x_file, plugin._coverage_plugin_name - def _find_executable_files(self, src_dir): + def _find_executable_files(self, src_dir: str) -> Iterable[Tuple[str, Optional[str]]]: """Find executable files in `src_dir`. Search for files in `src_dir` that can be executed because they @@ -565,18 +553,21 @@ Yield the file path, and the plugin name that handles the file. """ - py_files = ((py_file, None) for py_file in find_python_files(src_dir)) + py_files = ( + (py_file, None) for py_file in + find_python_files(src_dir, self.include_namespace_packages) + ) plugin_files = self._find_plugin_files(src_dir) for file_path, plugin_name in itertools.chain(py_files, plugin_files): file_path = canonical_filename(file_path) if self.omit_match and self.omit_match.match(file_path): # Turns out this file was omitted, so don't pull it back - # in as unexecuted. + # in as un-executed. continue yield file_path, plugin_name - def sys_info(self): + def sys_info(self) -> Iterable[Tuple[str, Any]]: """Our information for Coverage.sys_info. Returns a list of (key, value) pairs. @@ -585,12 +576,13 @@ ("coverage_paths", self.cover_paths), ("stdlib_paths", self.pylib_paths), ("third_party_paths", self.third_paths), + ("source_in_third_party_paths", self.source_in_third_paths), ] matcher_names = [ - 'source_match', 'source_pkgs_match', - 'include_match', 'omit_match', - 'cover_match', 'pylib_match', 'third_match', + "source_match", "source_pkgs_match", + "include_match", "omit_match", + "cover_match", "pylib_match", "third_match", "source_in_third_match", ] for matcher_name in matcher_names: @@ -598,7 +590,7 @@ if matcher: matcher_info = matcher.info() else: - matcher_info = '-none-' + matcher_info = "-none-" info.append((matcher_name, matcher_info)) return info diff -Nru python-coverage-6.5.0+dfsg1/coverage/jsonreport.py python-coverage-7.2.7+dfsg1/coverage/jsonreport.py --- python-coverage-6.5.0+dfsg1/coverage/jsonreport.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/jsonreport.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,13 +3,22 @@ """Json reporting for coverage.py""" +from __future__ import annotations + import datetime import json import sys +from typing import Any, Dict, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING + from coverage import __version__ -from coverage.report import get_analysis_to_report -from coverage.results import Numbers +from coverage.report_core import get_analysis_to_report +from coverage.results import Analysis, Numbers +from coverage.types import TMorf, TLineNo + +if TYPE_CHECKING: + from coverage import Coverage + from coverage.data import CoverageData class JsonReporter: @@ -17,13 +26,13 @@ report_type = "JSON report" - def __init__(self, coverage): + def __init__(self, coverage: Coverage) -> None: self.coverage = coverage self.config = self.coverage.config self.total = Numbers(self.config.precision) - self.report_data = {} + self.report_data: Dict[str, Any] = {} - def report(self, morfs, outfile=None): + def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float: """Generate a json report for `morfs`. `morfs` is a list of modules or file names. @@ -51,20 +60,20 @@ self.report_data["files"] = measured_files self.report_data["totals"] = { - 'covered_lines': self.total.n_executed, - 'num_statements': self.total.n_statements, - 'percent_covered': self.total.pc_covered, - 'percent_covered_display': self.total.pc_covered_str, - 'missing_lines': self.total.n_missing, - 'excluded_lines': self.total.n_excluded, + "covered_lines": self.total.n_executed, + "num_statements": self.total.n_statements, + "percent_covered": self.total.pc_covered, + "percent_covered_display": self.total.pc_covered_str, + "missing_lines": self.total.n_missing, + "excluded_lines": self.total.n_excluded, } if coverage_data.has_arcs(): self.report_data["totals"].update({ - 'num_branches': self.total.n_branches, - 'num_partial_branches': self.total.n_partial_branches, - 'covered_branches': self.total.n_executed_branches, - 'missing_branches': self.total.n_missing_branches, + "num_branches": self.total.n_branches, + "num_partial_branches": self.total.n_partial_branches, + "covered_branches": self.total.n_executed_branches, + "missing_branches": self.total.n_missing_branches, }) json.dump( @@ -75,43 +84,45 @@ return self.total.n_statements and self.total.pc_covered - def report_one_file(self, coverage_data, analysis): + def report_one_file(self, coverage_data: CoverageData, analysis: Analysis) -> Dict[str, Any]: """Extract the relevant report data for a single file.""" nums = analysis.numbers self.total += nums summary = { - 'covered_lines': nums.n_executed, - 'num_statements': nums.n_statements, - 'percent_covered': nums.pc_covered, - 'percent_covered_display': nums.pc_covered_str, - 'missing_lines': nums.n_missing, - 'excluded_lines': nums.n_excluded, + "covered_lines": nums.n_executed, + "num_statements": nums.n_statements, + "percent_covered": nums.pc_covered, + "percent_covered_display": nums.pc_covered_str, + "missing_lines": nums.n_missing, + "excluded_lines": nums.n_excluded, } reported_file = { - 'executed_lines': sorted(analysis.executed), - 'summary': summary, - 'missing_lines': sorted(analysis.missing), - 'excluded_lines': sorted(analysis.excluded), + "executed_lines": sorted(analysis.executed), + "summary": summary, + "missing_lines": sorted(analysis.missing), + "excluded_lines": sorted(analysis.excluded), } if self.config.json_show_contexts: - reported_file['contexts'] = analysis.data.contexts_by_lineno(analysis.filename) + reported_file["contexts"] = analysis.data.contexts_by_lineno(analysis.filename) if coverage_data.has_arcs(): - reported_file['summary'].update({ - 'num_branches': nums.n_branches, - 'num_partial_branches': nums.n_partial_branches, - 'covered_branches': nums.n_executed_branches, - 'missing_branches': nums.n_missing_branches, + summary.update({ + "num_branches": nums.n_branches, + "num_partial_branches": nums.n_partial_branches, + "covered_branches": nums.n_executed_branches, + "missing_branches": nums.n_missing_branches, }) - reported_file['executed_branches'] = list( + reported_file["executed_branches"] = list( _convert_branch_arcs(analysis.executed_branch_arcs()) ) - reported_file['missing_branches'] = list( + reported_file["missing_branches"] = list( _convert_branch_arcs(analysis.missing_branch_arcs()) ) return reported_file -def _convert_branch_arcs(branch_arcs): +def _convert_branch_arcs( + branch_arcs: Dict[TLineNo, List[TLineNo]], +) -> Iterable[Tuple[TLineNo, TLineNo]]: """Convert branch arcs to a list of two-element tuples.""" for source, targets in branch_arcs.items(): for target in targets: diff -Nru python-coverage-6.5.0+dfsg1/coverage/lcovreport.py python-coverage-7.2.7+dfsg1/coverage/lcovreport.py --- python-coverage-6.5.0+dfsg1/coverage/lcovreport.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/lcovreport.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,11 +3,27 @@ """LCOV reporting for coverage.py.""" -import sys +from __future__ import annotations + import base64 -from hashlib import md5 +import hashlib +import sys + +from typing import IO, Iterable, Optional, TYPE_CHECKING + +from coverage.plugin import FileReporter +from coverage.report_core import get_analysis_to_report +from coverage.results import Analysis, Numbers +from coverage.types import TMorf -from coverage.report import get_analysis_to_report +if TYPE_CHECKING: + from coverage import Coverage + + +def line_hash(line: str) -> str: + """Produce a hash of a source line for use in the LCOV file.""" + hashed = hashlib.md5(line.encode("utf-8")).digest() + return base64.b64encode(hashed).decode("ascii").rstrip("=") class LcovReporter: @@ -15,14 +31,14 @@ report_type = "LCOV report" - def __init__(self, coverage): + def __init__(self, coverage: Coverage) -> None: self.coverage = coverage - self.config = self.coverage.config + self.total = Numbers(self.coverage.config.precision) - def report(self, morfs, outfile=None): + def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float: """Renders the full lcov report. - 'morfs' is a list of modules or filenames + `morfs` is a list of modules or filenames outfile is the file object to write the file into. """ @@ -33,12 +49,16 @@ for fr, analysis in get_analysis_to_report(self.coverage, morfs): self.get_lcov(fr, analysis, outfile) - def get_lcov(self, fr, analysis, outfile=None): + return self.total.n_statements and self.total.pc_covered + + def get_lcov(self, fr: FileReporter, analysis: Analysis, outfile: IO[str]) -> None: """Produces the lcov data for a single file. This currently supports both line and branch coverage, however function coverage is not supported. """ + self.total += analysis.numbers + outfile.write("TN:\n") outfile.write(f"SF:{fr.relative_filename()}\n") source_lines = fr.source().splitlines() @@ -54,20 +74,20 @@ # characters of the encoding ("==") are removed from the hash to # allow genhtml to run on the resulting lcov file. if source_lines: - line = source_lines[covered-1].encode("utf-8") + if covered-1 >= len(source_lines): + break + line = source_lines[covered-1] else: - line = b"" - hashed = base64.b64encode(md5(line).digest()).decode().rstrip("=") - outfile.write(f"DA:{covered},1,{hashed}\n") + line = "" + outfile.write(f"DA:{covered},1,{line_hash(line)}\n") for missed in sorted(analysis.missing): assert source_lines - line = source_lines[missed-1].encode("utf-8") - hashed = base64.b64encode(md5(line).digest()).decode().rstrip("=") - outfile.write(f"DA:{missed},0,{hashed}\n") + line = source_lines[missed-1] + outfile.write(f"DA:{missed},0,{line_hash(line)}\n") - outfile.write(f"LF:{len(analysis.statements)}\n") - outfile.write(f"LH:{len(analysis.executed)}\n") + outfile.write(f"LF:{analysis.numbers.n_statements}\n") + outfile.write(f"LH:{analysis.numbers.n_executed}\n") # More information dense branch coverage data. missing_arcs = analysis.missing_branch_arcs() diff -Nru python-coverage-6.5.0+dfsg1/coverage/misc.py python-coverage-7.2.7+dfsg1/coverage/misc.py --- python-coverage-6.5.0+dfsg1/coverage/misc.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/misc.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,7 +3,10 @@ """Miscellaneous stuff for coverage.py.""" +from __future__ import annotations + import contextlib +import datetime import errno import hashlib import importlib @@ -16,18 +19,25 @@ import sys import types +from types import ModuleType +from typing import ( + Any, Callable, Dict, IO, Iterable, Iterator, List, Mapping, Optional, + Sequence, Tuple, TypeVar, Union, +) + from coverage import env from coverage.exceptions import CoverageException +from coverage.types import TArc # 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 = {} +ISOLATED_MODULES: Dict[ModuleType, ModuleType] = {} -def isolate_module(mod): +def isolate_module(mod: ModuleType) -> ModuleType: """Copy a module so that we are isolated from aggressive mocking. If a test suite mocks os.path.exists (for example), and then we need to use @@ -50,10 +60,10 @@ class SysModuleSaver: """Saves the contents of sys.modules, and removes new modules later.""" - def __init__(self): + def __init__(self) -> None: self.old_modules = set(sys.modules) - def restore(self): + def restore(self) -> None: """Remove any modules imported since this object started.""" new_modules = set(sys.modules) - self.old_modules for m in new_modules: @@ -61,7 +71,7 @@ @contextlib.contextmanager -def sys_modules_saved(): +def sys_modules_saved() -> Iterator[None]: """A context manager to remove any modules imported during a block.""" saver = SysModuleSaver() try: @@ -70,7 +80,7 @@ saver.restore() -def import_third_party(modname): +def import_third_party(modname: str) -> Tuple[ModuleType, bool]: """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 @@ -81,64 +91,19 @@ modname (str): the name of the module to import. Returns: - The imported module, or None if the module couldn't be imported. + The imported module, and a boolean indicating if the module could be imported. + + If the boolean is False, the module returned is not the one you want: don't use it. """ with sys_modules_saved(): try: - return importlib.import_module(modname) + return importlib.import_module(modname), True except ImportError: - return None - - -def dummy_decorator_with_args(*args_unused, **kwargs_unused): - """Dummy no-op implementation of a decorator with arguments.""" - def _decorator(func): - return func - return _decorator - - -# Use PyContracts for assertion testing on parameters and returns, but only if -# we are running our own test suite. -if env.USE_CONTRACTS: - from contracts import contract # pylint: disable=unused-import - from contracts import new_contract as raw_new_contract - - def new_contract(*args, **kwargs): - """A proxy for contracts.new_contract that doesn't mind happening twice.""" - try: - raw_new_contract(*args, **kwargs) - except ValueError: - # During meta-coverage, this module is imported twice, and - # PyContracts doesn't like redefining contracts. It's OK. - pass + return sys, False - # Define contract words that PyContract doesn't have. - new_contract('bytes', lambda v: isinstance(v, bytes)) - new_contract('unicode', lambda v: isinstance(v, str)) - - def one_of(argnames): - """Ensure that only one of the argnames is non-None.""" - def _decorator(func): - argnameset = {name.strip() for name in argnames.split(",")} - def _wrapper(*args, **kwargs): - vals = [kwargs.get(name) for name in argnameset] - assert sum(val is not None for val in vals) == 1 - return func(*args, **kwargs) - return _wrapper - return _decorator -else: # pragma: not testing - # We aren't using real PyContracts, so just define our decorators as - # stunt-double no-ops. - contract = dummy_decorator_with_args - one_of = dummy_decorator_with_args - - def new_contract(*args_unused, **kwargs_unused): - """Dummy no-op implementation of `new_contract`.""" - pass - -def nice_pair(pair): +def nice_pair(pair: TArc) -> str: """Make a nice string representation of a pair of numbers. If the numbers are equal, just return the number, otherwise return the pair @@ -152,7 +117,10 @@ return "%d-%d" % (start, end) -def expensive(fn): +TSelf = TypeVar("TSelf") +TRetVal = TypeVar("TRetVal") + +def expensive(fn: Callable[[TSelf], TRetVal]) -> Callable[[TSelf], TRetVal]: """A decorator to indicate that a method shouldn't be called more than once. Normally, this does nothing. During testing, this raises an exception if @@ -162,7 +130,7 @@ if env.TESTING: attr = "_once_" + fn.__name__ - def _wrapper(self): + def _wrapper(self: TSelf) -> TRetVal: if hasattr(self, attr): raise AssertionError(f"Shouldn't have called {fn.__name__} more than once") setattr(self, attr, True) @@ -172,7 +140,7 @@ return fn # pragma: not testing -def bool_or_none(b): +def bool_or_none(b: Any) -> Optional[bool]: """Return bool(b), but preserve None.""" if b is None: return None @@ -180,12 +148,16 @@ return bool(b) -def join_regex(regexes): - """Combine a list of regexes into one that matches any of them.""" - return "|".join(f"(?:{r})" for r in regexes) +def join_regex(regexes: Iterable[str]) -> str: + """Combine a series of regex strings into one that matches any of them.""" + regexes = list(regexes) + if len(regexes) == 1: + return regexes[0] + else: + return "|".join(f"(?:{r})" for r in regexes) -def file_be_gone(path): +def file_be_gone(path: str) -> None: """Remove a file, and don't get annoyed if it doesn't exist.""" try: os.remove(path) @@ -194,7 +166,7 @@ raise -def ensure_dir(directory): +def ensure_dir(directory: str) -> None: """Make sure the directory exists. If `directory` is None or empty, do nothing. @@ -203,12 +175,12 @@ os.makedirs(directory, exist_ok=True) -def ensure_dir_for_file(path): +def ensure_dir_for_file(path: str) -> None: """Make sure the directory for the path exists.""" ensure_dir(os.path.dirname(path)) -def output_encoding(outfile=None): +def output_encoding(outfile: Optional[IO[str]] = None) -> str: """Determine the encoding to use for output written to `outfile` or stdout.""" if outfile is None: outfile = sys.stdout @@ -222,10 +194,10 @@ class Hasher: """Hashes Python data for fingerprinting.""" - def __init__(self): + def __init__(self) -> None: self.hash = hashlib.new("sha3_256") - def update(self, v): + def update(self, v: Any) -> None: """Add `v` to the hash, recursively if needed.""" self.hash.update(str(type(v)).encode("utf-8")) if isinstance(v, str): @@ -246,21 +218,21 @@ self.update(v[k]) else: for k in dir(v): - if k.startswith('__'): + if k.startswith("__"): continue a = getattr(v, k) if inspect.isroutine(a): continue self.update(k) self.update(a) - self.hash.update(b'.') + self.hash.update(b".") - def hexdigest(self): + def hexdigest(self) -> str: """Retrieve the hex digest of the hash.""" return self.hash.hexdigest()[:32] -def _needs_to_implement(that, func_name): +def _needs_to_implement(that: Any, func_name: str) -> None: """Helper to raise NotImplementedError in interface stubs.""" if hasattr(that, "_coverage_plugin_name"): thing = "Plugin" @@ -282,14 +254,14 @@ and Sphinx output. """ - def __init__(self, display_as): + def __init__(self, display_as: str) -> None: self.display_as = display_as - def __repr__(self): + def __repr__(self) -> str: return self.display_as -def substitute_variables(text, variables): +def substitute_variables(text: str, variables: Mapping[str, str]) -> str: """Substitute ``${VAR}`` variables in `text` with their values. Variables in the text can take a number of shell-inspired forms:: @@ -320,9 +292,9 @@ ) """ - dollar_groups = ('dollar', 'word1', 'word2') + dollar_groups = ("dollar", "word1", "word2") - def dollar_replace(match): + def dollar_replace(match: re.Match[str]) -> str: """Called for each $replacement.""" # Only one of the dollar_groups will have matched, just get its text. word = next(g for g in match.group(*dollar_groups) if g) # pragma: always breaks @@ -330,23 +302,23 @@ return "$" elif word in variables: return variables[word] - elif match['strict']: + elif match["strict"]: msg = f"Variable {word} is undefined: {text!r}" raise CoverageException(msg) else: - return match['defval'] + return match["defval"] text = re.sub(dollar_pattern, dollar_replace, text) return text -def format_local_datetime(dt): +def format_local_datetime(dt: datetime.datetime) -> str: """Return a string with local timezone representing the date. """ - return dt.astimezone().strftime('%Y-%m-%d %H:%M %z') + return dt.astimezone().strftime("%Y-%m-%d %H:%M %z") -def import_local_file(modname, modfile=None): +def import_local_file(modname: str, modfile: Optional[str] = None) -> ModuleType: """Import a local file as a module. Opens a file in the current directory named `modname`.py, imports it @@ -355,20 +327,22 @@ """ if modfile is None: - modfile = modname + '.py' + modfile = modname + ".py" spec = importlib.util.spec_from_file_location(modname, modfile) + assert spec is not None mod = importlib.util.module_from_spec(spec) sys.modules[modname] = mod + assert spec.loader is not None spec.loader.exec_module(mod) return mod -def human_key(s): +def _human_key(s: str) -> List[Union[str, int]]: """Turn a string into a list of string and number chunks. "z23a" -> ["z", 23, "a"] """ - def tryint(s): + def tryint(s: str) -> Union[str, int]: """If `s` is a number, return an int, else `s` unchanged.""" try: return int(s) @@ -377,7 +351,7 @@ return [tryint(c) for c in re.split(r"(\d+)", s)] -def human_sorted(strings): +def human_sorted(strings: Iterable[str]) -> List[str]: """Sort the given iterable of strings the way that humans expect. Numeric components in the strings are sorted as numbers. @@ -385,17 +359,25 @@ Returns the sorted list. """ - return sorted(strings, key=human_key) + return sorted(strings, key=_human_key) + +SortableItem = TypeVar("SortableItem", bound=Sequence[Any]) + +def human_sorted_items( + items: Iterable[SortableItem], + reverse: bool = False, +) -> List[SortableItem]: + """Sort (string, ...) items the way humans expect. -def human_sorted_items(items, reverse=False): - """Sort the (string, value) items the way humans expect. + The elements of `items` can be any tuple/list. They'll be sorted by the + first element (a string), with ties broken by the remaining elements. Returns the sorted list of items. """ - return sorted(items, key=lambda pair: (human_key(pair[0]), pair[1]), reverse=reverse) + return sorted(items, key=lambda item: (_human_key(item[0]), *item[1:]), reverse=reverse) -def plural(n, thing="", things=""): +def plural(n: int, thing: str = "", things: str = "") -> str: """Pluralize a word. If n is 1, return thing. Otherwise return things, or thing+s. @@ -404,3 +386,15 @@ return thing else: return things or (thing + "s") + + +def stdout_link(text: str, url: str) -> str: + """Format text+url as a clickable link for stdout. + + If attached to a terminal, use escape sequences. Otherwise, just return + the text. + """ + if hasattr(sys.stdout, "isatty") and sys.stdout.isatty(): + return f"\033]8;;{url}\a{text}\033]8;;\a" + else: + return text diff -Nru python-coverage-6.5.0+dfsg1/coverage/multiproc.py python-coverage-7.2.7+dfsg1/coverage/multiproc.py --- python-coverage-6.5.0+dfsg1/coverage/multiproc.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/multiproc.py 2023-05-29 19:46:30.000000000 +0000 @@ -10,7 +10,8 @@ import sys import traceback -from coverage.misc import contract +from typing import Any, Dict + # An attribute that will be set on the module to indicate that it has been # monkey-patched. @@ -18,12 +19,12 @@ OriginalProcess = multiprocessing.process.BaseProcess -original_bootstrap = OriginalProcess._bootstrap +original_bootstrap = OriginalProcess._bootstrap # type: ignore[attr-defined] class ProcessWithCoverage(OriginalProcess): # pylint: disable=abstract-method """A replacement for multiprocess.Process that starts coverage.""" - def _bootstrap(self, *args, **kwargs): + def _bootstrap(self, *args, **kwargs): # type: ignore[no-untyped-def] """Wrapper around _bootstrap to start coverage.""" try: from coverage import Coverage # avoid circular import @@ -31,6 +32,7 @@ cov._warn_preimported_source = False cov.start() debug = cov._debug + assert debug is not None if debug.should("multiproc"): debug.write("Calling multiprocessing bootstrap") except Exception: @@ -50,18 +52,17 @@ class Stowaway: """An object to pickle, so when it is unpickled, it can apply the monkey-patch.""" - def __init__(self, rcfile): + def __init__(self, rcfile: str) -> None: self.rcfile = rcfile - def __getstate__(self): - return {'rcfile': self.rcfile} + def __getstate__(self) -> Dict[str, str]: + return {"rcfile": self.rcfile} - def __setstate__(self, state): - patch_multiprocessing(state['rcfile']) + def __setstate__(self, state: Dict[str, str]) -> None: + patch_multiprocessing(state["rcfile"]) -@contract(rcfile=str) -def patch_multiprocessing(rcfile): +def patch_multiprocessing(rcfile: str) -> None: """Monkey-patch the multiprocessing module. This enables coverage measurement of processes started by multiprocessing. @@ -74,7 +75,7 @@ if hasattr(multiprocessing, PATCHED_MARKER): return - OriginalProcess._bootstrap = ProcessWithCoverage._bootstrap + OriginalProcess._bootstrap = ProcessWithCoverage._bootstrap # type: ignore[attr-defined] # Set the value in ProcessWithCoverage that will be pickled into the child # process. @@ -92,10 +93,10 @@ except (ImportError, AttributeError): pass else: - def get_preparation_data_with_stowaway(name): + def get_preparation_data_with_stowaway(name: str) -> Dict[str, Any]: """Get the original preparation data, and also insert our stowaway.""" d = original_get_preparation_data(name) - d['stowaway'] = Stowaway(rcfile) + d["stowaway"] = Stowaway(rcfile) return d spawn.get_preparation_data = get_preparation_data_with_stowaway diff -Nru python-coverage-6.5.0+dfsg1/coverage/numbits.py python-coverage-7.2.7+dfsg1/coverage/numbits.py --- python-coverage-6.5.0+dfsg1/coverage/numbits.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/numbits.py 2023-05-29 19:46:30.000000000 +0000 @@ -13,21 +13,17 @@ the future. Use these functions to work with those binary blobs of data. """ -import json - -from itertools import zip_longest -from coverage.misc import contract, new_contract +from __future__ import annotations -def _to_blob(b): - """Convert a bytestring into a type SQLite will accept for a blob.""" - return b +import json +import sqlite3 -new_contract('blob', lambda v: isinstance(v, bytes)) +from itertools import zip_longest +from typing import Iterable, List -@contract(nums='Iterable', returns='blob') -def nums_to_numbits(nums): +def nums_to_numbits(nums: Iterable[int]) -> bytes: """Convert `nums` into a numbits. Arguments: @@ -40,15 +36,14 @@ nbytes = max(nums) // 8 + 1 except ValueError: # nums was empty. - return _to_blob(b'') + return b"" b = bytearray(nbytes) for num in nums: b[num//8] |= 1 << num % 8 - return _to_blob(bytes(b)) + return bytes(b) -@contract(numbits='blob', returns='list[int]') -def numbits_to_nums(numbits): +def numbits_to_nums(numbits: bytes) -> List[int]: """Convert a numbits into a list of numbers. Arguments: @@ -69,19 +64,17 @@ return nums -@contract(numbits1='blob', numbits2='blob', returns='blob') -def numbits_union(numbits1, numbits2): +def numbits_union(numbits1: bytes, numbits2: bytes) -> bytes: """Compute the union of two numbits. Returns: A new numbits, the union of `numbits1` and `numbits2`. """ byte_pairs = zip_longest(numbits1, numbits2, fillvalue=0) - return _to_blob(bytes(b1 | b2 for b1, b2 in byte_pairs)) + return bytes(b1 | b2 for b1, b2 in byte_pairs) -@contract(numbits1='blob', numbits2='blob', returns='blob') -def numbits_intersection(numbits1, numbits2): +def numbits_intersection(numbits1: bytes, numbits2: bytes) -> bytes: """Compute the intersection of two numbits. Returns: @@ -89,11 +82,10 @@ """ byte_pairs = zip_longest(numbits1, numbits2, fillvalue=0) intersection_bytes = bytes(b1 & b2 for b1, b2 in byte_pairs) - return _to_blob(intersection_bytes.rstrip(b'\0')) + return intersection_bytes.rstrip(b"\0") -@contract(numbits1='blob', numbits2='blob', returns='bool') -def numbits_any_intersection(numbits1, numbits2): +def numbits_any_intersection(numbits1: bytes, numbits2: bytes) -> bool: """Is there any number that appears in both numbits? Determine whether two number sets have a non-empty intersection. This is @@ -106,8 +98,7 @@ return any(b1 & b2 for b1, b2 in byte_pairs) -@contract(num='int', numbits='blob', returns='bool') -def num_in_numbits(num, numbits): +def num_in_numbits(num: int, numbits: bytes) -> bool: """Does the integer `num` appear in `numbits`? Returns: @@ -119,7 +110,7 @@ return bool(numbits[nbyte] & (1 << nbit)) -def register_sqlite_functions(connection): +def register_sqlite_functions(connection: sqlite3.Connection) -> None: """ Define numbits functions in a SQLite connection. @@ -139,7 +130,7 @@ import sqlite3 from coverage.numbits import register_sqlite_functions - conn = sqlite3.connect('example.db') + conn = sqlite3.connect("example.db") register_sqlite_functions(conn) c = conn.cursor() # Kind of a nonsense query: diff -Nru python-coverage-6.5.0+dfsg1/coverage/parser.py python-coverage-7.2.7+dfsg1/coverage/parser.py --- python-coverage-6.5.0+dfsg1/coverage/parser.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/parser.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,19 +3,28 @@ """Code parsing for coverage.py.""" +from __future__ import annotations + import ast import collections import os import re +import sys import token import tokenize +from types import CodeType +from typing import ( + cast, Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple, +) + from coverage import env from coverage.bytecode import code_objects from coverage.debug import short_stack -from coverage.exceptions import NoSource, NotPython, _StopEverything -from coverage.misc import contract, join_regex, new_contract, nice_pair, one_of -from coverage.phystokens import compile_unicode, generate_tokens, neuter_encoding_declaration +from coverage.exceptions import NoSource, NotPython +from coverage.misc import join_regex, nice_pair +from coverage.phystokens import generate_tokens +from coverage.types import Protocol, TArc, TLineNo class PythonParser: @@ -25,18 +34,23 @@ involved. """ - @contract(text='unicode|None') - def __init__(self, text=None, filename=None, exclude=None): + def __init__( + self, + text: Optional[str] = None, + filename: Optional[str] = None, + exclude: Optional[str] = None, + ) -> None: """ Source can be provided as `text`, the text itself, or `filename`, from which the text will be read. Excluded lines are those that match - `exclude`, a regex. + `exclude`, a regex string. """ assert text or filename, "PythonParser needs either text or filename" self.filename = filename or "" - self.text = text - if not self.text: + if text is not None: + self.text: str = text + else: from coverage.python import get_python_source try: self.text = get_python_source(self.filename) @@ -46,45 +60,45 @@ self.exclude = exclude # The text lines of the parsed code. - self.lines = self.text.split('\n') + self.lines: List[str] = self.text.split("\n") # The normalized line numbers of the statements in the code. Exclusions # are taken into account, and statements are adjusted to their first # lines. - self.statements = set() + self.statements: Set[TLineNo] = set() # The normalized line numbers of the excluded lines in the code, # adjusted to their first lines. - self.excluded = set() + self.excluded: Set[TLineNo] = set() # The raw_* attributes are only used in this class, and in # lab/parser.py to show how this class is working. # The line numbers that start statements, as reported by the line # number table in the bytecode. - self.raw_statements = set() + self.raw_statements: Set[TLineNo] = set() # The raw line numbers of excluded lines of code, as marked by pragmas. - self.raw_excluded = set() + self.raw_excluded: Set[TLineNo] = set() # The line numbers of class definitions. - self.raw_classdefs = set() + self.raw_classdefs: Set[TLineNo] = set() # The line numbers of docstring lines. - self.raw_docstrings = set() + self.raw_docstrings: Set[TLineNo] = set() # Internal detail, used by lab/parser.py. self.show_tokens = False # A dict mapping line numbers to lexical statement starts for # multi-line statements. - self._multiline = {} + self._multiline: Dict[TLineNo, TLineNo] = {} # Lazily-created arc data, and missing arc descriptions. - self._all_arcs = None - self._missing_arc_fragments = None + self._all_arcs: Optional[Set[TArc]] = None + self._missing_arc_fragments: Optional[TArcFragments] = None - def lines_matching(self, *regexes): + def lines_matching(self, *regexes: str) -> Set[TLineNo]: """Find the lines matching one of a list of regexes. Returns a set of line numbers, the lines that contain a match for one @@ -100,7 +114,7 @@ matches.add(i) return matches - def _raw_parse(self): + def _raw_parse(self) -> None: """Parse the source to find the interesting facts about its lines. A handful of attributes are updated. @@ -122,6 +136,7 @@ first_on_line = True nesting = 0 + assert self.text is not None tokgen = generate_tokens(self.text) for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen: if self.show_tokens: # pragma: debugging @@ -134,13 +149,13 @@ elif toktype == token.DEDENT: indent -= 1 elif toktype == token.NAME: - if ttext == 'class': + if ttext == "class": # Class definitions look like branches in the bytecode, so # we need to exclude them. The simplest way is to note the - # lines with the 'class' keyword. + # lines with the "class" keyword. self.raw_classdefs.add(slineno) elif toktype == token.OP: - if ttext == ':' and nesting == 0: + if ttext == ":" and nesting == 0: should_exclude = (elineno in self.raw_excluded) or excluding_decorators if not excluding and should_exclude: # Start excluding a suite. We trigger off of the colon @@ -150,7 +165,7 @@ exclude_indent = indent excluding = True excluding_decorators = False - elif ttext == '@' and first_on_line: + elif ttext == "@" and first_on_line: # A decorator. if elineno in self.raw_excluded: excluding_decorators = True @@ -167,21 +182,20 @@ # http://stackoverflow.com/questions/1769332/x/1769794#1769794 self.raw_docstrings.update(range(slineno, elineno+1)) elif toktype == token.NEWLINE: - if first_line is not None and elineno != first_line: + if first_line is not None and elineno != first_line: # type: ignore[unreachable] # We're at the end of a line, and we've ended on a # different line than the first line of the statement, # so record a multi-line range. - for l in range(first_line, elineno+1): + for l in range(first_line, elineno+1): # type: ignore[unreachable] self._multiline[l] = first_line first_line = None first_on_line = True if ttext.strip() and toktype != tokenize.COMMENT: - # A non-whitespace token. + # A non-white-space token. empty = False if first_line is None: - # The token is not whitespace, and is the first in a - # statement. + # The token is not white space, and is the first in a statement. first_line = slineno # Check whether to end an excluded suite. if excluding and indent <= exclude_indent: @@ -203,32 +217,32 @@ if env.PYBEHAVIOR.module_firstline_1 and self._multiline: self._multiline[1] = min(self.raw_statements) - def first_line(self, line): - """Return the first line number of the statement including `line`.""" - if line < 0: - line = -self._multiline.get(-line, -line) + def first_line(self, lineno: TLineNo) -> TLineNo: + """Return the first line number of the statement including `lineno`.""" + if lineno < 0: + lineno = -self._multiline.get(-lineno, -lineno) else: - line = self._multiline.get(line, line) - return line + lineno = self._multiline.get(lineno, lineno) + return lineno - def first_lines(self, lines): - """Map the line numbers in `lines` to the correct first line of the + def first_lines(self, linenos: Iterable[TLineNo]) -> Set[TLineNo]: + """Map the line numbers in `linenos` to the correct first line of the statement. Returns a set of the first lines. """ - return {self.first_line(l) for l in lines} + return {self.first_line(l) for l in linenos} - def translate_lines(self, lines): + def translate_lines(self, lines: Iterable[TLineNo]) -> Set[TLineNo]: """Implement `FileReporter.translate_lines`.""" return self.first_lines(lines) - def translate_arcs(self, arcs): + def translate_arcs(self, arcs: Iterable[TArc]) -> Set[TArc]: """Implement `FileReporter.translate_arcs`.""" - return [(self.first_line(a), self.first_line(b)) for (a, b) in arcs] + return {(self.first_line(a), self.first_line(b)) for (a, b) in arcs} - def parse_source(self): + def parse_source(self) -> None: """Parse source text to find executable lines, excluded lines, etc. Sets the .excluded and .statements attributes, normalized to the first @@ -237,7 +251,7 @@ """ try: self._raw_parse() - except (tokenize.TokenError, IndentationError) as err: + except (tokenize.TokenError, IndentationError, SyntaxError) as err: if hasattr(err, "lineno"): lineno = err.lineno # IndentationError else: @@ -253,7 +267,7 @@ starts = self.raw_statements - ignore self.statements = self.first_lines(starts) - ignore - def arcs(self): + def arcs(self) -> Set[TArc]: """Get information about the arcs available in the code. Returns a set of line number pairs. Line numbers have been normalized @@ -262,9 +276,10 @@ """ if self._all_arcs is None: self._analyze_ast() + assert self._all_arcs is not None return self._all_arcs - def _analyze_ast(self): + def _analyze_ast(self) -> None: """Run the AstArcAnalyzer and save its results. `_all_arcs` is the set of arcs in the code. @@ -282,13 +297,13 @@ self._missing_arc_fragments = aaa.missing_arc_fragments - def exit_counts(self): + def exit_counts(self) -> Dict[TLineNo, int]: """Get a count of exits from that each line. Excluded lines are excluded. """ - exit_counts = collections.defaultdict(int) + exit_counts: Dict[TLineNo, int] = collections.defaultdict(int) for l1, l2 in self.arcs(): if l1 < 0: # Don't ever report -1 as a line number @@ -309,10 +324,16 @@ return exit_counts - def missing_arc_description(self, start, end, executed_arcs=None): + def missing_arc_description( + self, + start: TLineNo, + end: TLineNo, + executed_arcs: Optional[Iterable[TArc]] = None, + ) -> str: """Provide an English sentence describing a missing arc.""" if self._missing_arc_fragments is None: self._analyze_ast() + assert self._missing_arc_fragments is not None actual_start = start @@ -352,31 +373,27 @@ class ByteParser: """Parse bytecode to understand the structure of code.""" - @contract(text='unicode') - def __init__(self, text, code=None, filename=None): + def __init__( + self, + text: str, + code: Optional[CodeType] = None, + filename: Optional[str] = None, + ) -> None: self.text = text - if code: + if code is not None: self.code = code else: + assert filename is not None try: - self.code = compile_unicode(text, filename, "exec") + self.code = compile(text, filename, "exec", dont_inherit=True) except SyntaxError as synerr: raise NotPython( "Couldn't parse '%s' as Python source: '%s' at line %d" % ( - filename, synerr.msg, synerr.lineno + filename, synerr.msg, synerr.lineno or 0 ) ) from synerr - # Alternative Python implementations don't always provide all the - # attributes on code objects that we need to do the analysis. - for attr in ['co_lnotab', 'co_firstlineno']: - if not hasattr(self.code, attr): - raise _StopEverything( # pragma: only jython - "This implementation of Python doesn't support code analysis.\n" + - "Run coverage.py under another Python for this command." - ) - - def child_parsers(self): + def child_parsers(self) -> Iterable[ByteParser]: """Iterate over all the code objects nested within this one. The iteration includes `self` as its first value. @@ -384,7 +401,7 @@ """ return (ByteParser(self.text, code=c) for c in code_objects(self.code)) - def _line_numbers(self): + def _line_numbers(self) -> Iterable[TLineNo]: """Yield the line numbers possible in this code object. Uses co_lnotab described in Python/compile.c to find the @@ -414,7 +431,7 @@ if line_num != last_line_num: yield line_num - def _find_statements(self): + def _find_statements(self) -> Iterable[TLineNo]: """Find the statements in `self.code`. Produce a sequence of line numbers that start statements. Recurses @@ -430,7 +447,36 @@ # AST analysis # -class BlockBase: +class ArcStart(collections.namedtuple("Arc", "lineno, cause")): + """The information needed to start an arc. + + `lineno` is the line number the arc starts from. + + `cause` is an English text fragment used as the `startmsg` for + AstArcAnalyzer.missing_arc_fragments. It will be used to describe why an + arc wasn't executed, so should fit well into a sentence of the form, + "Line 17 didn't run because {cause}." The fragment can include "{lineno}" + to have `lineno` interpolated into it. + + """ + def __new__(cls, lineno: TLineNo, cause: Optional[str] = None) -> ArcStart: + return super().__new__(cls, lineno, cause) + + +class TAddArcFn(Protocol): + """The type for AstArcAnalyzer.add_arc().""" + def __call__( + self, + start: TLineNo, + end: TLineNo, + smsg: Optional[str] = None, + emsg: Optional[str] = None, + ) -> None: + ... + +TArcFragments = Dict[TArc, List[Tuple[Optional[str], Optional[str]]]] + +class Block: """ Blocks need to handle various exiting statements in their own ways. @@ -440,56 +486,54 @@ stack. """ # pylint: disable=unused-argument - def process_break_exits(self, exits, add_arc): + def process_break_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: """Process break exits.""" # Because break can only appear in loops, and most subclasses # implement process_break_exits, this function is never reached. raise AssertionError - def process_continue_exits(self, exits, add_arc): + def process_continue_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: """Process continue exits.""" # Because continue can only appear in loops, and most subclasses # implement process_continue_exits, this function is never reached. raise AssertionError - def process_raise_exits(self, exits, add_arc): + def process_raise_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: """Process raise exits.""" return False - def process_return_exits(self, exits, add_arc): + def process_return_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: """Process return exits.""" return False -class LoopBlock(BlockBase): +class LoopBlock(Block): """A block on the block stack representing a `for` or `while` loop.""" - @contract(start=int) - def __init__(self, start): + def __init__(self, start: TLineNo) -> None: # The line number where the loop starts. self.start = start # A set of ArcStarts, the arcs from break statements exiting this loop. - self.break_exits = set() + self.break_exits: Set[ArcStart] = set() - def process_break_exits(self, exits, add_arc): + def process_break_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: self.break_exits.update(exits) return True - def process_continue_exits(self, exits, add_arc): + def process_continue_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: for xit in exits: add_arc(xit.lineno, self.start, xit.cause) return True -class FunctionBlock(BlockBase): +class FunctionBlock(Block): """A block on the block stack representing a function definition.""" - @contract(start=int, name=str) - def __init__(self, start, name): + def __init__(self, start: TLineNo, name: str) -> None: # The line number where the function starts. self.start = start # The name of the function. self.name = name - def process_raise_exits(self, exits, add_arc): + def process_raise_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: for xit in exits: add_arc( xit.lineno, -self.start, xit.cause, @@ -497,7 +541,7 @@ ) return True - def process_return_exits(self, exits, add_arc): + def process_return_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: for xit in exits: add_arc( xit.lineno, -self.start, xit.cause, @@ -506,10 +550,9 @@ return True -class TryBlock(BlockBase): +class TryBlock(Block): """A block on the block stack representing a `try` block.""" - @contract(handler_start='int|None', final_start='int|None') - def __init__(self, handler_start, final_start): + def __init__(self, handler_start: Optional[TLineNo], final_start: Optional[TLineNo]) -> None: # The line number of the first "except" handler, if any. self.handler_start = handler_start # The line number of the "finally:" clause, if any. @@ -517,24 +560,24 @@ # The ArcStarts for breaks/continues/returns/raises inside the "try:" # that need to route through the "finally:" clause. - self.break_from = set() - self.continue_from = set() - self.raise_from = set() - self.return_from = set() + self.break_from: Set[ArcStart] = set() + self.continue_from: Set[ArcStart] = set() + self.raise_from: Set[ArcStart] = set() + self.return_from: Set[ArcStart] = set() - def process_break_exits(self, exits, add_arc): + def process_break_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: if self.final_start is not None: self.break_from.update(exits) return True return False - def process_continue_exits(self, exits, add_arc): + def process_continue_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: if self.final_start is not None: self.continue_from.update(exits) return True return False - def process_raise_exits(self, exits, add_arc): + def process_raise_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: if self.handler_start is not None: for xit in exits: add_arc(xit.lineno, self.handler_start, xit.cause) @@ -543,17 +586,16 @@ self.raise_from.update(exits) return True - def process_return_exits(self, exits, add_arc): + def process_return_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: if self.final_start is not None: self.return_from.update(exits) return True return False -class WithBlock(BlockBase): +class WithBlock(Block): """A block on the block stack representing a `with` block.""" - @contract(start=int) - def __init__(self, start): + def __init__(self, start: TLineNo) -> None: # We only ever use this block if it is needed, so that we don't have to # check this setting in all the methods. assert env.PYBEHAVIOR.exit_through_with @@ -563,11 +605,16 @@ # The ArcStarts for breaks/continues/returns/raises inside the "with:" # that need to go through the with-statement while exiting. - self.break_from = set() - self.continue_from = set() - self.return_from = set() - - def _process_exits(self, exits, add_arc, from_set=None): + self.break_from: Set[ArcStart] = set() + self.continue_from: Set[ArcStart] = set() + self.return_from: Set[ArcStart] = set() + + def _process_exits( + self, + exits: Set[ArcStart], + add_arc: TAddArcFn, + from_set: Optional[Set[ArcStart]] = None, + ) -> bool: """Helper to process the four kinds of exits.""" for xit in exits: add_arc(xit.lineno, self.start, xit.cause) @@ -575,48 +622,27 @@ from_set.update(exits) return True - def process_break_exits(self, exits, add_arc): + def process_break_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: return self._process_exits(exits, add_arc, self.break_from) - def process_continue_exits(self, exits, add_arc): + def process_continue_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: return self._process_exits(exits, add_arc, self.continue_from) - def process_raise_exits(self, exits, add_arc): + def process_raise_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: return self._process_exits(exits, add_arc) - def process_return_exits(self, exits, add_arc): + def process_return_exits(self, exits: Set[ArcStart], add_arc: TAddArcFn) -> bool: return self._process_exits(exits, add_arc, self.return_from) -class ArcStart(collections.namedtuple("Arc", "lineno, cause")): - """The information needed to start an arc. - - `lineno` is the line number the arc starts from. - - `cause` is an English text fragment used as the `startmsg` for - AstArcAnalyzer.missing_arc_fragments. It will be used to describe why an - arc wasn't executed, so should fit well into a sentence of the form, - "Line 17 didn't run because {cause}." The fragment can include "{lineno}" - to have `lineno` interpolated into it. - - """ - def __new__(cls, lineno, cause=None): - return super().__new__(cls, lineno, cause) - - -# Define contract words that PyContract doesn't have. -# ArcStarts is for a list or set of ArcStart's. -new_contract('ArcStarts', lambda seq: all(isinstance(x, ArcStart) for x in seq)) - - -class NodeList: +class NodeList(ast.AST): """A synthetic fictitious node, containing a sequence of nodes. This is used when collapsing optimized if-statements, to represent the unconditional execution of one of the clauses. """ - def __init__(self, body): + def __init__(self, body: Sequence[ast.AST]) -> None: self.body = body self.lineno = body[0].lineno @@ -624,17 +650,25 @@ # TODO: the cause messages have too many commas. # TODO: Shouldn't the cause messages join with "and" instead of "or"? -def ast_parse(text): - """How we create an AST parse.""" - return ast.parse(neuter_encoding_declaration(text)) +def _make_expression_code_method(noun: str) -> Callable[[AstArcAnalyzer, ast.AST], None]: + """A function to make methods for expression-based callable _code_object__ methods.""" + def _code_object__expression_callable(self: AstArcAnalyzer, node: ast.AST) -> None: + start = self.line_for_node(node) + self.add_arc(-start, start, None, f"didn't run the {noun} on line {start}") + self.add_arc(start, -start, None, f"didn't finish the {noun} on line {start}") + return _code_object__expression_callable class AstArcAnalyzer: """Analyze source text with an AST to find executable code paths.""" - @contract(text='unicode', statements=set) - def __init__(self, text, statements, multiline): - self.root_node = ast_parse(text) + def __init__( + self, + text: str, + statements: Set[TLineNo], + multiline: Dict[TLineNo, TLineNo], + ) -> None: + self.root_node = ast.parse(text) # TODO: I think this is happening in too many places. self.statements = {multiline.get(l, l) for l in statements} self.multiline = multiline @@ -649,20 +683,20 @@ print(f"Multiline map: {self.multiline}") ast_dump(self.root_node) - self.arcs = set() + self.arcs: Set[TArc] = set() # A map from arc pairs to a list of pairs of sentence fragments: # { (start, end): [(startmsg, endmsg), ...], } # # For an arc from line 17, they should be usable like: # "Line 17 {endmsg}, because {startmsg}" - self.missing_arc_fragments = collections.defaultdict(list) - self.block_stack = [] + self.missing_arc_fragments: TArcFragments = collections.defaultdict(list) + self.block_stack: List[Block] = [] # $set_env.py: COVERAGE_TRACK_ARCS - Trace possible arcs added while parsing code. self.debug = bool(int(os.environ.get("COVERAGE_TRACK_ARCS", 0))) - def analyze(self): + def analyze(self) -> None: """Examine the AST tree from `root_node` to determine possible arcs. This sets the `arcs` attribute to be a set of (from, to) line number @@ -675,8 +709,13 @@ if code_object_handler is not None: code_object_handler(node) - @contract(start=int, end=int) - def add_arc(self, start, end, smsg=None, emsg=None): + def add_arc( + self, + start: TLineNo, + end: TLineNo, + smsg: Optional[str] = None, + emsg: Optional[str] = None, + ) -> None: """Add an arc, including message fragments to use if it is missing.""" if self.debug: # pragma: debugging print(f"\nAdding possible arc: ({start}, {end}): {smsg!r}, {emsg!r}") @@ -686,25 +725,27 @@ if smsg is not None or emsg is not None: self.missing_arc_fragments[(start, end)].append((smsg, emsg)) - def nearest_blocks(self): + def nearest_blocks(self) -> Iterable[Block]: """Yield the blocks in nearest-to-farthest order.""" return reversed(self.block_stack) - @contract(returns=int) - def line_for_node(self, node): + def line_for_node(self, node: ast.AST) -> TLineNo: """What is the right line number to use for this node? This dispatches to _line__Node functions where needed. """ node_name = node.__class__.__name__ - handler = getattr(self, "_line__" + node_name, None) + handler = cast( + Optional[Callable[[ast.AST], TLineNo]], + getattr(self, "_line__" + node_name, None) + ) if handler is not None: return handler(node) else: return node.lineno - def _line_decorated(self, node): + def _line_decorated(self, node: ast.FunctionDef) -> TLineNo: """Compute first line number for things that can be decorated (classes and functions).""" lineno = node.lineno if env.PYBEHAVIOR.trace_decorated_def or env.PYBEHAVIOR.def_ast_no_decorator: @@ -712,17 +753,17 @@ lineno = node.decorator_list[0].lineno return lineno - def _line__Assign(self, node): + def _line__Assign(self, node: ast.Assign) -> TLineNo: return self.line_for_node(node.value) _line__ClassDef = _line_decorated - def _line__Dict(self, node): + def _line__Dict(self, node: ast.Dict) -> TLineNo: if node.keys: if node.keys[0] is not None: return node.keys[0].lineno else: - # Unpacked dict literals `{**{'a':1}}` have None as the key, + # Unpacked dict literals `{**{"a":1}}` have None as the key, # use the value in that case. return node.values[0].lineno else: @@ -731,13 +772,13 @@ _line__FunctionDef = _line_decorated _line__AsyncFunctionDef = _line_decorated - def _line__List(self, node): + def _line__List(self, node: ast.List) -> TLineNo: if node.elts: return self.line_for_node(node.elts[0]) else: return node.lineno - def _line__Module(self, node): + def _line__Module(self, node: ast.Module) -> TLineNo: if env.PYBEHAVIOR.module_firstline_1: return 1 elif node.body: @@ -752,8 +793,7 @@ "Import", "ImportFrom", "Nonlocal", "Pass", } - @contract(returns='ArcStarts') - def add_arcs(self, node): + def add_arcs(self, node: ast.AST) -> Set[ArcStart]: """Add the arcs for `node`. Return a set of ArcStarts, exits from this node to the next. Because a @@ -770,7 +810,10 @@ """ node_name = node.__class__.__name__ - handler = getattr(self, "_handle__" + node_name, None) + handler = cast( + Optional[Callable[[ast.AST], Set[ArcStart]]], + getattr(self, "_handle__" + node_name, None) + ) if handler is not None: return handler(node) else: @@ -778,14 +821,17 @@ # statement), or it's something we overlooked. if env.TESTING: if node_name not in self.OK_TO_DEFAULT: - raise Exception(f"*** Unhandled: {node}") # pragma: only failure + raise RuntimeError(f"*** Unhandled: {node}") # pragma: only failure # Default for simple statements: one exit from this node. return {ArcStart(self.line_for_node(node))} - @one_of("from_start, prev_starts") - @contract(returns='ArcStarts') - def add_body_arcs(self, body, from_start=None, prev_starts=None): + def add_body_arcs( + self, + body: Sequence[ast.AST], + from_start: Optional[ArcStart] = None, + prev_starts: Optional[Set[ArcStart]] = None + ) -> Set[ArcStart]: """Add arcs for the body of a compound statement. `body` is the body node. `from_start` is a single `ArcStart` that can @@ -797,21 +843,23 @@ """ if prev_starts is None: + assert from_start is not None prev_starts = {from_start} for body_node in body: lineno = self.line_for_node(body_node) first_line = self.multiline.get(lineno, lineno) if first_line not in self.statements: - body_node = self.find_non_missing_node(body_node) - if body_node is None: + maybe_body_node = self.find_non_missing_node(body_node) + if maybe_body_node is None: continue + body_node = maybe_body_node lineno = self.line_for_node(body_node) for prev_start in prev_starts: self.add_arc(prev_start.lineno, lineno, prev_start.cause) prev_starts = self.add_arcs(body_node) return prev_starts - def find_non_missing_node(self, node): + def find_non_missing_node(self, node: ast.AST) -> Optional[ast.AST]: """Search `node` looking for a child that has not been optimized away. This might return the node you started with, or it will work recursively @@ -828,12 +876,15 @@ if first_line in self.statements: return node - missing_fn = getattr(self, "_missing__" + node.__class__.__name__, None) - if missing_fn: - node = missing_fn(node) + missing_fn = cast( + Optional[Callable[[ast.AST], Optional[ast.AST]]], + getattr(self, "_missing__" + node.__class__.__name__, None) + ) + if missing_fn is not None: + ret_node = missing_fn(node) else: - node = None - return node + ret_node = None + return ret_node # Missing nodes: _missing__* # @@ -842,7 +893,7 @@ # find_non_missing_node) to find a node to use instead of the missing # node. They can return None if the node should truly be gone. - def _missing__If(self, node): + def _missing__If(self, node: ast.If) -> Optional[ast.AST]: # If the if-node is missing, then one of its children might still be # here, but not both. So return the first of the two that isn't missing. # Use a NodeList to hold the clauses as a single node. @@ -853,14 +904,14 @@ return self.find_non_missing_node(NodeList(node.orelse)) return None - def _missing__NodeList(self, node): + def _missing__NodeList(self, node: NodeList) -> Optional[ast.AST]: # A NodeList might be a mixture of missing and present nodes. Find the # ones that are present. non_missing_children = [] for child in node.body: - child = self.find_non_missing_node(child) - if child is not None: - non_missing_children.append(child) + maybe_child = self.find_non_missing_node(child) + if maybe_child is not None: + non_missing_children.append(maybe_child) # Return the simplest representation of the present children. if not non_missing_children: @@ -869,7 +920,7 @@ return non_missing_children[0] return NodeList(non_missing_children) - def _missing__While(self, node): + def _missing__While(self, node: ast.While) -> Optional[ast.AST]: body_nodes = self.find_non_missing_node(NodeList(node.body)) if not body_nodes: return None @@ -879,16 +930,17 @@ new_while.test = ast.Name() new_while.test.lineno = body_nodes.lineno new_while.test.id = "True" + assert hasattr(body_nodes, "body") new_while.body = body_nodes.body - new_while.orelse = None + new_while.orelse = [] return new_while - def is_constant_expr(self, node): + def is_constant_expr(self, node: ast.AST) -> Optional[str]: """Is this a compile-time constant?""" node_name = node.__class__.__name__ if node_name in ["Constant", "NameConstant", "Num"]: return "Num" - elif node_name == "Name": + elif isinstance(node, ast.Name): if node.id in ["True", "False", "None", "__debug__"]: return "Name" return None @@ -900,7 +952,6 @@ # listcomps hidden in lists: x = [[i for i in range(10)]] # nested function definitions - # Exit processing: process_*_exits # # These functions process the four kinds of jump exits: break, continue, @@ -909,29 +960,25 @@ # enclosing loop block, or the nearest enclosing finally block, whichever # is nearer. - @contract(exits='ArcStarts') - def process_break_exits(self, exits): + def process_break_exits(self, exits: Set[ArcStart]) -> None: """Add arcs due to jumps from `exits` being breaks.""" for block in self.nearest_blocks(): # pragma: always breaks if block.process_break_exits(exits, self.add_arc): break - @contract(exits='ArcStarts') - def process_continue_exits(self, exits): + def process_continue_exits(self, exits: Set[ArcStart]) -> None: """Add arcs due to jumps from `exits` being continues.""" for block in self.nearest_blocks(): # pragma: always breaks if block.process_continue_exits(exits, self.add_arc): break - @contract(exits='ArcStarts') - def process_raise_exits(self, exits): + def process_raise_exits(self, exits: Set[ArcStart]) -> None: """Add arcs due to jumps from `exits` being raises.""" for block in self.nearest_blocks(): if block.process_raise_exits(exits, self.add_arc): break - @contract(exits='ArcStarts') - def process_return_exits(self, exits): + def process_return_exits(self, exits: Set[ArcStart]) -> None: """Add arcs due to jumps from `exits` being returns.""" for block in self.nearest_blocks(): # pragma: always breaks if block.process_return_exits(exits, self.add_arc): @@ -948,17 +995,16 @@ # Every node type that represents a statement should have a handler, or it # should be listed in OK_TO_DEFAULT. - @contract(returns='ArcStarts') - def _handle__Break(self, node): + def _handle__Break(self, node: ast.Break) -> Set[ArcStart]: here = self.line_for_node(node) break_start = ArcStart(here, cause="the break on line {lineno} wasn't executed") - self.process_break_exits([break_start]) + self.process_break_exits({break_start}) return set() - @contract(returns='ArcStarts') - def _handle_decorated(self, node): + def _handle_decorated(self, node: ast.FunctionDef) -> Set[ArcStart]: """Add arcs for things that can be decorated (classes and functions).""" - main_line = last = node.lineno + main_line: TLineNo = node.lineno + last: Optional[TLineNo] = node.lineno decs = node.decorator_list if decs: if env.PYBEHAVIOR.trace_decorated_def or env.PYBEHAVIOR.def_ast_no_decorator: @@ -968,6 +1014,7 @@ if last is not None and dec_start != last: self.add_arc(last, dec_start) last = dec_start + assert last is not None if env.PYBEHAVIOR.trace_decorated_def: self.add_arc(last, main_line) last = main_line @@ -988,19 +1035,18 @@ self.add_arc(last, lineno) last = lineno # The body is handled in collect_arcs. + assert last is not None return {ArcStart(last)} _handle__ClassDef = _handle_decorated - @contract(returns='ArcStarts') - def _handle__Continue(self, node): + def _handle__Continue(self, node: ast.Continue) -> Set[ArcStart]: here = self.line_for_node(node) continue_start = ArcStart(here, cause="the continue on line {lineno} wasn't executed") - self.process_continue_exits([continue_start]) + self.process_continue_exits({continue_start}) return set() - @contract(returns='ArcStarts') - def _handle__For(self, node): + def _handle__For(self, node: ast.For) -> Set[ArcStart]: start = self.line_for_node(node.iter) self.block_stack.append(LoopBlock(start=start)) from_start = ArcStart(start, cause="the loop on line {lineno} never started") @@ -1009,6 +1055,7 @@ for xit in exits: self.add_arc(xit.lineno, start, xit.cause) my_block = self.block_stack.pop() + assert isinstance(my_block, LoopBlock) exits = my_block.break_exits from_start = ArcStart(start, cause="the loop on line {lineno} didn't complete") if node.orelse: @@ -1024,8 +1071,7 @@ _handle__FunctionDef = _handle_decorated _handle__AsyncFunctionDef = _handle_decorated - @contract(returns='ArcStarts') - def _handle__If(self, node): + def _handle__If(self, node: ast.If) -> Set[ArcStart]: start = self.line_for_node(node.test) from_start = ArcStart(start, cause="the condition on line {lineno} was never true") exits = self.add_body_arcs(node.body, from_start=from_start) @@ -1033,48 +1079,50 @@ exits |= self.add_body_arcs(node.orelse, from_start=from_start) return exits - @contract(returns='ArcStarts') - def _handle__Match(self, node): - start = self.line_for_node(node) - last_start = start - exits = set() - had_wildcard = False - for case in node.cases: - case_start = self.line_for_node(case.pattern) - if isinstance(case.pattern, ast.MatchAs): - had_wildcard = True - self.add_arc(last_start, case_start, "the pattern on line {lineno} always matched") - from_start = ArcStart(case_start, cause="the pattern on line {lineno} never matched") - exits |= self.add_body_arcs(case.body, from_start=from_start) - last_start = case_start - if not had_wildcard: - exits.add(from_start) - return exits + if sys.version_info >= (3, 10): + def _handle__Match(self, node: ast.Match) -> Set[ArcStart]: + start = self.line_for_node(node) + last_start = start + exits = set() + had_wildcard = False + for case in node.cases: + case_start = self.line_for_node(case.pattern) + pattern = case.pattern + while isinstance(pattern, ast.MatchOr): + pattern = pattern.patterns[-1] + if isinstance(pattern, ast.MatchAs): + had_wildcard = True + self.add_arc(last_start, case_start, "the pattern on line {lineno} always matched") + from_start = ArcStart( + case_start, + cause="the pattern on line {lineno} never matched", + ) + exits |= self.add_body_arcs(case.body, from_start=from_start) + last_start = case_start + if not had_wildcard: + exits.add(from_start) + return exits - @contract(returns='ArcStarts') - def _handle__NodeList(self, node): + def _handle__NodeList(self, node: NodeList) -> Set[ArcStart]: start = self.line_for_node(node) exits = self.add_body_arcs(node.body, from_start=ArcStart(start)) return exits - @contract(returns='ArcStarts') - def _handle__Raise(self, node): + def _handle__Raise(self, node: ast.Raise) -> Set[ArcStart]: here = self.line_for_node(node) raise_start = ArcStart(here, cause="the raise on line {lineno} wasn't executed") - self.process_raise_exits([raise_start]) + self.process_raise_exits({raise_start}) # `raise` statement jumps away, no exits from here. return set() - @contract(returns='ArcStarts') - def _handle__Return(self, node): + def _handle__Return(self, node: ast.Return) -> Set[ArcStart]: here = self.line_for_node(node) return_start = ArcStart(here, cause="the return on line {lineno} wasn't executed") - self.process_return_exits([return_start]) + self.process_return_exits({return_start}) # `return` statement jumps away, no exits from here. return set() - @contract(returns='ArcStarts') - def _handle__Try(self, node): + def _handle__Try(self, node: ast.Try) -> Set[ArcStart]: if node.handlers: handler_start = self.line_for_node(node.handlers[0]) else: @@ -1107,10 +1155,10 @@ else: self.block_stack.pop() - handler_exits = set() + handler_exits: Set[ArcStart] = set() if node.handlers: - last_handler_start = None + last_handler_start: Optional[TLineNo] = None for handler_node in node.handlers: handler_start = self.line_for_node(handler_node) if last_handler_start is not None: @@ -1185,8 +1233,7 @@ return exits - @contract(starts='ArcStarts', exits='ArcStarts', returns='ArcStarts') - def _combine_finally_starts(self, starts, exits): + def _combine_finally_starts(self, starts: Set[ArcStart], exits: Set[ArcStart]) -> Set[ArcStart]: """Helper for building the cause of `finally` branches. "finally" clauses might not execute their exits, and the causes could @@ -1201,8 +1248,7 @@ exits = {ArcStart(xit.lineno, cause) for xit in exits} return exits - @contract(returns='ArcStarts') - def _handle__While(self, node): + def _handle__While(self, node: ast.While) -> Set[ArcStart]: start = to_top = self.line_for_node(node.test) constant_test = self.is_constant_expr(node.test) top_is_body0 = False @@ -1219,6 +1265,7 @@ self.add_arc(xit.lineno, to_top, xit.cause) exits = set() my_block = self.block_stack.pop() + assert isinstance(my_block, LoopBlock) exits.update(my_block.break_exits) from_start = ArcStart(start, cause="the condition on line {lineno} was never false") if node.orelse: @@ -1230,14 +1277,14 @@ exits.add(from_start) return exits - @contract(returns='ArcStarts') - def _handle__With(self, node): + def _handle__With(self, node: ast.With) -> Set[ArcStart]: start = self.line_for_node(node) if env.PYBEHAVIOR.exit_through_with: self.block_stack.append(WithBlock(start=start)) exits = self.add_body_arcs(node.body, from_start=ArcStart(start)) if env.PYBEHAVIOR.exit_through_with: with_block = self.block_stack.pop() + assert isinstance(with_block, WithBlock) with_exit = {ArcStart(start)} if exits: for xit in exits: @@ -1264,7 +1311,7 @@ # These methods are used by analyze() as the start of the analysis. # There is one for each construct with a code object. - def _code_object__Module(self, node): + def _code_object__Module(self, node: ast.Module) -> None: start = self.line_for_node(node) if node.body: exits = self.add_body_arcs(node.body, from_start=ArcStart(-start)) @@ -1275,7 +1322,7 @@ self.add_arc(-start, start) self.add_arc(start, -start) - def _code_object__FunctionDef(self, node): + def _code_object__FunctionDef(self, node: ast.FunctionDef) -> None: start = self.line_for_node(node) self.block_stack.append(FunctionBlock(start=start, name=node.name)) exits = self.add_body_arcs(node.body, from_start=ArcStart(-start)) @@ -1284,7 +1331,7 @@ _code_object__AsyncFunctionDef = _code_object__FunctionDef - def _code_object__ClassDef(self, node): + def _code_object__ClassDef(self, node: ast.ClassDef) -> None: start = self.line_for_node(node) self.add_arc(-start, start) exits = self.add_body_arcs(node.body, from_start=ArcStart(start)) @@ -1294,33 +1341,30 @@ f"didn't exit the body of class {node.name!r}", ) - def _make_expression_code_method(noun): # pylint: disable=no-self-argument - """A function to make methods for expression-based callable _code_object__ methods.""" - def _code_object__expression_callable(self, node): - start = self.line_for_node(node) - self.add_arc(-start, start, None, f"didn't run the {noun} on line {start}") - self.add_arc(start, -start, None, f"didn't finish the {noun} on line {start}") - return _code_object__expression_callable - _code_object__Lambda = _make_expression_code_method("lambda") _code_object__GeneratorExp = _make_expression_code_method("generator expression") - _code_object__DictComp = _make_expression_code_method("dictionary comprehension") - _code_object__SetComp = _make_expression_code_method("set comprehension") - _code_object__ListComp = _make_expression_code_method("list comprehension") + if env.PYBEHAVIOR.comprehensions_are_functions: + _code_object__DictComp = _make_expression_code_method("dictionary comprehension") + _code_object__SetComp = _make_expression_code_method("set comprehension") + _code_object__ListComp = _make_expression_code_method("list comprehension") # Code only used when dumping the AST for debugging. SKIP_DUMP_FIELDS = ["ctx"] -def _is_simple_value(value): +def _is_simple_value(value: Any) -> bool: """Is `value` simple enough to be displayed on a single line?""" return ( - value in [None, [], (), {}, set()] or + value in [None, [], (), {}, set(), frozenset(), Ellipsis] or isinstance(value, (bytes, int, float, str)) ) -def ast_dump(node, depth=0, print=print): # pylint: disable=redefined-builtin +def ast_dump( + node: ast.AST, + depth: int = 0, + print: Callable[[str], None] = print, # pylint: disable=redefined-builtin +) -> None: """Dump the AST for `node`. This recursively walks the AST, printing a readable version. @@ -1331,6 +1375,7 @@ if lineno is not None: linemark = f" @ {node.lineno},{node.col_offset}" if hasattr(node, "end_lineno"): + assert hasattr(node, "end_col_offset") linemark += ":" if node.end_lineno != node.lineno: linemark += f"{node.end_lineno}," @@ -1352,7 +1397,7 @@ else: print(head) if 0: - print("{}# mro: {}".format( + print("{}# mro: {}".format( # type: ignore[unreachable] indent, ", ".join(c.__name__ for c in node.__class__.__mro__[1:]), )) next_indent = indent + " " diff -Nru python-coverage-6.5.0+dfsg1/coverage/phystokens.py python-coverage-7.2.7+dfsg1/coverage/phystokens.py --- python-coverage-6.5.0+dfsg1/coverage/phystokens.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/phystokens.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,17 +3,26 @@ """Better tokenizing for coverage.py.""" +from __future__ import annotations + import ast +import io import keyword import re +import sys import token import tokenize +from typing import Iterable, List, Optional, Set, Tuple + from coverage import env -from coverage.misc import contract +from coverage.types import TLineNo, TSourceTokenLines + +TokenInfos = Iterable[tokenize.TokenInfo] -def phys_tokens(toks): + +def _phys_tokens(toks: TokenInfos) -> TokenInfos: """Return all physical tokens, even line continuations. tokenize.generate_tokens() doesn't return a token for the backslash that @@ -23,9 +32,9 @@ Returns the same values as generate_tokens() """ - last_line = None + last_line: Optional[str] = None last_lineno = -1 - last_ttext = None + last_ttext: str = "" for ttype, ttext, (slineno, scol), (elineno, ecol), ltext in toks: if last_lineno != elineno: if last_line and last_line.endswith("\\\n"): @@ -48,7 +57,7 @@ if last_ttext.endswith("\\"): inject_backslash = False elif ttype == token.STRING: - if "\n" in ttext and ttext.split('\n', 1)[0][-1] == '\\': + if "\n" in ttext and ttext.split("\n", 1)[0][-1] == "\\": # It's a multi-line string and the first line ends with # a backslash, so we don't need to inject another. inject_backslash = False @@ -56,7 +65,7 @@ # Figure out what column the backslash is in. ccol = len(last_line.split("\n")[-2]) - 1 # Yield the token, with a fake token type. - yield ( + yield tokenize.TokenInfo( 99999, "\\\n", (slineno, ccol), (slineno, ccol+2), last_line @@ -64,27 +73,27 @@ last_line = ltext if ttype not in (tokenize.NEWLINE, tokenize.NL): last_ttext = ttext - yield ttype, ttext, (slineno, scol), (elineno, ecol), ltext + yield tokenize.TokenInfo(ttype, ttext, (slineno, scol), (elineno, ecol), ltext) last_lineno = elineno class MatchCaseFinder(ast.NodeVisitor): """Helper for finding match/case lines.""" - def __init__(self, source): + def __init__(self, source: str) -> None: # This will be the set of line numbers that start match or case statements. - self.match_case_lines = set() + self.match_case_lines: Set[TLineNo] = set() self.visit(ast.parse(source)) - def visit_Match(self, node): - """Invoked by ast.NodeVisitor.visit""" - self.match_case_lines.add(node.lineno) - for case in node.cases: - self.match_case_lines.add(case.pattern.lineno) - self.generic_visit(node) + if sys.version_info >= (3, 10): + def visit_Match(self, node: ast.Match) -> None: + """Invoked by ast.NodeVisitor.visit""" + self.match_case_lines.add(node.lineno) + for case in node.cases: + self.match_case_lines.add(case.pattern.lineno) + self.generic_visit(node) -@contract(source='unicode') -def source_token_lines(source): +def source_token_lines(source: str) -> TSourceTokenLines: """Generate a series of lines, one for each line in `source`. Each line is a list of pairs, each pair is a token:: @@ -95,30 +104,30 @@ If you concatenate all the token texts, and then join them with newlines, you should have your original `source` back, with two differences: - trailing whitespace is not preserved, and a final line with no newline + trailing white space is not preserved, and a final line with no newline is indistinguishable from a final line with a newline. """ ws_tokens = {token.INDENT, token.DEDENT, token.NEWLINE, tokenize.NL} - line = [] + line: List[Tuple[str, str]] = [] col = 0 - source = source.expandtabs(8).replace('\r\n', '\n') + source = source.expandtabs(8).replace("\r\n", "\n") tokgen = generate_tokens(source) if env.PYBEHAVIOR.soft_keywords: match_case_lines = MatchCaseFinder(source).match_case_lines - for ttype, ttext, (sline, scol), (_, ecol), _ in phys_tokens(tokgen): + for ttype, ttext, (sline, scol), (_, ecol), _ in _phys_tokens(tokgen): mark_start = True - for part in re.split('(\n)', ttext): - if part == '\n': + for part in re.split("(\n)", ttext): + if part == "\n": yield line line = [] col = 0 mark_end = False - elif part == '': + elif part == "": mark_end = False elif ttype in ws_tokens: mark_end = False @@ -126,22 +135,25 @@ if mark_start and scol > col: line.append(("ws", " " * (scol - col))) mark_start = False - tok_class = tokenize.tok_name.get(ttype, 'xx').lower()[:3] + tok_class = tokenize.tok_name.get(ttype, "xx").lower()[:3] if ttype == token.NAME: if keyword.iskeyword(ttext): # Hard keywords are always keywords. tok_class = "key" - elif env.PYBEHAVIOR.soft_keywords and keyword.issoftkeyword(ttext): - # Soft keywords appear at the start of the line, on lines that start - # match or case statements. - if len(line) == 0: - is_start_of_line = True - elif (len(line) == 1) and line[0][0] == "ws": - is_start_of_line = True - else: - is_start_of_line = False - if is_start_of_line and sline in match_case_lines: - tok_class = "key" + elif sys.version_info >= (3, 10): # PYVERSIONS + # Need the version_info check to keep mypy from borking + # on issoftkeyword here. + if env.PYBEHAVIOR.soft_keywords and keyword.issoftkeyword(ttext): + # Soft keywords appear at the start of the line, + # on lines that start match or case statements. + if len(line) == 0: + is_start_of_line = True + elif (len(line) == 1) and line[0][0] == "ws": + is_start_of_line = True + else: + is_start_of_line = False + if is_start_of_line and sline in match_case_lines: + tok_class = "key" line.append((tok_class, part)) mark_end = True scol = 0 @@ -163,16 +175,15 @@ actually tokenize twice. """ - def __init__(self): - self.last_text = None - self.last_tokens = None + def __init__(self) -> None: + self.last_text: Optional[str] = None + self.last_tokens: List[tokenize.TokenInfo] = [] - @contract(text='unicode') - def generate_tokens(self, text): + def generate_tokens(self, text: str) -> TokenInfos: """A stand-in for `tokenize.generate_tokens`.""" if text != self.last_text: self.last_text = text - readline = iter(text.splitlines(True)).__next__ + readline = io.StringIO(text).readline try: self.last_tokens = list(tokenize.generate_tokens(readline)) except: @@ -184,10 +195,7 @@ generate_tokens = CachedTokenizer().generate_tokens -COOKIE_RE = re.compile(r"^[ \t]*#.*coding[:=][ \t]*([-\w.]+)", flags=re.MULTILINE) - -@contract(source='bytes') -def source_encoding(source): +def source_encoding(source: bytes) -> str: """Determine the encoding for `source`, according to PEP 263. `source` is a byte string: the text of the program. @@ -197,31 +205,3 @@ """ readline = iter(source.splitlines(True)).__next__ return tokenize.detect_encoding(readline)[0] - - -@contract(source='unicode') -def compile_unicode(source, filename, mode): - """Just like the `compile` builtin, but works on any Unicode string. - - Python 2's compile() builtin has a stupid restriction: if the source string - is Unicode, then it may not have a encoding declaration in it. Why not? - Who knows! It also decodes to utf-8, and then tries to interpret those - utf-8 bytes according to the encoding declaration. Why? Who knows! - - This function neuters the coding declaration, and compiles it. - - """ - source = neuter_encoding_declaration(source) - code = compile(source, filename, mode) - return code - - -@contract(source='unicode', returns='unicode') -def neuter_encoding_declaration(source): - """Return `source`, with any encoding declaration neutered.""" - if COOKIE_RE.search(source): - source_lines = source.splitlines(True) - for lineno in range(min(2, len(source_lines))): - source_lines[lineno] = COOKIE_RE.sub("# (deleted declaration)", source_lines[lineno]) - source = "".join(source_lines) - return source diff -Nru python-coverage-6.5.0+dfsg1/coverage/plugin.py python-coverage-7.2.7+dfsg1/coverage/plugin.py --- python-coverage-6.5.0+dfsg1/coverage/plugin.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/plugin.py 2023-05-29 19:46:30.000000000 +0000 @@ -112,16 +112,25 @@ """ +from __future__ import annotations + import functools +from types import FrameType +from typing import Any, Dict, Iterable, Optional, Set, Tuple, Union + from coverage import files -from coverage.misc import contract, _needs_to_implement +from coverage.misc import _needs_to_implement +from coverage.types import TArc, TConfigurable, TLineNo, TSourceTokenLines class CoveragePlugin: """Base class for coverage.py plug-ins.""" - def file_tracer(self, filename): # pylint: disable=unused-argument + _coverage_plugin_name: str + _coverage_enabled: bool + + def file_tracer(self, filename: str) -> Optional[FileTracer]: # pylint: disable=unused-argument """Get a :class:`FileTracer` object for a file. Plug-in type: file tracer. @@ -161,7 +170,10 @@ """ return None - def file_reporter(self, filename): # pylint: disable=unused-argument + def file_reporter( # type: ignore[return] + self, + filename: str, # pylint: disable=unused-argument + ) -> Union[FileReporter, str]: # str should be Literal["python"] """Get the :class:`FileReporter` class to use for a file. Plug-in type: file tracer. @@ -175,7 +187,10 @@ """ _needs_to_implement(self, "file_reporter") - def dynamic_context(self, frame): # pylint: disable=unused-argument + def dynamic_context( + self, + frame: FrameType, # pylint: disable=unused-argument + ) -> Optional[str]: """Get the dynamically computed context label for `frame`. Plug-in type: dynamic context. @@ -191,7 +206,10 @@ """ return None - def find_executable_files(self, src_dir): # pylint: disable=unused-argument + def find_executable_files( + self, + src_dir: str, # pylint: disable=unused-argument + ) -> Iterable[str]: """Yield all of the executable files in `src_dir`, recursively. Plug-in type: file tracer. @@ -206,7 +224,7 @@ """ return [] - def configure(self, config): + def configure(self, config: TConfigurable) -> None: """Modify the configuration of coverage.py. Plug-in type: configurer. @@ -220,7 +238,7 @@ """ pass - def sys_info(self): + def sys_info(self) -> Iterable[Tuple[str, Any]]: """Get a list of information useful for debugging. Plug-in type: any. @@ -234,7 +252,12 @@ return [] -class FileTracer: +class CoveragePluginBase: + """Plugins produce specialized objects, which point back to the original plugin.""" + _coverage_plugin: CoveragePlugin + + +class FileTracer(CoveragePluginBase): """Support needed for files during the execution phase. File tracer plug-ins implement subclasses of FileTracer to return from @@ -251,7 +274,7 @@ """ - def source_filename(self): + def source_filename(self) -> str: # type: ignore[return] """The source file name for this file. This may be any file name you like. A key responsibility of a plug-in @@ -266,7 +289,7 @@ """ _needs_to_implement(self, "source_filename") - def has_dynamic_source_filename(self): + def has_dynamic_source_filename(self) -> bool: """Does this FileTracer have dynamic source file names? FileTracers can provide dynamically determined file names by @@ -284,7 +307,11 @@ """ return False - def dynamic_source_filename(self, filename, frame): # pylint: disable=unused-argument + def dynamic_source_filename( + self, + filename: str, # pylint: disable=unused-argument + frame: FrameType, # pylint: disable=unused-argument + ) -> Optional[str]: """Get a dynamically computed source file name. Some plug-ins need to compute the source file name dynamically for each @@ -299,7 +326,7 @@ """ return None - def line_number_range(self, frame): + def line_number_range(self, frame: FrameType) -> Tuple[TLineNo, TLineNo]: """Get the range of source line numbers for a given a call frame. The call frame is examined, and the source line number in the original @@ -318,7 +345,7 @@ @functools.total_ordering -class FileReporter: +class FileReporter(CoveragePluginBase): """Support needed for files during the analysis and reporting phases. File tracer plug-ins implement a subclass of `FileReporter`, and return @@ -331,7 +358,7 @@ """ - def __init__(self, filename): + def __init__(self, filename: str) -> None: """Simple initialization of a `FileReporter`. The `filename` argument is the path to the file being reported. This @@ -341,10 +368,10 @@ """ self.filename = filename - def __repr__(self): + def __repr__(self) -> str: return "<{0.__class__.__name__} filename={0.filename!r}>".format(self) - def relative_filename(self): + def relative_filename(self) -> str: """Get the relative file name for this file. This file path will be displayed in reports. The default @@ -355,8 +382,7 @@ """ return files.relative_filename(self.filename) - @contract(returns='unicode') - def source(self): + def source(self) -> str: """Get the source for the file. Returns a Unicode string. @@ -366,10 +392,10 @@ as a text file, or if you need other encoding support. """ - with open(self.filename, "rb") as f: - return f.read().decode("utf-8") + with open(self.filename, encoding="utf-8") as f: + return f.read() - def lines(self): + def lines(self) -> Set[TLineNo]: # type: ignore[return] """Get the executable lines in this file. Your plug-in must determine which lines in the file were possibly @@ -380,7 +406,7 @@ """ _needs_to_implement(self, "lines") - def excluded_lines(self): + def excluded_lines(self) -> Set[TLineNo]: """Get the excluded executable lines in this file. Your plug-in can use any method it likes to allow the user to exclude @@ -393,7 +419,7 @@ """ return set() - def translate_lines(self, lines): + def translate_lines(self, lines: Iterable[TLineNo]) -> Set[TLineNo]: """Translate recorded lines into reported lines. Some file formats will want to report lines slightly differently than @@ -413,7 +439,7 @@ """ return set(lines) - def arcs(self): + def arcs(self) -> Set[TArc]: """Get the executable arcs in this file. To support branch coverage, your plug-in needs to be able to indicate @@ -427,7 +453,7 @@ """ return set() - def no_branch_lines(self): + def no_branch_lines(self) -> Set[TLineNo]: """Get the lines excused from branch coverage in this file. Your plug-in can use any method it likes to allow the user to exclude @@ -440,7 +466,7 @@ """ return set() - def translate_arcs(self, arcs): + def translate_arcs(self, arcs: Iterable[TArc]) -> Set[TArc]: """Translate recorded arcs into reported arcs. Similar to :meth:`translate_lines`, but for arcs. `arcs` is a set of @@ -451,9 +477,9 @@ The default implementation returns `arcs` unchanged. """ - return arcs + return set(arcs) - def exit_counts(self): + def exit_counts(self) -> Dict[TLineNo, int]: """Get a count of exits from that each line. To determine which lines are branches, coverage.py looks for lines that @@ -466,7 +492,12 @@ """ return {} - def missing_arc_description(self, start, end, executed_arcs=None): # pylint: disable=unused-argument + def missing_arc_description( + self, + start: TLineNo, + end: TLineNo, + executed_arcs: Optional[Iterable[TArc]] = None, # pylint: disable=unused-argument + ) -> str: """Provide an English sentence describing a missing arc. The `start` and `end` arguments are the line numbers of the missing @@ -481,41 +512,42 @@ """ return f"Line {start} didn't jump to line {end}" - def source_token_lines(self): + def source_token_lines(self) -> TSourceTokenLines: """Generate a series of tokenized lines, one for each line in `source`. These tokens are used for syntax-colored reports. Each line is a list of pairs, each pair is a token:: - [('key', 'def'), ('ws', ' '), ('nam', 'hello'), ('op', '('), ... ] + [("key", "def"), ("ws", " "), ("nam", "hello"), ("op", "("), ... ] Each pair has a token class, and the token text. The token classes are: - * ``'com'``: a comment - * ``'key'``: a keyword - * ``'nam'``: a name, or identifier - * ``'num'``: a number - * ``'op'``: an operator - * ``'str'``: a string literal - * ``'ws'``: some white space - * ``'txt'``: some other kind of text + * ``"com"``: a comment + * ``"key"``: a keyword + * ``"nam"``: a name, or identifier + * ``"num"``: a number + * ``"op"``: an operator + * ``"str"``: a string literal + * ``"ws"``: some white space + * ``"txt"``: some other kind of text If you concatenate all the token texts, and then join them with newlines, you should have your original source back. The default implementation simply returns each line tagged as - ``'txt'``. + ``"txt"``. """ for line in self.source().splitlines(): - yield [('txt', line)] + yield [("txt", line)] - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: return isinstance(other, FileReporter) and self.filename == other.filename - def __lt__(self, other): + def __lt__(self, other: Any) -> bool: return isinstance(other, FileReporter) and self.filename < other.filename - __hash__ = None # This object doesn't need to be hashed. + # This object doesn't need to be hashed. + __hash__ = None # type: ignore[assignment] diff -Nru python-coverage-6.5.0+dfsg1/coverage/plugin_support.py python-coverage-7.2.7+dfsg1/coverage/plugin_support.py --- python-coverage-6.5.0+dfsg1/coverage/plugin_support.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/plugin_support.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,13 +3,21 @@ """Support for plugins.""" +from __future__ import annotations + import os import os.path import sys +from types import FrameType +from typing import Any, Dict, Iterable, Iterator, List, Optional, Set, Tuple, Union + from coverage.exceptions import PluginError from coverage.misc import isolate_module from coverage.plugin import CoveragePlugin, FileTracer, FileReporter +from coverage.types import ( + TArc, TConfigurable, TDebugCtl, TLineNo, TPluginConfig, TSourceTokenLines, +) os = isolate_module(os) @@ -17,18 +25,23 @@ class Plugins: """The currently loaded collection of coverage.py plugins.""" - def __init__(self): - self.order = [] - self.names = {} - self.file_tracers = [] - self.configurers = [] - self.context_switchers = [] + def __init__(self) -> None: + self.order: List[CoveragePlugin] = [] + self.names: Dict[str, CoveragePlugin] = {} + self.file_tracers: List[CoveragePlugin] = [] + self.configurers: List[CoveragePlugin] = [] + self.context_switchers: List[CoveragePlugin] = [] - self.current_module = None - self.debug = None + self.current_module: Optional[str] = None + self.debug: Optional[TDebugCtl] @classmethod - def load_plugins(cls, modules, config, debug=None): + def load_plugins( + cls, + modules: Iterable[str], + config: TPluginConfig, + debug: Optional[TDebugCtl] = None, + ) -> Plugins: """Load plugins from `modules`. Returns a Plugins object with the loaded and configured plugins. @@ -54,7 +67,7 @@ plugins.current_module = None return plugins - def add_file_tracer(self, plugin): + def add_file_tracer(self, plugin: CoveragePlugin) -> None: """Add a file tracer plugin. `plugin` is an instance of a third-party plugin class. It must @@ -63,7 +76,7 @@ """ self._add_plugin(plugin, self.file_tracers) - def add_configurer(self, plugin): + def add_configurer(self, plugin: CoveragePlugin) -> None: """Add a configuring plugin. `plugin` is an instance of a third-party plugin class. It must @@ -72,7 +85,7 @@ """ self._add_plugin(plugin, self.configurers) - def add_dynamic_context(self, plugin): + def add_dynamic_context(self, plugin: CoveragePlugin) -> None: """Add a dynamic context plugin. `plugin` is an instance of a third-party plugin class. It must @@ -81,7 +94,7 @@ """ self._add_plugin(plugin, self.context_switchers) - def add_noop(self, plugin): + def add_noop(self, plugin: CoveragePlugin) -> None: """Add a plugin that does nothing. This is only useful for testing the plugin support. @@ -89,7 +102,11 @@ """ self._add_plugin(plugin, None) - def _add_plugin(self, plugin, specialized): + def _add_plugin( + self, + plugin: CoveragePlugin, + specialized: Optional[List[CoveragePlugin]], + ) -> None: """Add a plugin object. `plugin` is a :class:`CoveragePlugin` instance to add. `specialized` @@ -97,12 +114,11 @@ """ plugin_name = f"{self.current_module}.{plugin.__class__.__name__}" - if self.debug and self.debug.should('plugin'): + if self.debug and self.debug.should("plugin"): self.debug.write(f"Loaded plugin {self.current_module!r}: {plugin!r}") labelled = LabelledDebug(f"plugin {self.current_module!r}", self.debug) plugin = DebugPluginWrapper(plugin, labelled) - # pylint: disable=attribute-defined-outside-init plugin._coverage_plugin_name = plugin_name plugin._coverage_enabled = True self.order.append(plugin) @@ -110,13 +126,13 @@ if specialized is not None: specialized.append(plugin) - def __bool__(self): + def __bool__(self) -> bool: return bool(self.order) - def __iter__(self): + def __iter__(self) -> Iterator[CoveragePlugin]: return iter(self.order) - def get(self, plugin_name): + def get(self, plugin_name: str) -> CoveragePlugin: """Return a plugin by name.""" return self.names[plugin_name] @@ -124,20 +140,20 @@ class LabelledDebug: """A Debug writer, but with labels for prepending to the messages.""" - def __init__(self, label, debug, prev_labels=()): + def __init__(self, label: str, debug: TDebugCtl, prev_labels: Iterable[str] = ()): self.labels = list(prev_labels) + [label] self.debug = debug - def add_label(self, label): + def add_label(self, label: str) -> LabelledDebug: """Add a label to the writer, and return a new `LabelledDebug`.""" return LabelledDebug(label, self.debug, self.labels) - def message_prefix(self): + def message_prefix(self) -> str: """The prefix to use on messages, combining the labels.""" - prefixes = self.labels + [''] + prefixes = self.labels + [""] return ":\n".join(" "*i+label for i, label in enumerate(prefixes)) - def write(self, message): + def write(self, message: str) -> None: """Write `message`, but with the labels prepended.""" self.debug.write(f"{self.message_prefix()}{message}") @@ -145,12 +161,12 @@ class DebugPluginWrapper(CoveragePlugin): """Wrap a plugin, and use debug to report on what it's doing.""" - def __init__(self, plugin, debug): + def __init__(self, plugin: CoveragePlugin, debug: LabelledDebug) -> None: super().__init__() self.plugin = plugin self.debug = debug - def file_tracer(self, filename): + def file_tracer(self, filename: str) -> Optional[FileTracer]: tracer = self.plugin.file_tracer(filename) self.debug.write(f"file_tracer({filename!r}) --> {tracer!r}") if tracer: @@ -158,64 +174,65 @@ tracer = DebugFileTracerWrapper(tracer, debug) return tracer - def file_reporter(self, filename): + def file_reporter(self, filename: str) -> Union[FileReporter, str]: reporter = self.plugin.file_reporter(filename) + assert isinstance(reporter, FileReporter) self.debug.write(f"file_reporter({filename!r}) --> {reporter!r}") if reporter: debug = self.debug.add_label(f"file {filename!r}") reporter = DebugFileReporterWrapper(filename, reporter, debug) return reporter - def dynamic_context(self, frame): + def dynamic_context(self, frame: FrameType) -> Optional[str]: context = self.plugin.dynamic_context(frame) self.debug.write(f"dynamic_context({frame!r}) --> {context!r}") return context - def find_executable_files(self, src_dir): + def find_executable_files(self, src_dir: str) -> Iterable[str]: executable_files = self.plugin.find_executable_files(src_dir) self.debug.write(f"find_executable_files({src_dir!r}) --> {executable_files!r}") return executable_files - def configure(self, config): + def configure(self, config: TConfigurable) -> None: self.debug.write(f"configure({config!r})") self.plugin.configure(config) - def sys_info(self): + def sys_info(self) -> Iterable[Tuple[str, Any]]: return self.plugin.sys_info() class DebugFileTracerWrapper(FileTracer): """A debugging `FileTracer`.""" - def __init__(self, tracer, debug): + def __init__(self, tracer: FileTracer, debug: LabelledDebug) -> None: self.tracer = tracer self.debug = debug - def _show_frame(self, frame): + def _show_frame(self, frame: FrameType) -> str: """A short string identifying a frame, for debug messages.""" return "%s@%d" % ( os.path.basename(frame.f_code.co_filename), frame.f_lineno, ) - def source_filename(self): + def source_filename(self) -> str: sfilename = self.tracer.source_filename() self.debug.write(f"source_filename() --> {sfilename!r}") return sfilename - def has_dynamic_source_filename(self): + def has_dynamic_source_filename(self) -> bool: has = self.tracer.has_dynamic_source_filename() self.debug.write(f"has_dynamic_source_filename() --> {has!r}") return has - def dynamic_source_filename(self, filename, frame): + def dynamic_source_filename(self, filename: str, frame: FrameType) -> Optional[str]: dyn = self.tracer.dynamic_source_filename(filename, frame) self.debug.write("dynamic_source_filename({!r}, {}) --> {!r}".format( filename, self._show_frame(frame), dyn, )) return dyn - def line_number_range(self, frame): + def line_number_range(self, frame: FrameType) -> Tuple[TLineNo, TLineNo]: pair = self.tracer.line_number_range(frame) self.debug.write(f"line_number_range({self._show_frame(frame)}) --> {pair!r}") return pair @@ -224,57 +241,57 @@ class DebugFileReporterWrapper(FileReporter): """A debugging `FileReporter`.""" - def __init__(self, filename, reporter, debug): + def __init__(self, filename: str, reporter: FileReporter, debug: LabelledDebug) -> None: super().__init__(filename) self.reporter = reporter self.debug = debug - def relative_filename(self): + def relative_filename(self) -> str: ret = self.reporter.relative_filename() self.debug.write(f"relative_filename() --> {ret!r}") return ret - def lines(self): + def lines(self) -> Set[TLineNo]: ret = self.reporter.lines() self.debug.write(f"lines() --> {ret!r}") return ret - def excluded_lines(self): + def excluded_lines(self) -> Set[TLineNo]: ret = self.reporter.excluded_lines() self.debug.write(f"excluded_lines() --> {ret!r}") return ret - def translate_lines(self, lines): + def translate_lines(self, lines: Iterable[TLineNo]) -> Set[TLineNo]: ret = self.reporter.translate_lines(lines) self.debug.write(f"translate_lines({lines!r}) --> {ret!r}") return ret - def translate_arcs(self, arcs): + def translate_arcs(self, arcs: Iterable[TArc]) -> Set[TArc]: ret = self.reporter.translate_arcs(arcs) self.debug.write(f"translate_arcs({arcs!r}) --> {ret!r}") return ret - def no_branch_lines(self): + def no_branch_lines(self) -> Set[TLineNo]: ret = self.reporter.no_branch_lines() self.debug.write(f"no_branch_lines() --> {ret!r}") return ret - def exit_counts(self): + def exit_counts(self) -> Dict[TLineNo, int]: ret = self.reporter.exit_counts() self.debug.write(f"exit_counts() --> {ret!r}") return ret - def arcs(self): + def arcs(self) -> Set[TArc]: ret = self.reporter.arcs() self.debug.write(f"arcs() --> {ret!r}") return ret - def source(self): + def source(self) -> str: ret = self.reporter.source() self.debug.write("source() --> %d chars" % (len(ret),)) return ret - def source_token_lines(self): + def source_token_lines(self) -> TSourceTokenLines: ret = list(self.reporter.source_token_lines()) self.debug.write("source_token_lines() --> %d tokens" % (len(ret),)) return ret diff -Nru python-coverage-6.5.0+dfsg1/coverage/python.py python-coverage-7.2.7+dfsg1/coverage/python.py --- python-coverage-6.5.0+dfsg1/coverage/python.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/python.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,23 +3,30 @@ """Python source expertise for coverage.py""" +from __future__ import annotations + import os.path import types import zipimport +from typing import Dict, Iterable, Optional, Set, TYPE_CHECKING + from coverage import env from coverage.exceptions import CoverageException, NoSource -from coverage.files import canonical_filename, relative_filename -from coverage.misc import contract, expensive, isolate_module, join_regex +from coverage.files import canonical_filename, relative_filename, zip_location +from coverage.misc import expensive, isolate_module, join_regex from coverage.parser import PythonParser from coverage.phystokens import source_token_lines, source_encoding from coverage.plugin import FileReporter +from coverage.types import TArc, TLineNo, TMorf, TSourceTokenLines + +if TYPE_CHECKING: + from coverage import Coverage os = isolate_module(os) -@contract(returns='bytes') -def read_python_source(filename): +def read_python_source(filename: str) -> bytes: """Read the Python source text from `filename`. Returns bytes. @@ -28,15 +35,10 @@ with open(filename, "rb") as f: source = f.read() - if env.IRONPYTHON: - # IronPython reads Unicode strings even for "rb" files. - source = bytes(source) - return source.replace(b"\r\n", b"\n").replace(b"\r", b"\n") -@contract(returns='unicode') -def get_python_source(filename): +def get_python_source(filename: str) -> str: """Return the source code, as unicode.""" base, ext = os.path.splitext(filename) if ext == ".py" and env.WINDOWS: @@ -44,34 +46,34 @@ else: exts = [ext] + source_bytes: Optional[bytes] for ext in exts: try_filename = base + ext if os.path.exists(try_filename): # A regular text file: open it. - source = read_python_source(try_filename) + source_bytes = read_python_source(try_filename) break # Maybe it's in a zip file? - source = get_zip_bytes(try_filename) - if source is not None: + source_bytes = get_zip_bytes(try_filename) + if source_bytes is not None: break else: # Couldn't find source. raise NoSource(f"No source for code: '{filename}'.") # Replace \f because of http://bugs.python.org/issue19035 - source = source.replace(b'\f', b' ') - source = source.decode(source_encoding(source), "replace") + source_bytes = source_bytes.replace(b"\f", b" ") + source = source_bytes.decode(source_encoding(source_bytes), "replace") # Python code should always end with a line with a newline. - if source and source[-1] != '\n': - source += '\n' + if source and source[-1] != "\n": + source += "\n" return source -@contract(returns='bytes|None') -def get_zip_bytes(filename): +def get_zip_bytes(filename: str) -> Optional[bytes]: """Get data from `filename` if it is a zip file path. Returns the bytestring data read from the zip file, or None if no zip file @@ -79,23 +81,22 @@ an empty string if the file is empty. """ - markers = ['.zip'+os.sep, '.egg'+os.sep, '.pex'+os.sep] - for marker in markers: - if marker in filename: - parts = filename.split(marker) - try: - zi = zipimport.zipimporter(parts[0]+marker[:-1]) - except zipimport.ZipImportError: - continue - try: - data = zi.get_data(parts[1]) - except OSError: - continue - return data + zipfile_inner = zip_location(filename) + if zipfile_inner is not None: + zipfile, inner = zipfile_inner + try: + zi = zipimport.zipimporter(zipfile) + except zipimport.ZipImportError: + return None + try: + data = zi.get_data(inner) + except OSError: + return None + return data return None -def source_for_file(filename): +def source_for_file(filename: str) -> str: """Return the source filename for `filename`. Given a file name being traced, return the best guess as to the source @@ -120,17 +121,13 @@ # Didn't find source, but it's probably the .py file we want. return py_filename - elif filename.endswith("$py.class"): - # Jython is easy to guess. - return filename[:-9] + ".py" - # No idea, just use the file name as-is. return filename -def source_for_morf(morf): +def source_for_morf(morf: TMorf) -> str: """Get the source filename for the module-or-file `morf`.""" - if hasattr(morf, '__file__') and morf.__file__: + if hasattr(morf, "__file__") and morf.__file__: filename = morf.__file__ elif isinstance(morf, types.ModuleType): # A module should have had .__file__, otherwise we can't use it. @@ -146,60 +143,68 @@ class PythonFileReporter(FileReporter): """Report support for a Python file.""" - def __init__(self, morf, coverage=None): + def __init__(self, morf: TMorf, coverage: Optional[Coverage] = None) -> None: self.coverage = coverage filename = source_for_morf(morf) - super().__init__(canonical_filename(filename)) + fname = filename + canonicalize = True + if self.coverage is not None: + if self.coverage.config.relative_files: + canonicalize = False + if canonicalize: + fname = canonical_filename(filename) + super().__init__(fname) - if hasattr(morf, '__name__'): + if hasattr(morf, "__name__"): name = morf.__name__.replace(".", os.sep) - if os.path.basename(filename).startswith('__init__.'): + if os.path.basename(filename).startswith("__init__."): name += os.sep + "__init__" name += ".py" else: name = relative_filename(filename) self.relname = name - self._source = None - self._parser = None + self._source: Optional[str] = None + self._parser: Optional[PythonParser] = None self._excluded = None - def __repr__(self): + def __repr__(self) -> str: return f"" - @contract(returns='unicode') - def relative_filename(self): + def relative_filename(self) -> str: return self.relname @property - def parser(self): + def parser(self) -> PythonParser: """Lazily create a :class:`PythonParser`.""" + assert self.coverage is not None if self._parser is None: self._parser = PythonParser( filename=self.filename, - exclude=self.coverage._exclude_regex('exclude'), + exclude=self.coverage._exclude_regex("exclude"), ) self._parser.parse_source() return self._parser - def lines(self): + def lines(self) -> Set[TLineNo]: """Return the line numbers of statements in the file.""" return self.parser.statements - def excluded_lines(self): + def excluded_lines(self) -> Set[TLineNo]: """Return the line numbers of statements in the file.""" return self.parser.excluded - def translate_lines(self, lines): + def translate_lines(self, lines: Iterable[TLineNo]) -> Set[TLineNo]: return self.parser.translate_lines(lines) - def translate_arcs(self, arcs): + def translate_arcs(self, arcs: Iterable[TArc]) -> Set[TArc]: return self.parser.translate_arcs(arcs) @expensive - def no_branch_lines(self): + def no_branch_lines(self) -> Set[TLineNo]: + assert self.coverage is not None no_branch = self.parser.lines_matching( join_regex(self.coverage.config.partial_list), join_regex(self.coverage.config.partial_always_list), @@ -207,23 +212,27 @@ return no_branch @expensive - def arcs(self): + def arcs(self) -> Set[TArc]: return self.parser.arcs() @expensive - def exit_counts(self): + def exit_counts(self) -> Dict[TLineNo, int]: return self.parser.exit_counts() - def missing_arc_description(self, start, end, executed_arcs=None): + def missing_arc_description( + self, + start: TLineNo, + end: TLineNo, + executed_arcs: Optional[Iterable[TArc]] = None, + ) -> str: return self.parser.missing_arc_description(start, end, executed_arcs) - @contract(returns='unicode') - def source(self): + def source(self) -> str: if self._source is None: self._source = get_python_source(self.filename) return self._source - def should_be_python(self): + def should_be_python(self) -> bool: """Does it seem like this file should contain Python? This is used to decide if a file reported as part of the execution of @@ -235,7 +244,7 @@ _, ext = os.path.splitext(self.filename) # Anything named *.py* should be Python. - if ext.startswith('.py'): + if ext.startswith(".py"): return True # A file with no extension should be Python. if not ext: @@ -243,5 +252,5 @@ # Everything else is probably not Python. return False - def source_token_lines(self): + def source_token_lines(self) -> TSourceTokenLines: return source_token_lines(self.source()) diff -Nru python-coverage-6.5.0+dfsg1/coverage/pytracer.py python-coverage-7.2.7+dfsg1/coverage/pytracer.py --- python-coverage-6.5.0+dfsg1/coverage/pytracer.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/pytracer.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,18 +3,28 @@ """Raw data collector for coverage.py.""" +from __future__ import annotations + import atexit import dis import sys +import threading + +from types import FrameType, ModuleType +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, cast from coverage import env +from coverage.types import ( + TArc, TFileDisposition, TLineNo, TTraceData, TTraceFileData, TTraceFn, + TTracer, TWarnFn, +) # We need the YIELD_VALUE opcode below, in a comparison-friendly form. -RESUME = dis.opmap.get('RESUME') -RETURN_VALUE = dis.opmap['RETURN_VALUE'] +RESUME = dis.opmap.get("RESUME") +RETURN_VALUE = dis.opmap["RETURN_VALUE"] if RESUME is None: - YIELD_VALUE = dis.opmap['YIELD_VALUE'] - YIELD_FROM = dis.opmap['YIELD_FROM'] + YIELD_VALUE = dis.opmap["YIELD_VALUE"] + YIELD_FROM = dis.opmap["YIELD_FROM"] YIELD_FROM_OFFSET = 0 if env.PYPY else 2 # When running meta-coverage, this file can try to trace itself, which confuses @@ -22,7 +32,7 @@ THIS_FILE = __file__.rstrip("co") -class PyTracer: +class PyTracer(TTracer): """Python implementation of the raw data tracer.""" # Because of poor implementations of trace-function-manipulating tools, @@ -41,44 +51,46 @@ # PyTracer to get accurate results. The command-line --timid argument is # used to force the use of this tracer. - def __init__(self): + def __init__(self) -> None: + # pylint: disable=super-init-not-called # Attributes set from the collector: - self.data = None + self.data: TTraceData self.trace_arcs = False - self.should_trace = None - self.should_trace_cache = None - self.should_start_context = None - self.warn = None + self.should_trace: Callable[[str, FrameType], TFileDisposition] + self.should_trace_cache: Dict[str, Optional[TFileDisposition]] + self.should_start_context: Optional[Callable[[FrameType], Optional[str]]] = None + self.switch_context: Optional[Callable[[Optional[str]], None]] = None + self.warn: TWarnFn + # The threading module to use, if any. - self.threading = None + self.threading: Optional[ModuleType] = None - self.cur_file_data = None - self.last_line = 0 # int, but uninitialized. - self.cur_file_name = None - self.context = None + self.cur_file_data: Optional[TTraceFileData] = None + self.last_line: TLineNo = 0 + self.cur_file_name: Optional[str] = None + self.context: Optional[str] = None self.started_context = False - self.data_stack = [] - self.thread = None + self.data_stack: List[Tuple[Optional[TTraceFileData], Optional[str], TLineNo, bool]] = [] + self.thread: Optional[threading.Thread] = None self.stopped = False self._activity = False self.in_atexit = False # On exit, self.in_atexit = True - atexit.register(setattr, self, 'in_atexit', True) + atexit.register(setattr, self, "in_atexit", True) # Cache a bound method on the instance, so that we don't have to # re-create a bound method object all the time. - self._cached_bound_method_trace = self._trace + self._cached_bound_method_trace: TTraceFn = self._trace - def __repr__(self): - return "".format( - id(self), - sum(len(v) for v in self.data.values()), - len(self.data), - ) + def __repr__(self) -> str: + me = id(self) + points = sum(len(v) for v in self.data.values()) + files = len(self.data) + return f"" - def log(self, marker, *args): + def log(self, marker: str, *args: Any) -> None: """For hard-core logging of what this tracer is doing.""" with open("/tmp/debug_trace.txt", "a") as f: f.write("{} {}[{}]".format( @@ -87,13 +99,13 @@ len(self.data_stack), )) if 0: # if you want thread ids.. - f.write(".{:x}.{:x}".format( + f.write(".{:x}.{:x}".format( # type: ignore[unreachable] self.thread.ident, self.threading.current_thread().ident, )) f.write(" {}".format(" ".join(map(str, args)))) if 0: # if you want callers.. - f.write(" | ") + f.write(" | ") # type: ignore[unreachable] stack = " / ".join( (fname or "???").rpartition("/")[-1] for _, fname, _, _ in self.data_stack @@ -101,7 +113,13 @@ f.write(stack) f.write("\n") - def _trace(self, frame, event, arg_unused): + def _trace( + self, + frame: FrameType, + event: str, + arg: Any, # pylint: disable=unused-argument + lineno: Optional[TLineNo] = None, # pylint: disable=unused-argument + ) -> Optional[TTraceFn]: """The trace function passed to sys.settrace.""" if THIS_FILE in frame.f_code.co_filename: @@ -113,27 +131,36 @@ # The PyTrace.stop() method has been called, possibly by another # thread, let's deactivate ourselves now. if 0: - self.log("---\nX", frame.f_code.co_filename, frame.f_lineno) - f = frame + f = frame # type: ignore[unreachable] + self.log("---\nX", f.f_code.co_filename, f.f_lineno) while f: self.log(">", f.f_code.co_filename, f.f_lineno, f.f_code.co_name, f.f_trace) f = f.f_back sys.settrace(None) - self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = ( - self.data_stack.pop() - ) + try: + self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = ( + self.data_stack.pop() + ) + except IndexError: + self.log( + "Empty stack!", + frame.f_code.co_filename, + frame.f_lineno, + frame.f_code.co_name + ) return None - # if event != 'call' and frame.f_code.co_filename != self.cur_file_name: + # if event != "call" and frame.f_code.co_filename != self.cur_file_name: # self.log("---\n*", frame.f_code.co_filename, self.cur_file_name, frame.f_lineno) - if event == 'call': + if event == "call": # Should we start a new context? if self.should_start_context and self.context is None: context_maybe = self.should_start_context(frame) if context_maybe is not None: self.context = context_maybe started_context = True + assert self.switch_context is not None self.switch_context(self.context) else: started_context = False @@ -155,7 +182,7 @@ # Improve tracing performance: when calling a function, both caller # and callee are often within the same file. if that's the case, we # don't have to re-check whether to trace the corresponding - # function (which is a little bit espensive since it involves + # function (which is a little bit expensive since it involves # dictionary lookups). This optimization is only correct if we # didn't start a context. filename = frame.f_code.co_filename @@ -169,8 +196,9 @@ self.cur_file_data = None if disp.trace: tracename = disp.source_filename + assert tracename is not None if tracename not in self.data: - self.data[tracename] = set() + self.data[tracename] = set() # type: ignore[assignment] self.cur_file_data = self.data[tracename] else: frame.f_trace_lines = False @@ -187,24 +215,24 @@ oparg = frame.f_code.co_code[frame.f_lasti + 1] real_call = (oparg == 0) else: - real_call = (getattr(frame, 'f_lasti', -1) < 0) + real_call = (getattr(frame, "f_lasti", -1) < 0) if real_call: self.last_line = -frame.f_code.co_firstlineno else: self.last_line = frame.f_lineno - elif event == 'line': + elif event == "line": # Record an executed line. if self.cur_file_data is not None: - lineno = frame.f_lineno + flineno: TLineNo = frame.f_lineno if self.trace_arcs: - self.cur_file_data.add((self.last_line, lineno)) + cast(Set[TArc], self.cur_file_data).add((self.last_line, flineno)) else: - self.cur_file_data.add(lineno) - self.last_line = lineno + cast(Set[TLineNo], self.cur_file_data).add(flineno) + self.last_line = flineno - elif event == 'return': + elif event == "return": if self.trace_arcs and self.cur_file_data: # Record an arc leaving the function, but beware that a # "return" event might just mean yielding from a generator. @@ -230,7 +258,7 @@ real_return = True if real_return: first = frame.f_code.co_firstlineno - self.cur_file_data.add((self.last_line, -first)) + cast(Set[TArc], self.cur_file_data).add((self.last_line, -first)) # Leaving this function, pop the filename stack. self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = ( @@ -238,11 +266,12 @@ ) # Leaving a context? if self.started_context: + assert self.switch_context is not None self.context = None self.switch_context(None) return self._cached_bound_method_trace - def start(self): + def start(self) -> TTraceFn: """Start this Tracer. Return a Python function suitable for use with sys.settrace(). @@ -263,7 +292,7 @@ sys.settrace(self._cached_bound_method_trace) return self._cached_bound_method_trace - def stop(self): + def stop(self) -> None: """Stop this Tracer.""" # Get the active tracer callback before setting the stop flag to be # able to detect if the tracer was changed prior to stopping it. @@ -274,12 +303,14 @@ # right thread. self.stopped = True - if self.threading and self.thread.ident != self.threading.current_thread().ident: - # Called on a different thread than started us: we can't unhook - # ourselves, but we've set the flag that we should stop, so we - # won't do any more tracing. - #self.log("~", "stopping on different threads") - return + if self.threading: + assert self.thread is not None + if self.thread.ident != self.threading.current_thread().ident: + # Called on a different thread than started us: we can't unhook + # ourselves, but we've set the flag that we should stop, so we + # won't do any more tracing. + #self.log("~", "stopping on different threads") + return if self.warn: # PyPy clears the trace function before running atexit functions, @@ -293,14 +324,14 @@ slug="trace-changed", ) - def activity(self): + def activity(self) -> bool: """Has there been any activity?""" return self._activity - def reset_activity(self): + def reset_activity(self) -> None: """Reset the activity() flag.""" self._activity = False - def get_stats(self): + def get_stats(self) -> Optional[Dict[str, int]]: """Return a dictionary of statistics, or None.""" return None diff -Nru python-coverage-6.5.0+dfsg1/coverage/py.typed python-coverage-7.2.7+dfsg1/coverage/py.typed --- python-coverage-6.5.0+dfsg1/coverage/py.typed 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/py.typed 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1 @@ +# Marker file for PEP 561 to indicate that this package has type hints. diff -Nru python-coverage-6.5.0+dfsg1/coverage/report_core.py python-coverage-7.2.7+dfsg1/coverage/report_core.py --- python-coverage-6.5.0+dfsg1/coverage/report_core.py 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/report_core.py 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,117 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Reporter foundation for coverage.py.""" + +from __future__ import annotations + +import sys + +from typing import Callable, Iterable, Iterator, IO, Optional, Tuple, TYPE_CHECKING + +from coverage.exceptions import NoDataError, NotPython +from coverage.files import prep_patterns, GlobMatcher +from coverage.misc import ensure_dir_for_file, file_be_gone +from coverage.plugin import FileReporter +from coverage.results import Analysis +from coverage.types import Protocol, TMorf + +if TYPE_CHECKING: + from coverage import Coverage + + +class Reporter(Protocol): + """What we expect of reporters.""" + + report_type: str + + def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float: + """Generate a report of `morfs`, written to `outfile`.""" + + +def render_report( + output_path: str, + reporter: Reporter, + morfs: Optional[Iterable[TMorf]], + msgfn: Callable[[str], None], +) -> float: + """Run a one-file report generator, managing the output file. + + This function ensures the output file is ready to be written to. Then writes + the report to it. Then closes the file and cleans up. + + """ + file_to_close = None + delete_file = False + + if output_path == "-": + outfile = sys.stdout + else: + # Ensure that the output directory is created; done here because this + # report pre-opens the output file. HtmlReporter does this on its own + # because its task is more complex, being multiple files. + ensure_dir_for_file(output_path) + outfile = open(output_path, "w", encoding="utf-8") + file_to_close = outfile + delete_file = True + + try: + ret = reporter.report(morfs, outfile=outfile) + if file_to_close is not None: + msgfn(f"Wrote {reporter.report_type} to {output_path}") + delete_file = False + return ret + finally: + if file_to_close is not None: + file_to_close.close() + if delete_file: + file_be_gone(output_path) # pragma: part covered (doesn't return) + + +def get_analysis_to_report( + coverage: Coverage, + morfs: Optional[Iterable[TMorf]], +) -> Iterator[Tuple[FileReporter, Analysis]]: + """Get the files to report on. + + For each morf in `morfs`, if it should be reported on (based on the omit + and include configuration options), yield a pair, the `FileReporter` and + `Analysis` for the morf. + + """ + file_reporters = coverage._get_file_reporters(morfs) + config = coverage.config + + if config.report_include: + matcher = GlobMatcher(prep_patterns(config.report_include), "report_include") + file_reporters = [fr for fr in file_reporters if matcher.match(fr.filename)] + + if config.report_omit: + matcher = GlobMatcher(prep_patterns(config.report_omit), "report_omit") + file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)] + + if not file_reporters: + raise NoDataError("No data to report.") + + for fr in sorted(file_reporters): + try: + analysis = coverage._analyze(fr) + except NotPython: + # Only report errors for .py files, and only if we didn't + # explicitly suppress those errors. + # NotPython is only raised by PythonFileReporter, which has a + # should_be_python() method. + if fr.should_be_python(): # type: ignore[attr-defined] + if config.ignore_errors: + msg = f"Couldn't parse Python file '{fr.filename}'" + coverage._warn(msg, slug="couldnt-parse") + else: + raise + except Exception as exc: + if config.ignore_errors: + msg = f"Couldn't parse '{fr.filename}': {exc}".rstrip() + coverage._warn(msg, slug="couldnt-parse") + else: + raise + else: + yield (fr, analysis) diff -Nru python-coverage-6.5.0+dfsg1/coverage/report.py python-coverage-7.2.7+dfsg1/coverage/report.py --- python-coverage-6.5.0+dfsg1/coverage/report.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/report.py 2023-05-29 19:46:30.000000000 +0000 @@ -1,91 +1,281 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt -"""Reporter foundation for coverage.py.""" +"""Summary reporting""" + +from __future__ import annotations import sys -from coverage.exceptions import CoverageException, NoDataError, NotPython -from coverage.files import prep_patterns, FnmatchMatcher -from coverage.misc import ensure_dir_for_file, file_be_gone - - -def render_report(output_path, reporter, morfs, msgfn): - """Run a one-file report generator, managing the output file. - - This function ensures the output file is ready to be written to. Then writes - the report to it. Then closes the file and cleans up. - - """ - file_to_close = None - delete_file = False - - if output_path == "-": - outfile = sys.stdout - else: - # Ensure that the output directory is created; done here - # because this report pre-opens the output file. - # HTMLReport does this using the Report plumbing because - # its task is more complex, being multiple files. - ensure_dir_for_file(output_path) - outfile = open(output_path, "w", encoding="utf-8") - file_to_close = outfile - - try: - return reporter.report(morfs, outfile=outfile) - except CoverageException: - delete_file = True - raise - finally: - if file_to_close: - file_to_close.close() - if delete_file: - file_be_gone(output_path) # pragma: part covered (doesn't return) +from typing import Any, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING + +from coverage.exceptions import ConfigError, NoDataError +from coverage.misc import human_sorted_items +from coverage.plugin import FileReporter +from coverage.report_core import get_analysis_to_report +from coverage.results import Analysis, Numbers +from coverage.types import TMorf + +if TYPE_CHECKING: + from coverage import Coverage + + +class SummaryReporter: + """A reporter for writing the summary report.""" + + def __init__(self, coverage: Coverage) -> None: + self.coverage = coverage + self.config = self.coverage.config + self.branches = coverage.get_data().has_arcs() + self.outfile: Optional[IO[str]] = None + self.output_format = self.config.format or "text" + if self.output_format not in {"text", "markdown", "total"}: + raise ConfigError(f"Unknown report format choice: {self.output_format!r}") + self.fr_analysis: List[Tuple[FileReporter, Analysis]] = [] + self.skipped_count = 0 + self.empty_count = 0 + self.total = Numbers(precision=self.config.precision) + + def write(self, line: str) -> None: + """Write a line to the output, adding a newline.""" + assert self.outfile is not None + self.outfile.write(line.rstrip()) + self.outfile.write("\n") + + def write_items(self, items: Iterable[str]) -> None: + """Write a list of strings, joined together.""" + self.write("".join(items)) + + def _report_text( + self, + header: List[str], + lines_values: List[List[Any]], + total_line: List[Any], + end_lines: List[str], + ) -> None: + """Internal method that prints report data in text format. + + `header` is a list with captions. + `lines_values` is list of lists of sortable values. + `total_line` is a list with values of the total line. + `end_lines` is a list of ending lines with information about skipped files. + + """ + # Prepare the formatting strings, header, and column sorting. + max_name = max([len(line[0]) for line in lines_values] + [5]) + 1 + max_n = max(len(total_line[header.index("Cover")]) + 2, len(" Cover")) + 1 + max_n = max([max_n] + [len(line[header.index("Cover")]) + 2 for line in lines_values]) + formats = dict( + Name="{:{name_len}}", + Stmts="{:>7}", + Miss="{:>7}", + Branch="{:>7}", + BrPart="{:>7}", + Cover="{:>{n}}", + Missing="{:>10}", + ) + header_items = [ + formats[item].format(item, name_len=max_name, n=max_n) + for item in header + ] + header_str = "".join(header_items) + rule = "-" * len(header_str) + + # Write the header + self.write(header_str) + self.write(rule) + + formats.update(dict(Cover="{:>{n}}%"), Missing=" {:9}") + for values in lines_values: + # build string with line values + line_items = [ + formats[item].format(str(value), + name_len=max_name, n=max_n-1) for item, value in zip(header, values) + ] + self.write_items(line_items) + + # Write a TOTAL line + if lines_values: + self.write(rule) + + line_items = [ + formats[item].format(str(value), + name_len=max_name, n=max_n-1) for item, value in zip(header, total_line) + ] + self.write_items(line_items) + + for end_line in end_lines: + self.write(end_line) + + def _report_markdown( + self, + header: List[str], + lines_values: List[List[Any]], + total_line: List[Any], + end_lines: List[str], + ) -> None: + """Internal method that prints report data in markdown format. + + `header` is a list with captions. + `lines_values` is a sorted list of lists containing coverage information. + `total_line` is a list with values of the total line. + `end_lines` is a list of ending lines with information about skipped files. + + """ + # Prepare the formatting strings, header, and column sorting. + max_name = max((len(line[0].replace("_", "\\_")) for line in lines_values), default=0) + max_name = max(max_name, len("**TOTAL**")) + 1 + formats = dict( + Name="| {:{name_len}}|", + Stmts="{:>9} |", + Miss="{:>9} |", + Branch="{:>9} |", + BrPart="{:>9} |", + Cover="{:>{n}} |", + Missing="{:>10} |", + ) + max_n = max(len(total_line[header.index("Cover")]) + 6, len(" Cover ")) + header_items = [formats[item].format(item, name_len=max_name, n=max_n) for item in header] + header_str = "".join(header_items) + rule_str = "|" + " ".join(["- |".rjust(len(header_items[0])-1, "-")] + + ["-: |".rjust(len(item)-1, "-") for item in header_items[1:]] + ) + + # Write the header + self.write(header_str) + self.write(rule_str) + + for values in lines_values: + # build string with line values + formats.update(dict(Cover="{:>{n}}% |")) + line_items = [ + formats[item].format(str(value).replace("_", "\\_"), name_len=max_name, n=max_n-1) + for item, value in zip(header, values) + ] + self.write_items(line_items) + + # Write the TOTAL line + formats.update(dict(Name="|{:>{name_len}} |", Cover="{:>{n}} |")) + total_line_items: List[str] = [] + for item, value in zip(header, total_line): + if value == "": + insert = value + elif item == "Cover": + insert = f" **{value}%**" else: - msgfn(f"Wrote {reporter.report_type} to {output_path}") + insert = f" **{value}**" + total_line_items += formats[item].format(insert, name_len=max_name, n=max_n) + self.write_items(total_line_items) + for end_line in end_lines: + self.write(end_line) + def report(self, morfs: Optional[Iterable[TMorf]], outfile: Optional[IO[str]] = None) -> float: + """Writes a report summarizing coverage statistics per module. -def get_analysis_to_report(coverage, morfs): - """Get the files to report on. + `outfile` is a text-mode file object to write the summary to. - For each morf in `morfs`, if it should be reported on (based on the omit - and include configuration options), yield a pair, the `FileReporter` and - `Analysis` for the morf. - - """ - file_reporters = coverage._get_file_reporters(morfs) - config = coverage.config - - if config.report_include: - matcher = FnmatchMatcher(prep_patterns(config.report_include), "report_include") - file_reporters = [fr for fr in file_reporters if matcher.match(fr.filename)] - - if config.report_omit: - matcher = FnmatchMatcher(prep_patterns(config.report_omit), "report_omit") - file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)] - - if not file_reporters: - raise NoDataError("No data to report.") - - for fr in sorted(file_reporters): - try: - analysis = coverage._analyze(fr) - except NotPython: - # Only report errors for .py files, and only if we didn't - # explicitly suppress those errors. - # NotPython is only raised by PythonFileReporter, which has a - # should_be_python() method. - if fr.should_be_python(): - if config.ignore_errors: - msg = f"Couldn't parse Python file '{fr.filename}'" - coverage._warn(msg, slug="couldnt-parse") - else: - raise - except Exception as exc: - if config.ignore_errors: - msg = f"Couldn't parse '{fr.filename}': {exc}".rstrip() - coverage._warn(msg, slug="couldnt-parse") - else: - raise + """ + self.outfile = outfile or sys.stdout + + self.coverage.get_data().set_query_contexts(self.config.report_contexts) + for fr, analysis in get_analysis_to_report(self.coverage, morfs): + self.report_one_file(fr, analysis) + + if not self.total.n_files and not self.skipped_count: + raise NoDataError("No data to report.") + + if self.output_format == "total": + self.write(self.total.pc_covered_str) + else: + self.tabular_report() + + return self.total.pc_covered + + def tabular_report(self) -> None: + """Writes tabular report formats.""" + # Prepare the header line and column sorting. + header = ["Name", "Stmts", "Miss"] + if self.branches: + header += ["Branch", "BrPart"] + header += ["Cover"] + if self.config.show_missing: + header += ["Missing"] + + column_order = dict(name=0, stmts=1, miss=2, cover=-1) + if self.branches: + column_order.update(dict(branch=3, brpart=4)) + + # `lines_values` is list of lists of sortable values. + lines_values = [] + + for (fr, analysis) in self.fr_analysis: + nums = analysis.numbers + + args = [fr.relative_filename(), nums.n_statements, nums.n_missing] + if self.branches: + args += [nums.n_branches, nums.n_partial_branches] + args += [nums.pc_covered_str] + if self.config.show_missing: + args += [analysis.missing_formatted(branches=True)] + args += [nums.pc_covered] + lines_values.append(args) + + # Line sorting. + sort_option = (self.config.sort or "name").lower() + reverse = False + if sort_option[0] == "-": + reverse = True + sort_option = sort_option[1:] + elif sort_option[0] == "+": + sort_option = sort_option[1:] + sort_idx = column_order.get(sort_option) + if sort_idx is None: + raise ConfigError(f"Invalid sorting option: {self.config.sort!r}") + if sort_option == "name": + lines_values = human_sorted_items(lines_values, reverse=reverse) + else: + lines_values.sort( + key=lambda line: (line[sort_idx], line[0]), # type: ignore[index] + reverse=reverse, + ) + + # Calculate total if we had at least one file. + total_line = ["TOTAL", self.total.n_statements, self.total.n_missing] + if self.branches: + total_line += [self.total.n_branches, self.total.n_partial_branches] + total_line += [self.total.pc_covered_str] + if self.config.show_missing: + total_line += [""] + + # Create other final lines. + end_lines = [] + if self.config.skip_covered and self.skipped_count: + file_suffix = "s" if self.skipped_count>1 else "" + end_lines.append( + f"\n{self.skipped_count} file{file_suffix} skipped due to complete coverage." + ) + if self.config.skip_empty and self.empty_count: + file_suffix = "s" if self.empty_count > 1 else "" + end_lines.append(f"\n{self.empty_count} empty file{file_suffix} skipped.") + + if self.output_format == "markdown": + formatter = self._report_markdown + else: + formatter = self._report_text + formatter(header, lines_values, total_line, end_lines) + + def report_one_file(self, fr: FileReporter, analysis: Analysis) -> None: + """Report on just one file, the callback from report().""" + nums = analysis.numbers + self.total += nums + + no_missing_lines = (nums.n_missing == 0) + no_missing_branches = (nums.n_partial_branches == 0) + if self.config.skip_covered and no_missing_lines and no_missing_branches: + # Don't report on 100% files. + self.skipped_count += 1 + elif self.config.skip_empty and nums.n_statements == 0: + # Don't report on empty files. + self.empty_count += 1 else: - yield (fr, analysis) + self.fr_analysis.append((fr, analysis)) diff -Nru python-coverage-6.5.0+dfsg1/coverage/results.py python-coverage-7.2.7+dfsg1/coverage/results.py --- python-coverage-6.5.0+dfsg1/coverage/results.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/results.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,17 +3,32 @@ """Results of coverage measurement.""" +from __future__ import annotations + import collections -from coverage.debug import SimpleReprMixin +from typing import Callable, Dict, Iterable, List, Optional, Tuple, TYPE_CHECKING + +from coverage.debug import AutoReprMixin from coverage.exceptions import ConfigError -from coverage.misc import contract, nice_pair +from coverage.misc import nice_pair +from coverage.types import TArc, TLineNo + +if TYPE_CHECKING: + from coverage.data import CoverageData + from coverage.plugin import FileReporter class Analysis: """The results of analyzing a FileReporter.""" - def __init__(self, data, precision, file_reporter, file_mapper): + def __init__( + self, + data: CoverageData, + precision: int, + file_reporter: FileReporter, + file_mapper: Callable[[str], str], + ) -> None: self.data = data self.file_reporter = file_reporter self.filename = file_mapper(self.file_reporter.filename) @@ -21,6 +36,7 @@ self.excluded = self.file_reporter.excluded_lines() # Identify missing statements. + executed: Iterable[TLineNo] executed = self.data.lines(self.filename) or [] executed = self.file_reporter.translate_lines(executed) self.executed = executed @@ -51,7 +67,7 @@ n_missing_branches=n_missing_branches, ) - def missing_formatted(self, branches=False): + def missing_formatted(self, branches: bool = False) -> str: """The missing line numbers, formatted nicely. Returns a string like "1-2, 5-11, 13-14". @@ -66,25 +82,23 @@ return format_lines(self.statements, self.missing, arcs=arcs) - def has_arcs(self): + def has_arcs(self) -> bool: """Were arcs measured in this result?""" return self.data.has_arcs() - @contract(returns='list(tuple(int, int))') - def arc_possibilities(self): + def arc_possibilities(self) -> List[TArc]: """Returns a sorted list of the arcs in the code.""" return self._arc_possibilities - @contract(returns='list(tuple(int, int))') - def arcs_executed(self): + def arcs_executed(self) -> List[TArc]: """Returns a sorted list of the arcs actually executed in the code.""" + executed: Iterable[TArc] executed = self.data.arcs(self.filename) or [] executed = self.file_reporter.translate_arcs(executed) return sorted(executed) - @contract(returns='list(tuple(int, int))') - def arcs_missing(self): - """Returns a sorted list of the unexecuted arcs in the code.""" + def arcs_missing(self) -> List[TArc]: + """Returns a sorted list of the un-executed arcs in the code.""" possible = self.arc_possibilities() executed = self.arcs_executed() missing = ( @@ -95,8 +109,7 @@ ) return sorted(missing) - @contract(returns='list(tuple(int, int))') - def arcs_unpredicted(self): + def arcs_unpredicted(self) -> List[TArc]: """Returns a sorted list of the executed arcs missing from the code.""" possible = self.arc_possibilities() executed = self.arcs_executed() @@ -113,16 +126,15 @@ ) return sorted(unpredicted) - def _branch_lines(self): + def _branch_lines(self) -> List[TLineNo]: """Returns a list of line numbers that have more than one exit.""" return [l1 for l1,count in self.exit_counts.items() if count > 1] - def _total_branches(self): + def _total_branches(self) -> int: """How many total branches are there?""" return sum(count for count in self.exit_counts.values() if count > 1) - @contract(returns='dict(int: list(int))') - def missing_branch_arcs(self): + def missing_branch_arcs(self) -> Dict[TLineNo, List[TLineNo]]: """Return arcs that weren't executed from branch lines. Returns {l1:[l2a,l2b,...], ...} @@ -136,8 +148,7 @@ mba[l1].append(l2) return mba - @contract(returns='dict(int: list(int))') - def executed_branch_arcs(self): + def executed_branch_arcs(self) -> Dict[TLineNo, List[TLineNo]]: """Return arcs that were executed from branch lines. Returns {l1:[l2a,l2b,...], ...} @@ -151,8 +162,7 @@ eba[l1].append(l2) return eba - @contract(returns='dict(int: tuple(int, int))') - def branch_stats(self): + def branch_stats(self) -> Dict[TLineNo, Tuple[int, int]]: """Get stats about branches. Returns a dict mapping line numbers to a tuple: @@ -168,7 +178,7 @@ return stats -class Numbers(SimpleReprMixin): +class Numbers(AutoReprMixin): """The numerical results of measuring coverage. This holds the basic statistics from `Analysis`, and is used to roll @@ -176,11 +186,17 @@ """ - def __init__(self, - precision=0, - n_files=0, n_statements=0, n_excluded=0, n_missing=0, - n_branches=0, n_partial_branches=0, n_missing_branches=0 - ): + def __init__( + self, + precision: int = 0, + n_files: int = 0, + n_statements: int = 0, + n_excluded: int = 0, + n_missing: int = 0, + n_branches: int = 0, + n_partial_branches: int = 0, + n_missing_branches: int = 0, + ) -> None: assert 0 <= precision < 10 self._precision = precision self._near0 = 1.0 / 10**precision @@ -193,7 +209,7 @@ self.n_partial_branches = n_partial_branches self.n_missing_branches = n_missing_branches - def init_args(self): + def init_args(self) -> List[int]: """Return a list for __init__(*args) to recreate this object.""" return [ self._precision, @@ -202,17 +218,17 @@ ] @property - def n_executed(self): + def n_executed(self) -> int: """Returns the number of executed statements.""" return self.n_statements - self.n_missing @property - def n_executed_branches(self): + def n_executed_branches(self) -> int: """Returns the number of executed branches.""" return self.n_branches - self.n_missing_branches @property - def pc_covered(self): + def pc_covered(self) -> float: """Returns a single percentage value for coverage.""" if self.n_statements > 0: numerator, denominator = self.ratio_covered @@ -222,7 +238,7 @@ return pc_cov @property - def pc_covered_str(self): + def pc_covered_str(self) -> str: """Returns the percent covered, as a string, without a percent sign. Note that "0" is only returned when the value is truly zero, and "100" @@ -232,7 +248,7 @@ """ return self.display_covered(self.pc_covered) - def display_covered(self, pc): + def display_covered(self, pc: float) -> str: """Return a displayable total percentage, as a string. Note that "0" is only returned when the value is truly zero, and "100" @@ -248,7 +264,7 @@ pc = round(pc, self._precision) return "%.*f" % (self._precision, pc) - def pc_str_width(self): + def pc_str_width(self) -> int: """How many characters wide can pc_covered_str be?""" width = 3 # "100" if self._precision > 0: @@ -256,13 +272,13 @@ return width @property - def ratio_covered(self): + def ratio_covered(self) -> Tuple[int, int]: """Return a numerator and denominator for the coverage ratio.""" numerator = self.n_executed + self.n_executed_branches denominator = self.n_statements + self.n_branches return numerator, denominator - def __add__(self, other): + def __add__(self, other: Numbers) -> Numbers: nums = Numbers(precision=self._precision) nums.n_files = self.n_files + other.n_files nums.n_statements = self.n_statements + other.n_statements @@ -277,13 +293,16 @@ ) return nums - def __radd__(self, other): + def __radd__(self, other: int) -> Numbers: # Implementing 0+Numbers allows us to sum() a list of Numbers. assert other == 0 # we only ever call it this way. return self -def _line_ranges(statements, lines): +def _line_ranges( + statements: Iterable[TLineNo], + lines: Iterable[TLineNo], +) -> List[Tuple[TLineNo, TLineNo]]: """Produce a list of ranges for `format_lines`.""" statements = sorted(statements) lines = sorted(lines) @@ -307,7 +326,11 @@ return pairs -def format_lines(statements, lines, arcs=None): +def format_lines( + statements: Iterable[TLineNo], + lines: Iterable[TLineNo], + arcs: Optional[Iterable[Tuple[TLineNo, List[TLineNo]]]] = None, +) -> str: """Nicely format a list of line numbers. Format a list of line numbers for printing by coalescing groups of lines as @@ -326,7 +349,7 @@ """ line_items = [(pair[0], nice_pair(pair)) for pair in _line_ranges(statements, lines)] - if arcs: + if arcs is not None: line_exits = sorted(arcs) for line, exits in line_exits: for ex in sorted(exits): @@ -334,12 +357,11 @@ dest = (ex if ex > 0 else "exit") line_items.append((line, f"{line}->{dest}")) - ret = ', '.join(t[-1] for t in sorted(line_items)) + ret = ", ".join(t[-1] for t in sorted(line_items)) return ret -@contract(total='number', fail_under='number', precision=int, returns=bool) -def should_fail_under(total, fail_under, precision): +def should_fail_under(total: float, fail_under: float, precision: int) -> bool: """Determine if a total should fail due to fail-under. `total` is a float, the coverage measurement total. `fail_under` is the diff -Nru python-coverage-6.5.0+dfsg1/coverage/sqldata.py python-coverage-7.2.7+dfsg1/coverage/sqldata.py --- python-coverage-6.5.0+dfsg1/coverage/sqldata.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/sqldata.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,7 +3,10 @@ """SQLite coverage data.""" +from __future__ import annotations + import collections +import contextlib import datetime import functools import glob @@ -18,16 +21,22 @@ import threading import zlib -from coverage.debug import NoDebugging, SimpleReprMixin, clipped_repr +from typing import ( + cast, Any, Callable, Collection, Dict, Iterable, Iterator, List, Mapping, + Optional, Sequence, Set, Tuple, TypeVar, Union, +) + +from coverage.debug import NoDebugging, AutoReprMixin, clipped_repr from coverage.exceptions import CoverageException, DataError from coverage.files import PathAliases -from coverage.misc import contract, file_be_gone, isolate_module +from coverage.misc import file_be_gone, isolate_module from coverage.numbits import numbits_to_nums, numbits_union, nums_to_numbits +from coverage.types import FilePath, TArc, TDebugCtl, TLineNo, TWarnFn from coverage.version import __version__ os = isolate_module(os) -# If you change the schema, increment the SCHEMA_VERSION, and update the +# If you change the schema: increment the SCHEMA_VERSION and update the # docs in docs/dbschema.rst by running "make cogdoc". SCHEMA_VERSION = 7 @@ -52,7 +61,7 @@ key text, value text, unique (key) - -- Keys: + -- Possible keys: -- 'has_arcs' boolean -- Is this data recording branches? -- 'sys_argv' text -- The coverage command line that recorded the data. -- 'version' text -- The version of coverage.py that made the file. @@ -103,7 +112,22 @@ ); """ -class CoverageData(SimpleReprMixin): +TMethod = TypeVar("TMethod", bound=Callable[..., Any]) + +def _locked(method: TMethod) -> TMethod: + """A decorator for methods that should hold self._lock.""" + @functools.wraps(method) + def _wrapped(self: CoverageData, *args: Any, **kwargs: Any) -> Any: + if self._debug.should("lock"): + self._debug.write(f"Locking {self._lock!r} for {method.__name__}") + with self._lock: + if self._debug.should("lock"): + self._debug.write(f"Locked {self._lock!r} for {method.__name__}") + return method(self, *args, **kwargs) + return _wrapped # type: ignore[return-value] + + +class CoverageData(AutoReprMixin): """Manages collected coverage data, including file storage. This class is the public supported API to the data that coverage.py @@ -173,9 +197,11 @@ Write the data to its file with :meth:`write`. - You can clear the data in memory with :meth:`erase`. Two data collections - can be combined by using :meth:`update` on one :class:`CoverageData`, - passing it the other. + You can clear the data in memory with :meth:`erase`. Data for specific + files can be removed from the database with :meth:`purge_files`. + + Two data collections can be combined by using :meth:`update` on one + :class:`CoverageData`, passing it the other. Data in a :class:`CoverageData` can be serialized and deserialized with :meth:`dumps` and :meth:`loads`. @@ -186,7 +212,14 @@ """ - def __init__(self, basename=None, suffix=None, no_disk=False, warn=None, debug=None): + def __init__( + self, + basename: Optional[FilePath] = None, + suffix: Optional[Union[str, bool]] = None, + no_disk: bool = False, + warn: Optional[TWarnFn] = None, + debug: Optional[TDebugCtl] = None, + ) -> None: """Create a :class:`CoverageData` object to hold coverage-measured data. Arguments: @@ -208,9 +241,10 @@ self._debug = debug or NoDebugging() self._choose_filename() - self._file_map = {} + # Maps filenames to row ids. + self._file_map: Dict[str, int] = {} # Maps thread ids to SqliteDb objects. - self._dbs = {} + self._dbs: Dict[int, SqliteDb] = {} self._pid = os.getpid() # Synchronize the operations used during collection. self._lock = threading.RLock() @@ -221,24 +255,11 @@ self._has_lines = False self._has_arcs = False - self._current_context = None - self._current_context_id = None - self._query_context_ids = None - - def _locked(method): # pylint: disable=no-self-argument - """A decorator for methods that should hold self._lock.""" - @functools.wraps(method) - def _wrapped(self, *args, **kwargs): - if self._debug.should("lock"): - self._debug.write(f"Locking {self._lock!r} for {method.__name__}") - with self._lock: - if self._debug.should("lock"): - self._debug.write(f"Locked {self._lock!r} for {method.__name__}") - # pylint: disable=not-callable - return method(self, *args, **kwargs) - return _wrapped + self._current_context: Optional[str] = None + self._current_context_id: Optional[int] = None + self._query_context_ids: Optional[List[int]] = None - def _choose_filename(self): + def _choose_filename(self) -> None: """Set self._filename based on inited attributes.""" if self._no_disk: self._filename = ":memory:" @@ -248,7 +269,7 @@ if suffix: self._filename += "." + suffix - def _reset(self): + def _reset(self) -> None: """Reset our attributes.""" if not self._no_disk: for db in self._dbs.values(): @@ -258,18 +279,19 @@ self._have_used = False self._current_context_id = None - def _open_db(self): + def _open_db(self) -> None: """Open an existing db file, and read its metadata.""" if self._debug.should("dataio"): self._debug.write(f"Opening data file {self._filename!r}") self._dbs[threading.get_ident()] = SqliteDb(self._filename, self._debug) self._read_db() - def _read_db(self): + def _read_db(self) -> None: """Read the metadata from a database so that we are ready to use it.""" with self._dbs[threading.get_ident()] as db: try: - schema_version, = db.execute_one("select version from coverage_schema") + row = db.execute_one("select version from coverage_schema") + assert row is not None except Exception as exc: if "no such table: coverage_schema" in str(exc): self._init_db(db) @@ -280,6 +302,7 @@ ) ) from exc else: + schema_version = row[0] if schema_version != SCHEMA_VERSION: raise DataError( "Couldn't use data file {!r}: wrong schema: {} instead of {}".format( @@ -287,46 +310,51 @@ ) ) - for row in db.execute("select value from meta where key = 'has_arcs'"): + row = db.execute_one("select value from meta where key = 'has_arcs'") + if row is not None: self._has_arcs = bool(int(row[0])) self._has_lines = not self._has_arcs - for file_id, path in db.execute("select id, path from file"): - self._file_map[path] = file_id + with db.execute("select id, path from file") as cur: + for file_id, path in cur: + self._file_map[path] = file_id - def _init_db(self, db): + def _init_db(self, db: SqliteDb) -> None: """Write the initial contents of the database.""" if self._debug.should("dataio"): self._debug.write(f"Initing data file {self._filename!r}") db.executescript(SCHEMA) - db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,)) - db.executemany( - "insert or ignore into meta (key, value) values (?, ?)", - [ + db.execute_void("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,)) + + # When writing metadata, avoid information that will needlessly change + # the hash of the data file, unless we're debugging processes. + meta_data = [ + ("version", __version__), + ] + if self._debug.should("process"): + meta_data.extend([ ("sys_argv", str(getattr(sys, "argv", None))), - ("version", __version__), ("when", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")), - ] - ) + ]) + db.executemany_void("insert or ignore into meta (key, value) values (?, ?)", meta_data) - def _connect(self): + def _connect(self) -> SqliteDb: """Get the SqliteDb object to use.""" if threading.get_ident() not in self._dbs: self._open_db() return self._dbs[threading.get_ident()] - def __bool__(self): + def __bool__(self) -> bool: if (threading.get_ident() not in self._dbs and not os.path.exists(self._filename)): return False try: with self._connect() as con: - rows = con.execute("select * from file limit 1") - return bool(list(rows)) + with con.execute("select * from file limit 1") as cur: + return bool(list(cur)) except CoverageException: return False - @contract(returns="bytes") - def dumps(self): + def dumps(self) -> bytes: """Serialize the current data to a byte string. The format of the serialized data is not documented. It is only @@ -349,8 +377,7 @@ script = con.dump() return b"z" + zlib.compress(script.encode("utf-8")) - @contract(data="bytes") - def loads(self, data): + def loads(self, data: bytes) -> None: """Deserialize data from :meth:`dumps`. Use with a newly-created empty :class:`CoverageData` object. It's @@ -378,7 +405,7 @@ self._read_db() self._have_used = True - def _file_id(self, filename, add=False): + def _file_id(self, filename: str, add: bool = False) -> Optional[int]: """Get the file id for `filename`. If filename is not in the database yet, add it if `add` is True. @@ -393,19 +420,19 @@ ) return self._file_map.get(filename) - def _context_id(self, context): + def _context_id(self, context: str) -> Optional[int]: """Get the id for a context.""" assert context is not None self._start_using() with self._connect() as con: row = con.execute_one("select id from context where context = ?", (context,)) if row is not None: - return row[0] + return cast(int, row[0]) else: return None @_locked - def set_context(self, context): + def set_context(self, context: Optional[str]) -> None: """Set the current context for future :meth:`add_lines` etc. `context` is a str, the name of the context to use for the next data @@ -419,7 +446,7 @@ self._current_context = context self._current_context_id = None - def _set_context_id(self): + def _set_context_id(self) -> None: """Use the _current_context to set _current_context_id.""" context = self._current_context or "" context_id = self._context_id(context) @@ -432,7 +459,7 @@ (context,) ) - def base_filename(self): + def base_filename(self) -> str: """The base filename for storing data. .. versionadded:: 5.0 @@ -440,7 +467,7 @@ """ return self._basename - def data_filename(self): + def data_filename(self) -> str: """Where is the data stored? .. versionadded:: 5.0 @@ -449,7 +476,7 @@ return self._filename @_locked - def add_lines(self, line_data): + def add_lines(self, line_data: Mapping[str, Collection[TLineNo]]) -> None: """Add measured line data. `line_data` is a dictionary mapping file names to iterables of ints:: @@ -459,7 +486,7 @@ """ if self._debug.should("dataop"): self._debug.write("Adding lines: %d files, %d lines total" % ( - len(line_data), sum(len(lines) for lines in line_data.values()) + len(line_data), sum(bool(len(lines)) for lines in line_data.values()) )) self._start_using() self._choose_lines_or_arcs(lines=True) @@ -471,18 +498,19 @@ linemap = nums_to_numbits(linenos) file_id = self._file_id(filename, add=True) query = "select numbits from line_bits where file_id = ? and context_id = ?" - existing = list(con.execute(query, (file_id, self._current_context_id))) + with con.execute(query, (file_id, self._current_context_id)) as cur: + existing = list(cur) if existing: linemap = numbits_union(linemap, existing[0][0]) - con.execute( + con.execute_void( "insert or replace into line_bits " + " (file_id, context_id, numbits) values (?, ?, ?)", (file_id, self._current_context_id, linemap), ) @_locked - def add_arcs(self, arc_data): + def add_arcs(self, arc_data: Mapping[str, Collection[TArc]]) -> None: """Add measured arc data. `arc_data` is a dictionary mapping file names to iterables of pairs of @@ -502,15 +530,17 @@ with self._connect() as con: self._set_context_id() for filename, arcs in arc_data.items(): + if not arcs: + continue file_id = self._file_id(filename, add=True) data = [(file_id, self._current_context_id, fromno, tono) for fromno, tono in arcs] - con.executemany( + con.executemany_void( "insert or ignore into arc " + "(file_id, context_id, fromno, tono) values (?, ?, ?, ?)", data, ) - def _choose_lines_or_arcs(self, lines=False, arcs=False): + def _choose_lines_or_arcs(self, lines: bool = False, arcs: bool = False) -> None: """Force the data file to choose between lines and arcs.""" assert lines or arcs assert not (lines and arcs) @@ -526,13 +556,13 @@ self._has_lines = lines self._has_arcs = arcs with self._connect() as con: - con.execute( + con.execute_void( "insert or ignore into meta (key, value) values (?, ?)", ("has_arcs", str(int(arcs))) ) @_locked - def add_file_tracers(self, file_tracers): + def add_file_tracers(self, file_tracers: Mapping[str, str]) -> None: """Add per-file plugin information. `file_tracers` is { filename: plugin_name, ... } @@ -545,12 +575,7 @@ self._start_using() with self._connect() as con: for filename, plugin_name in file_tracers.items(): - file_id = self._file_id(filename) - if file_id is None: - raise DataError( - f"Can't add file tracer data for unmeasured file '{filename}'" - ) - + file_id = self._file_id(filename, add=True) existing_plugin = self.file_tracer(filename) if existing_plugin: if existing_plugin != plugin_name: @@ -560,24 +585,24 @@ ) ) elif plugin_name: - con.execute( + con.execute_void( "insert into tracer (file_id, tracer) values (?, ?)", (file_id, plugin_name) ) - def touch_file(self, filename, plugin_name=""): + def touch_file(self, filename: str, plugin_name: str = "") -> None: """Ensure that `filename` appears in the data, empty if needed. - `plugin_name` is the name of the plugin responsible for this file. It is used - to associate the right filereporter, etc. + `plugin_name` is the name of the plugin responsible for this file. + It is used to associate the right filereporter, etc. """ self.touch_files([filename], plugin_name) - def touch_files(self, filenames, plugin_name=""): + def touch_files(self, filenames: Collection[str], plugin_name: Optional[str] = None) -> None: """Ensure that `filenames` appear in the data, empty if needed. - `plugin_name` is the name of the plugin responsible for these files. It is used - to associate the right filereporter, etc. + `plugin_name` is the name of the plugin responsible for these files. + It is used to associate the right filereporter, etc. """ if self._debug.should("dataop"): self._debug.write(f"Touching {filenames!r}") @@ -592,11 +617,37 @@ # Set the tracer for this file self.add_file_tracers({filename: plugin_name}) - def update(self, other_data, aliases=None): + def purge_files(self, filenames: Collection[str]) -> None: + """Purge any existing coverage data for the given `filenames`. + + .. versionadded:: 7.2 + + """ + if self._debug.should("dataop"): + self._debug.write(f"Purging data for {filenames!r}") + self._start_using() + with self._connect() as con: + + if self._has_lines: + sql = "delete from line_bits where file_id=?" + elif self._has_arcs: + sql = "delete from arc where file_id=?" + else: + raise DataError("Can't purge files in an empty CoverageData") + + for filename in filenames: + file_id = self._file_id(filename, add=False) + if file_id is None: + continue + con.execute_void(sql, (file_id,)) + + def update(self, other_data: CoverageData, aliases: Optional[PathAliases] = None) -> None: """Update this data with data from several other :class:`CoverageData` instances. If `aliases` is provided, it's a `PathAliases` object that is used to - re-map paths to match the local machine's. + re-map paths to match the local machine's. Note: `aliases` is None + only when called directly from the test suite. + """ if self._debug.should("dataop"): self._debug.write("Updating with data from {!r}".format( @@ -616,78 +667,80 @@ other_data.read() with other_data._connect() as con: # Get files data. - cur = con.execute("select path from file") - files = {path: aliases.map(path) for (path,) in cur} - cur.close() + with con.execute("select path from file") as cur: + files = {path: aliases.map(path) for (path,) in cur} # Get contexts data. - cur = con.execute("select context from context") - contexts = [context for (context,) in cur] - cur.close() + with con.execute("select context from context") as cur: + contexts = [context for (context,) in cur] # Get arc data. - cur = con.execute( + with con.execute( "select file.path, context.context, arc.fromno, arc.tono " + "from arc " + "inner join file on file.id = arc.file_id " + "inner join context on context.id = arc.context_id" - ) - arcs = [(files[path], context, fromno, tono) for (path, context, fromno, tono) in cur] - cur.close() + ) as cur: + arcs = [ + (files[path], context, fromno, tono) + for (path, context, fromno, tono) in cur + ] # Get line data. - cur = con.execute( + with con.execute( "select file.path, context.context, line_bits.numbits " + "from line_bits " + "inner join file on file.id = line_bits.file_id " + "inner join context on context.id = line_bits.context_id" - ) - lines = {(files[path], context): numbits for (path, context, numbits) in cur} - cur.close() + ) as cur: + lines: Dict[Tuple[str, str], bytes] = {} + for path, context, numbits in cur: + key = (files[path], context) + if key in lines: + numbits = numbits_union(lines[key], numbits) + lines[key] = numbits # Get tracer data. - cur = con.execute( + with con.execute( "select file.path, tracer " + "from tracer " + "inner join file on file.id = tracer.file_id" - ) - tracers = {files[path]: tracer for (path, tracer) in cur} - cur.close() + ) as cur: + tracers = {files[path]: tracer for (path, tracer) in cur} with self._connect() as con: + assert con.con is not None con.con.isolation_level = "IMMEDIATE" # Get all tracers in the DB. Files not in the tracers are assumed # to have an empty string tracer. Since Sqlite does not support # full outer joins, we have to make two queries to fill the # dictionary. - this_tracers = {path: "" for path, in con.execute("select path from file")} - this_tracers.update({ - aliases.map(path): tracer - for path, tracer in con.execute( - "select file.path, tracer from tracer " + - "inner join file on file.id = tracer.file_id" - ) - }) + with con.execute("select path from file") as cur: + this_tracers = {path: "" for path, in cur} + with con.execute( + "select file.path, tracer from tracer " + + "inner join file on file.id = tracer.file_id" + ) as cur: + this_tracers.update({ + aliases.map(path): tracer + for path, tracer in cur + }) # Create all file and context rows in the DB. - con.executemany( + con.executemany_void( "insert or ignore into file (path) values (?)", ((file,) for file in files.values()) ) - file_ids = { - path: id - for id, path in con.execute("select id, path from file") - } + with con.execute("select id, path from file") as cur: + file_ids = {path: id for id, path in cur} self._file_map.update(file_ids) - con.executemany( + con.executemany_void( "insert or ignore into context (context) values (?)", ((context,) for context in contexts) ) - context_ids = { - context: id - for id, context in con.execute("select id, context from context") - } + with con.execute("select id, context from context") as cur: + context_ids = {context: id for id, context in cur} # Prepare tracers and fail, if a conflict is found. # tracer_paths is used to ensure consistency over the tracer data @@ -714,24 +767,23 @@ ) # Get line data. - cur = con.execute( + with con.execute( "select file.path, context.context, line_bits.numbits " + "from line_bits " + "inner join file on file.id = line_bits.file_id " + "inner join context on context.id = line_bits.context_id" - ) - for path, context, numbits in cur: - key = (aliases.map(path), context) - if key in lines: - numbits = numbits_union(lines[key], numbits) - lines[key] = numbits - cur.close() + ) as cur: + for path, context, numbits in cur: + key = (aliases.map(path), context) + if key in lines: + numbits = numbits_union(lines[key], numbits) + lines[key] = numbits if arcs: self._choose_lines_or_arcs(arcs=True) # Write the combined data. - con.executemany( + con.executemany_void( "insert or ignore into arc " + "(file_id, context_id, fromno, tono) values (?, ?, ?, ?)", arc_rows @@ -739,8 +791,8 @@ if lines: self._choose_lines_or_arcs(lines=True) - con.execute("delete from line_bits") - con.executemany( + con.execute_void("delete from line_bits") + con.executemany_void( "insert into line_bits " + "(file_id, context_id, numbits) values (?, ?, ?)", [ @@ -748,7 +800,7 @@ for (file, context), numbits in lines.items() ] ) - con.executemany( + con.executemany_void( "insert or ignore into tracer (file_id, tracer) values (?, ?)", ((file_ids[filename], tracer) for filename, tracer in tracer_map.items()) ) @@ -758,7 +810,7 @@ self._reset() self.read() - def erase(self, parallel=False): + def erase(self, parallel: bool = False) -> None: """Erase the data in this object. If `parallel` is true, then also deletes data files created from the @@ -780,17 +832,17 @@ self._debug.write(f"Erasing parallel data file {filename!r}") file_be_gone(filename) - def read(self): + def read(self) -> None: """Start using an existing data file.""" if os.path.exists(self._filename): with self._connect(): self._have_used = True - def write(self): + def write(self) -> None: """Ensure the data is written to the data file.""" pass - def _start_using(self): + def _start_using(self) -> None: """Call this before using the database at all.""" if self._pid != os.getpid(): # Looks like we forked! Have to start a new data file. @@ -801,15 +853,20 @@ self.erase() self._have_used = True - def has_arcs(self): + def has_arcs(self) -> bool: """Does the database have arcs (True) or lines (False).""" return bool(self._has_arcs) - def measured_files(self): - """A set of all files that had been measured.""" + def measured_files(self) -> Set[str]: + """A set of all files that have been measured. + + Note that a file may be mentioned as measured even though no lines or + arcs for that file are present in the data. + + """ return set(self._file_map) - def measured_contexts(self): + def measured_contexts(self) -> Set[str]: """A set of all contexts that have been measured. .. versionadded:: 5.0 @@ -817,10 +874,11 @@ """ self._start_using() with self._connect() as con: - contexts = {row[0] for row in con.execute("select distinct(context) from context")} + with con.execute("select distinct(context) from context") as cur: + contexts = {row[0] for row in cur} return contexts - def file_tracer(self, filename): + def file_tracer(self, filename: str) -> Optional[str]: """Get the plugin name of the file tracer for a file. Returns the name of the plugin that handles this file. If the file was @@ -838,7 +896,7 @@ return row[0] or "" return "" # File was measured, but no tracer associated. - def set_query_context(self, context): + def set_query_context(self, context: str) -> None: """Set a context for subsequent querying. The next :meth:`lines`, :meth:`arcs`, or :meth:`contexts_by_lineno` @@ -851,10 +909,10 @@ """ self._start_using() with self._connect() as con: - cur = con.execute("select id from context where context = ?", (context,)) - self._query_context_ids = [row[0] for row in cur.fetchall()] + with con.execute("select id from context where context = ?", (context,)) as cur: + self._query_context_ids = [row[0] for row in cur.fetchall()] - def set_query_contexts(self, contexts): + def set_query_contexts(self, contexts: Optional[Sequence[str]]) -> None: """Set a number of contexts for subsequent querying. The next :meth:`lines`, :meth:`arcs`, or :meth:`contexts_by_lineno` @@ -870,12 +928,12 @@ if contexts: with self._connect() as con: context_clause = " or ".join(["context regexp ?"] * len(contexts)) - cur = con.execute("select id from context where " + context_clause, contexts) - self._query_context_ids = [row[0] for row in cur.fetchall()] + with con.execute("select id from context where " + context_clause, contexts) as cur: + self._query_context_ids = [row[0] for row in cur.fetchall()] else: self._query_context_ids = None - def lines(self, filename): + def lines(self, filename: str) -> Optional[List[TLineNo]]: """Get the list of lines executed for a source file. If the file was not measured, returns None. A file might be measured, @@ -903,13 +961,14 @@ ids_array = ", ".join("?" * len(self._query_context_ids)) query += " and context_id in (" + ids_array + ")" data += self._query_context_ids - bitmaps = list(con.execute(query, data)) + with con.execute(query, data) as cur: + bitmaps = list(cur) nums = set() for row in bitmaps: nums.update(numbits_to_nums(row[0])) return list(nums) - def arcs(self, filename): + def arcs(self, filename: str) -> Optional[List[TArc]]: """Get the list of arcs executed for a file. If the file was not measured, returns None. A file might be measured, @@ -938,10 +997,10 @@ ids_array = ", ".join("?" * len(self._query_context_ids)) query += " and context_id in (" + ids_array + ")" data += self._query_context_ids - arcs = con.execute(query, data) - return list(arcs) + with con.execute(query, data) as cur: + return list(cur) - def contexts_by_lineno(self, filename): + def contexts_by_lineno(self, filename: str) -> Dict[TLineNo, List[str]]: """Get the contexts for each line in a file. Returns: @@ -968,11 +1027,12 @@ ids_array = ", ".join("?" * len(self._query_context_ids)) query += " and arc.context_id in (" + ids_array + ")" data += self._query_context_ids - for fromno, tono, context in con.execute(query, data): - if fromno > 0: - lineno_contexts_map[fromno].add(context) - if tono > 0: - lineno_contexts_map[tono].add(context) + with con.execute(query, data) as cur: + for fromno, tono, context in cur: + if fromno > 0: + lineno_contexts_map[fromno].add(context) + if tono > 0: + lineno_contexts_map[tono].add(context) else: query = ( "select l.numbits, c.context from line_bits l, context c " + @@ -984,33 +1044,35 @@ ids_array = ", ".join("?" * len(self._query_context_ids)) query += " and l.context_id in (" + ids_array + ")" data += self._query_context_ids - for numbits, context in con.execute(query, data): - for lineno in numbits_to_nums(numbits): - lineno_contexts_map[lineno].add(context) + with con.execute(query, data) as cur: + for numbits, context in cur: + for lineno in numbits_to_nums(numbits): + lineno_contexts_map[lineno].add(context) return {lineno: list(contexts) for lineno, contexts in lineno_contexts_map.items()} @classmethod - def sys_info(cls): + def sys_info(cls) -> List[Tuple[str, Any]]: """Our information for `Coverage.sys_info`. Returns a list of (key, value) pairs. """ with SqliteDb(":memory:", debug=NoDebugging()) as db: - temp_store = [row[0] for row in db.execute("pragma temp_store")] - copts = [row[0] for row in db.execute("pragma compile_options")] + with db.execute("pragma temp_store") as cur: + temp_store = [row[0] for row in cur] + with db.execute("pragma compile_options") as cur: + copts = [row[0] for row in cur] copts = textwrap.wrap(", ".join(copts), width=75) return [ - ("sqlite3_version", sqlite3.version), ("sqlite3_sqlite_version", sqlite3.sqlite_version), ("sqlite3_temp_store", temp_store), ("sqlite3_compile_options", copts), ] -def filename_suffix(suffix): +def filename_suffix(suffix: Union[str, bool, None]) -> Union[str, None]: """Compute a filename suffix for a data file. If `suffix` is a string or None, simply return it. If `suffix` is True, @@ -1027,10 +1089,12 @@ # if the process forks. dice = random.Random(os.urandom(8)).randint(0, 999999) suffix = "%s.%s.%06d" % (socket.gethostname(), os.getpid(), dice) + elif suffix is False: + suffix = None return suffix -class SqliteDb(SimpleReprMixin): +class SqliteDb(AutoReprMixin): """A simple abstraction over a SQLite database. Use as a context manager, then you can use it like a @@ -1040,13 +1104,13 @@ db.execute("insert into schema (version) values (?)", (SCHEMA_VERSION,)) """ - def __init__(self, filename, debug): + def __init__(self, filename: str, debug: TDebugCtl) -> None: self.debug = debug self.filename = filename self.nest = 0 - self.con = None + self.con: Optional[sqlite3.Connection] = None - def _connect(self): + def _connect(self) -> None: """Connect to the db and do universal initialization.""" if self.con is not None: return @@ -1068,27 +1132,29 @@ # This pragma makes writing faster. It disables rollbacks, but we never need them. # PyPy needs the .close() calls here, or sqlite gets twisted up: # https://bitbucket.org/pypy/pypy/issues/2872/default-isolation-mode-is-different-on - self.execute("pragma journal_mode=off").close() + self.execute_void("pragma journal_mode=off") # This pragma makes writing faster. - self.execute("pragma synchronous=off").close() + self.execute_void("pragma synchronous=off") - def close(self): + def close(self) -> None: """If needed, close the connection.""" if self.con is not None and self.filename != ":memory:": self.con.close() self.con = None - def __enter__(self): + def __enter__(self) -> SqliteDb: if self.nest == 0: self._connect() + assert self.con is not None self.con.__enter__() self.nest += 1 return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type, exc_value, traceback) -> None: # type: ignore[no-untyped-def] self.nest -= 1 if self.nest == 0: try: + assert self.con is not None self.con.__exit__(exc_type, exc_value, traceback) self.close() except Exception as exc: @@ -1096,19 +1162,20 @@ self.debug.write(f"EXCEPTION from __exit__: {exc}") raise DataError(f"Couldn't end data file {self.filename!r}: {exc}") from exc - def execute(self, sql, parameters=()): + def _execute(self, sql: str, parameters: Iterable[Any]) -> sqlite3.Cursor: """Same as :meth:`python:sqlite3.Connection.execute`.""" if self.debug.should("sql"): tail = f" with {parameters!r}" if parameters else "" self.debug.write(f"Executing {sql!r}{tail}") try: + assert self.con is not None try: - return self.con.execute(sql, parameters) + return self.con.execute(sql, parameters) # type: ignore[arg-type] except Exception: # In some cases, an error might happen that isn't really an # error. Try again immediately. # https://github.com/nedbat/coveragepy/issues/1010 - return self.con.execute(sql, parameters) + return self.con.execute(sql, parameters) # type: ignore[arg-type] except sqlite3.Error as exc: msg = str(exc) try: @@ -1127,15 +1194,36 @@ self.debug.write(f"EXCEPTION from execute: {msg}") raise DataError(f"Couldn't use data file {self.filename!r}: {msg}") from exc - def execute_for_rowid(self, sql, parameters=()): + @contextlib.contextmanager + def execute( + self, + sql: str, + parameters: Iterable[Any] = (), + ) -> Iterator[sqlite3.Cursor]: + """Context managed :meth:`python:sqlite3.Connection.execute`. + + Use with a ``with`` statement to auto-close the returned cursor. + """ + cur = self._execute(sql, parameters) + try: + yield cur + finally: + cur.close() + + def execute_void(self, sql: str, parameters: Iterable[Any] = ()) -> None: + """Same as :meth:`python:sqlite3.Connection.execute` when you don't need the cursor.""" + self._execute(sql, parameters).close() + + def execute_for_rowid(self, sql: str, parameters: Iterable[Any] = ()) -> int: """Like execute, but returns the lastrowid.""" - con = self.execute(sql, parameters) - rowid = con.lastrowid + with self.execute(sql, parameters) as cur: + assert cur.lastrowid is not None + rowid: int = cur.lastrowid if self.debug.should("sqldata"): self.debug.write(f"Row id result: {rowid!r}") return rowid - def execute_one(self, sql, parameters=()): + def execute_one(self, sql: str, parameters: Iterable[Any] = ()) -> Optional[Tuple[Any, ...]]: """Execute a statement and return the one row that results. This is like execute(sql, parameters).fetchone(), except it is @@ -1144,23 +1232,24 @@ Returns a row, or None if there were no rows. """ - rows = list(self.execute(sql, parameters)) + with self.execute(sql, parameters) as cur: + rows = list(cur) if len(rows) == 0: return None elif len(rows) == 1: - return rows[0] + return cast(Tuple[Any, ...], rows[0]) else: raise AssertionError(f"SQL {sql!r} shouldn't return {len(rows)} rows") - def executemany(self, sql, data): + def _executemany(self, sql: str, data: List[Any]) -> sqlite3.Cursor: """Same as :meth:`python:sqlite3.Connection.executemany`.""" if self.debug.should("sql"): - data = list(data) final = ":" if self.debug.should("sqldata") else "" self.debug.write(f"Executing many {sql!r} with {len(data)} rows{final}") if self.debug.should("sqldata"): for i, row in enumerate(data): self.debug.write(f"{i:4d}: {row!r}") + assert self.con is not None try: return self.con.executemany(sql, data) except Exception: # pragma: cant happen @@ -1169,14 +1258,22 @@ # https://github.com/nedbat/coveragepy/issues/1010 return self.con.executemany(sql, data) - def executescript(self, script): + def executemany_void(self, sql: str, data: Iterable[Any]) -> None: + """Same as :meth:`python:sqlite3.Connection.executemany` when you don't need the cursor.""" + data = list(data) + if data: + self._executemany(sql, data).close() + + def executescript(self, script: str) -> None: """Same as :meth:`python:sqlite3.Connection.executescript`.""" if self.debug.should("sql"): self.debug.write("Executing script with {} chars: {}".format( len(script), clipped_repr(script, 100), )) - self.con.executescript(script) + assert self.con is not None + self.con.executescript(script).close() - def dump(self): + def dump(self) -> str: """Return a multi-line string, the SQL dump of the database.""" + assert self.con is not None return "\n".join(self.con.iterdump()) diff -Nru python-coverage-6.5.0+dfsg1/coverage/summary.py python-coverage-7.2.7+dfsg1/coverage/summary.py --- python-coverage-6.5.0+dfsg1/coverage/summary.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/summary.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,152 +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 - -"""Summary reporting""" - -import sys - -from coverage.exceptions import ConfigError, NoDataError -from coverage.misc import human_sorted_items -from coverage.report import get_analysis_to_report -from coverage.results import Numbers - - -class SummaryReporter: - """A reporter for writing the summary report.""" - - def __init__(self, coverage): - self.coverage = coverage - self.config = self.coverage.config - self.branches = coverage.get_data().has_arcs() - self.outfile = None - self.fr_analysis = [] - self.skipped_count = 0 - self.empty_count = 0 - self.total = Numbers(precision=self.config.precision) - self.fmt_err = "%s %s: %s" - - def writeout(self, line): - """Write a line to the output, adding a newline.""" - self.outfile.write(line.rstrip()) - self.outfile.write("\n") - - def report(self, morfs, outfile=None): - """Writes a report summarizing coverage statistics per module. - - `outfile` is a file object to write the summary to. It must be opened - for native strings (bytes on Python 2, Unicode on Python 3). - - """ - self.outfile = outfile or sys.stdout - - self.coverage.get_data().set_query_contexts(self.config.report_contexts) - for fr, analysis in get_analysis_to_report(self.coverage, morfs): - self.report_one_file(fr, analysis) - - # Prepare the formatting strings, header, and column sorting. - max_name = max([len(fr.relative_filename()) for (fr, analysis) in self.fr_analysis] + [5]) - fmt_name = "%%- %ds " % max_name - fmt_skip_covered = "\n%s file%s skipped due to complete coverage." - fmt_skip_empty = "\n%s empty file%s skipped." - - header = (fmt_name % "Name") + " Stmts Miss" - fmt_coverage = fmt_name + "%6d %6d" - if self.branches: - header += " Branch BrPart" - fmt_coverage += " %6d %6d" - width100 = Numbers(precision=self.config.precision).pc_str_width() - header += "%*s" % (width100+4, "Cover") - fmt_coverage += "%%%ds%%%%" % (width100+3,) - if self.config.show_missing: - header += " Missing" - fmt_coverage += " %s" - rule = "-" * len(header) - - column_order = dict(name=0, stmts=1, miss=2, cover=-1) - if self.branches: - column_order.update(dict(branch=3, brpart=4)) - - # Write the header - self.writeout(header) - self.writeout(rule) - - # `lines` is a list of pairs, (line text, line values). The line text - # is a string that will be printed, and line values is a tuple of - # sortable values. - lines = [] - - for (fr, analysis) in self.fr_analysis: - nums = analysis.numbers - - args = (fr.relative_filename(), nums.n_statements, nums.n_missing) - if self.branches: - args += (nums.n_branches, nums.n_partial_branches) - args += (nums.pc_covered_str,) - if self.config.show_missing: - args += (analysis.missing_formatted(branches=True),) - text = fmt_coverage % args - # Add numeric percent coverage so that sorting makes sense. - args += (nums.pc_covered,) - lines.append((text, args)) - - # Sort the lines and write them out. - sort_option = (self.config.sort or "name").lower() - reverse = False - if sort_option[0] == '-': - reverse = True - sort_option = sort_option[1:] - elif sort_option[0] == '+': - sort_option = sort_option[1:] - - if sort_option == "name": - lines = human_sorted_items(lines, reverse=reverse) - else: - position = column_order.get(sort_option) - if position is None: - raise ConfigError(f"Invalid sorting option: {self.config.sort!r}") - lines.sort(key=lambda l: (l[1][position], l[0]), reverse=reverse) - - for line in lines: - self.writeout(line[0]) - - # Write a TOTAL line if we had at least one file. - if self.total.n_files > 0: - self.writeout(rule) - args = ("TOTAL", self.total.n_statements, self.total.n_missing) - if self.branches: - args += (self.total.n_branches, self.total.n_partial_branches) - args += (self.total.pc_covered_str,) - if self.config.show_missing: - args += ("",) - self.writeout(fmt_coverage % args) - - # Write other final lines. - if not self.total.n_files and not self.skipped_count: - raise NoDataError("No data to report.") - - if self.config.skip_covered and self.skipped_count: - self.writeout( - fmt_skip_covered % (self.skipped_count, 's' if self.skipped_count > 1 else '') - ) - if self.config.skip_empty and self.empty_count: - self.writeout( - fmt_skip_empty % (self.empty_count, 's' if self.empty_count > 1 else '') - ) - - return self.total.n_statements and self.total.pc_covered - - def report_one_file(self, fr, analysis): - """Report on just one file, the callback from report().""" - nums = analysis.numbers - self.total += nums - - no_missing_lines = (nums.n_missing == 0) - no_missing_branches = (nums.n_partial_branches == 0) - if self.config.skip_covered and no_missing_lines and no_missing_branches: - # Don't report on 100% files. - self.skipped_count += 1 - elif self.config.skip_empty and nums.n_statements == 0: - # Don't report on empty files. - self.empty_count += 1 - else: - self.fr_analysis.append((fr, analysis)) diff -Nru python-coverage-6.5.0+dfsg1/coverage/templite.py python-coverage-7.2.7+dfsg1/coverage/templite.py --- python-coverage-6.5.0+dfsg1/coverage/templite.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/templite.py 2023-05-29 19:46:30.000000000 +0000 @@ -10,8 +10,14 @@ # Coincidentally named the same as http://code.activestate.com/recipes/496702/ +from __future__ import annotations + import re +from typing import ( + Any, Callable, Dict, List, NoReturn, Optional, Set, Union, cast, +) + class TempliteSyntaxError(ValueError): """Raised when a template has a syntax error.""" @@ -26,14 +32,14 @@ class CodeBuilder: """Build source code conveniently.""" - def __init__(self, indent=0): - self.code = [] + def __init__(self, indent: int = 0) -> None: + self.code: List[Union[str, CodeBuilder]] = [] self.indent_level = indent - def __str__(self): + def __str__(self) -> str: return "".join(str(c) for c in self.code) - def add_line(self, line): + def add_line(self, line: str) -> None: """Add a line of source to the code. Indentation and newline will be added for you, don't provide them. @@ -41,7 +47,7 @@ """ self.code.extend([" " * self.indent_level, line, "\n"]) - def add_section(self): + def add_section(self) -> CodeBuilder: """Add a section, a sub-CodeBuilder.""" section = CodeBuilder(self.indent_level) self.code.append(section) @@ -49,22 +55,22 @@ INDENT_STEP = 4 # PEP8 says so! - def indent(self): + def indent(self) -> None: """Increase the current indent for following lines.""" self.indent_level += self.INDENT_STEP - def dedent(self): + def dedent(self) -> None: """Decrease the current indent for following lines.""" self.indent_level -= self.INDENT_STEP - def get_globals(self): + def get_globals(self) -> Dict[str, Any]: """Execute the code, and return a dict of globals it defines.""" # A check that the caller really finished all the blocks they started. assert self.indent_level == 0 # Get the Python source as a single string. python_source = str(self) # Execute the source, defining globals, and return them. - global_namespace = {} + global_namespace: Dict[str, Any] = {} exec(python_source, global_namespace) return global_namespace @@ -92,7 +98,7 @@ and joined. Be careful, this could join words together! Any of these constructs can have a hyphen at the end (`-}}`, `-%}`, `-#}`), - which will collapse the whitespace following the tag. + which will collapse the white space following the tag. Construct a Templite with the template text, then use `render` against a dictionary context to create a finished string:: @@ -103,15 +109,15 @@

You are interested in {{topic}}.

{% endif %} ''', - {'upper': str.upper}, + {"upper": str.upper}, ) text = templite.render({ - 'name': "Ned", - 'topics': ['Python', 'Geometry', 'Juggling'], + "name": "Ned", + "topics": ["Python", "Geometry", "Juggling"], }) """ - def __init__(self, text, *contexts): + def __init__(self, text: str, *contexts: Dict[str, Any]) -> None: """Construct a Templite with the given `text`. `contexts` are dictionaries of values to use for future renderings. @@ -122,8 +128,8 @@ for context in contexts: self.context.update(context) - self.all_vars = set() - self.loop_vars = set() + self.all_vars: Set[str] = set() + self.loop_vars: Set[str] = set() # We construct a function in source form, then compile it and hold onto # it, and execute it to render the template. @@ -137,9 +143,9 @@ code.add_line("extend_result = result.extend") code.add_line("to_str = str") - buffered = [] + buffered: List[str] = [] - def flush_output(): + def flush_output() -> None: """Force `buffered` to the code builder.""" if len(buffered) == 1: code.add_line("append_result(%s)" % buffered[0]) @@ -155,37 +161,37 @@ squash = in_joined = False for token in tokens: - if token.startswith('{'): + if token.startswith("{"): start, end = 2, -2 - squash = (token[-3] == '-') + squash = (token[-3] == "-") if squash: end = -3 - if token.startswith('{#'): + if token.startswith("{#"): # Comment: ignore it and move on. continue - elif token.startswith('{{'): + elif token.startswith("{{"): # An expression to evaluate. expr = self._expr_code(token[start:end].strip()) buffered.append("to_str(%s)" % expr) else: - # token.startswith('{%') + # token.startswith("{%") # Action tag: split into words and parse further. flush_output() words = token[start:end].strip().split() - if words[0] == 'if': + if words[0] == "if": # An if statement: evaluate the expression to determine if. if len(words) != 2: self._syntax_error("Don't understand if", token) - ops_stack.append('if') + ops_stack.append("if") code.add_line("if %s:" % self._expr_code(words[1])) code.indent() - elif words[0] == 'for': + elif words[0] == "for": # A loop: iterate over expression result. - if len(words) != 4 or words[2] != 'in': + if len(words) != 4 or words[2] != "in": self._syntax_error("Don't understand for", token) - ops_stack.append('for') + ops_stack.append("for") self._variable(words[1], self.loop_vars) code.add_line( "for c_{} in {}:".format( @@ -194,10 +200,10 @@ ) ) code.indent() - elif words[0] == 'joined': - ops_stack.append('joined') + elif words[0] == "joined": + ops_stack.append("joined") in_joined = True - elif words[0].startswith('end'): + elif words[0].startswith("end"): # Endsomething. Pop the ops stack. if len(words) != 1: self._syntax_error("Don't understand end", token) @@ -207,7 +213,7 @@ start_what = ops_stack.pop() if start_what != end_what: self._syntax_error("Mismatched end tag", end_what) - if end_what == 'joined': + if end_what == "joined": in_joined = False else: code.dedent() @@ -230,11 +236,17 @@ for var_name in self.all_vars - self.loop_vars: vars_code.add_line(f"c_{var_name} = context[{var_name!r}]") - code.add_line('return "".join(result)') + code.add_line("return ''.join(result)") code.dedent() - self._render_function = code.get_globals()['render_function'] + self._render_function = cast( + Callable[ + [Dict[str, Any], Callable[..., Any]], + str + ], + code.get_globals()["render_function"], + ) - def _expr_code(self, expr): + def _expr_code(self, expr: str) -> str: """Generate a Python expression for `expr`.""" if "|" in expr: pipes = expr.split("|") @@ -252,11 +264,11 @@ code = "c_%s" % expr return code - def _syntax_error(self, msg, thing): + def _syntax_error(self, msg: str, thing: Any) -> NoReturn: """Raise a syntax error using `msg`, and showing `thing`.""" raise TempliteSyntaxError(f"{msg}: {thing!r}") - def _variable(self, name, vars_set): + def _variable(self, name: str, vars_set: Set[str]) -> None: """Track that `name` is used as a variable. Adds the name to `vars_set`, a set of variable names. @@ -268,7 +280,7 @@ self._syntax_error("Not a valid name", name) vars_set.add(name) - def render(self, context=None): + def render(self, context: Optional[Dict[str, Any]] = None) -> str: """Render this template by applying it to `context`. `context` is a dictionary of values to use in this rendering. @@ -280,7 +292,7 @@ render_context.update(context) return self._render_function(render_context, self._do_dots) - def _do_dots(self, value, *dots): + def _do_dots(self, value: Any, *dots: str) -> Any: """Evaluate dotted expressions at run-time.""" for dot in dots: try: diff -Nru python-coverage-6.5.0+dfsg1/coverage/tomlconfig.py python-coverage-7.2.7+dfsg1/coverage/tomlconfig.py --- python-coverage-6.5.0+dfsg1/coverage/tomlconfig.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/tomlconfig.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,25 +3,25 @@ """TOML configuration support for coverage.py""" -import configparser +from __future__ import annotations + import os import re +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, TypeVar + from coverage import env from coverage.exceptions import ConfigError from coverage.misc import import_third_party, substitute_variables +from coverage.types import TConfigSectionOut, TConfigValueOut if env.PYVERSION >= (3, 11, 0, "alpha", 7): import tomllib # pylint: disable=import-error + has_tomllib = True else: # TOML support on Python 3.10 and below is an install-time extra option. - # (Import typing is here because import_third_party will unload any module - # that wasn't already imported. tomli imports typing, and if we unload it, - # later it's imported again, and on Python 3.6, this causes infinite - # recursion.) - import typing # pylint: disable=unused-import - tomllib = import_third_party("tomli") + tomllib, has_tomllib = import_third_party("tomli") class TomlDecodeError(Exception): @@ -29,6 +29,8 @@ pass +TWant = TypeVar("TWant") + class TomlConfigParser: """TOML file reading with the interface of HandyConfigParser.""" @@ -36,11 +38,11 @@ # need for docstrings. # pylint: disable=missing-function-docstring - def __init__(self, our_file): + def __init__(self, our_file: bool) -> None: self.our_file = our_file - self.data = None + self.data: Dict[str, Any] = {} - def read(self, filenames): + def read(self, filenames: Iterable[str]) -> List[str]: # RawConfigParser takes a filename or list of filenames, but we only # ever call this with a single filename. assert isinstance(filenames, (bytes, str, os.PathLike)) @@ -51,22 +53,21 @@ toml_text = fp.read() except OSError: return [] - if tomllib is not None: - toml_text = substitute_variables(toml_text, os.environ) + if has_tomllib: try: self.data = tomllib.loads(toml_text) except tomllib.TOMLDecodeError as err: raise TomlDecodeError(str(err)) from err return [filename] else: - has_toml = re.search(r"^\[tool\.coverage\.", toml_text, flags=re.MULTILINE) + has_toml = re.search(r"^\[tool\.coverage(\.|])", toml_text, flags=re.MULTILINE) if self.our_file or has_toml: # Looks like they meant to read TOML, but we can't read it. msg = "Can't read {!r} without TOML support. Install with [toml] extra" raise ConfigError(msg.format(filename)) return [] - def _get_section(self, section): + def _get_section(self, section: str) -> Tuple[Optional[str], Optional[TConfigSectionOut]]: """Get a section from the data. Arguments: @@ -79,8 +80,6 @@ """ prefixes = ["tool.coverage."] - if self.our_file: - prefixes.append("") for prefix in prefixes: real_section = prefix + section parts = real_section.split(".") @@ -95,60 +94,101 @@ return None, None return real_section, data - def _get(self, section, option): + def _get(self, section: str, option: str) -> Tuple[str, TConfigValueOut]: """Like .get, but returns the real section name and the value.""" name, data = self._get_section(section) if data is None: - raise configparser.NoSectionError(section) + raise ConfigError(f"No section: {section!r}") + assert name is not None try: - return name, data[option] - except KeyError as exc: - raise configparser.NoOptionError(option, name) from exc + value = data[option] + except KeyError: + raise ConfigError(f"No option {option!r} in section: {name!r}") from None + return name, value + + def _get_single(self, section: str, option: str) -> Any: + """Get a single-valued option. + + Performs environment substitution if the value is a string. Other types + will be converted later as needed. + """ + name, value = self._get(section, option) + if isinstance(value, str): + value = substitute_variables(value, os.environ) + return name, value - def has_option(self, section, option): + def has_option(self, section: str, option: str) -> bool: _, data = self._get_section(section) if data is None: return False return option in data - def has_section(self, section): + def real_section(self, section: str) -> Optional[str]: name, _ = self._get_section(section) return name - def options(self, section): + def has_section(self, section: str) -> bool: + name, _ = self._get_section(section) + return bool(name) + + def options(self, section: str) -> List[str]: _, data = self._get_section(section) if data is None: - raise configparser.NoSectionError(section) + raise ConfigError(f"No section: {section!r}") return list(data.keys()) - def get_section(self, section): + def get_section(self, section: str) -> TConfigSectionOut: _, data = self._get_section(section) - return data + return data or {} - def get(self, section, option): - _, value = self._get(section, option) + def get(self, section: str, option: str) -> Any: + _, value = self._get_single(section, option) return value - def _check_type(self, section, option, value, type_, type_desc): - if not isinstance(value, type_): - raise ValueError( - 'Option {!r} in section {!r} is not {}: {!r}' - .format(option, section, type_desc, value) - ) + def _check_type( + self, + section: str, + option: str, + value: Any, + type_: Type[TWant], + converter: Optional[Callable[[Any], TWant]], + type_desc: str, + ) -> TWant: + """Check that `value` has the type we want, converting if needed. - def getboolean(self, section, option): - name, value = self._get(section, option) - self._check_type(name, option, value, bool, "a boolean") - return value + Returns the resulting value of the desired type. + """ + if isinstance(value, type_): + return value + if isinstance(value, str) and converter is not None: + try: + return converter(value) + except Exception as e: + raise ValueError( + f"Option [{section}]{option} couldn't convert to {type_desc}: {value!r}" + ) from e + raise ValueError( + f"Option [{section}]{option} is not {type_desc}: {value!r}" + ) + + def getboolean(self, section: str, option: str) -> bool: + name, value = self._get_single(section, option) + bool_strings = {"true": True, "false": False} + return self._check_type(name, option, value, bool, bool_strings.__getitem__, "a boolean") - def getlist(self, section, option): + def _get_list(self, section: str, option: str) -> Tuple[str, List[str]]: + """Get a list of strings, substituting environment variables in the elements.""" name, values = self._get(section, option) - self._check_type(name, option, values, list, "a list") + values = self._check_type(name, option, values, list, None, "a list") + values = [substitute_variables(value, os.environ) for value in values] + return name, values + + def getlist(self, section: str, option: str) -> List[str]: + _, values = self._get_list(section, option) return values - def getregexlist(self, section, option): - name, values = self._get(section, option) - self._check_type(name, option, values, list, "a list") + def getregexlist(self, section: str, option: str) -> List[str]: + name, values = self._get_list(section, option) for value in values: value = value.strip() try: @@ -157,14 +197,12 @@ raise ConfigError(f"Invalid [{name}].{option} value {value!r}: {e}") from e return values - def getint(self, section, option): - name, value = self._get(section, option) - self._check_type(name, option, value, int, "an integer") - return value + def getint(self, section: str, option: str) -> int: + name, value = self._get_single(section, option) + return self._check_type(name, option, value, int, int, "an integer") - def getfloat(self, section, option): - name, value = self._get(section, option) + def getfloat(self, section: str, option: str) -> float: + name, value = self._get_single(section, option) if isinstance(value, int): value = float(value) - self._check_type(name, option, value, float, "a float") - return value + return self._check_type(name, option, value, float, float, "a float") diff -Nru python-coverage-6.5.0+dfsg1/coverage/tracer.pyi python-coverage-7.2.7+dfsg1/coverage/tracer.pyi --- python-coverage-6.5.0+dfsg1/coverage/tracer.pyi 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/tracer.pyi 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,35 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +from typing import Any, Dict + +from coverage.types import TFileDisposition, TTraceData, TTraceFn, TTracer + +class CFileDisposition(TFileDisposition): + canonical_filename: Any + file_tracer: Any + has_dynamic_filename: Any + original_filename: Any + reason: Any + source_filename: Any + trace: Any + def __init__(self) -> None: ... + +class CTracer(TTracer): + check_include: Any + concur_id_func: Any + data: TTraceData + disable_plugin: Any + file_tracers: Any + should_start_context: Any + should_trace: Any + should_trace_cache: Any + switch_context: Any + trace_arcs: Any + warn: Any + def __init__(self) -> None: ... + def activity(self) -> bool: ... + def get_stats(self) -> Dict[str, int]: ... + def reset_activity(self) -> Any: ... + def start(self) -> TTraceFn: ... + def stop(self) -> None: ... diff -Nru python-coverage-6.5.0+dfsg1/coverage/types.py python-coverage-7.2.7+dfsg1/coverage/types.py --- python-coverage-6.5.0+dfsg1/coverage/types.py 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/types.py 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,197 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +""" +Types for use throughout coverage.py. +""" + +from __future__ import annotations + +import os +import pathlib + +from types import FrameType, ModuleType +from typing import ( + Any, Callable, Dict, Iterable, List, Mapping, Optional, Set, Tuple, Type, Union, + TYPE_CHECKING, +) + +if TYPE_CHECKING: + # Protocol is new in 3.8. PYVERSIONS + from typing import Protocol + + from coverage.plugin import FileTracer + +else: + class Protocol: # pylint: disable=missing-class-docstring + pass + +## File paths + +# For arguments that are file paths: +if TYPE_CHECKING: + FilePath = Union[str, os.PathLike[str]] +else: + # PathLike < python3.9 doesn't support subscription + FilePath = Union[str, os.PathLike] +# For testing FilePath arguments +FilePathClasses = [str, pathlib.Path] +FilePathType = Union[Type[str], Type[pathlib.Path]] + +## Python tracing + +class TTraceFn(Protocol): + """A Python trace function.""" + def __call__( + self, + frame: FrameType, + event: str, + arg: Any, + lineno: Optional[TLineNo] = None # Our own twist, see collector.py + ) -> Optional[TTraceFn]: + ... + +## Coverage.py tracing + +# Line numbers are pervasive enough that they deserve their own type. +TLineNo = int + +TArc = Tuple[TLineNo, TLineNo] + +class TFileDisposition(Protocol): + """A simple value type for recording what to do with a file.""" + + original_filename: str + canonical_filename: str + source_filename: Optional[str] + trace: bool + reason: str + file_tracer: Optional[FileTracer] + has_dynamic_filename: bool + + +# When collecting data, we use a dictionary with a few possible shapes. The +# keys are always file names. +# - If measuring line coverage, the values are sets of line numbers. +# - If measuring arcs in the Python tracer, the values are sets of arcs (pairs +# of line numbers). +# - If measuring arcs in the C tracer, the values are sets of packed arcs (two +# line numbers combined into one integer). + +TTraceFileData = Union[Set[TLineNo], Set[TArc], Set[int]] + +TTraceData = Dict[str, TTraceFileData] + +class TTracer(Protocol): + """Either CTracer or PyTracer.""" + + data: TTraceData + trace_arcs: bool + should_trace: Callable[[str, FrameType], TFileDisposition] + should_trace_cache: Mapping[str, Optional[TFileDisposition]] + should_start_context: Optional[Callable[[FrameType], Optional[str]]] + switch_context: Optional[Callable[[Optional[str]], None]] + warn: TWarnFn + + def __init__(self) -> None: + ... + + def start(self) -> TTraceFn: + """Start this tracer, returning a trace function.""" + + def stop(self) -> None: + """Stop this tracer.""" + + def activity(self) -> bool: + """Has there been any activity?""" + + def reset_activity(self) -> None: + """Reset the activity() flag.""" + + def get_stats(self) -> Optional[Dict[str, int]]: + """Return a dictionary of statistics, or None.""" + +## Coverage + +# Many places use kwargs as Coverage kwargs. +TCovKwargs = Any + + +## Configuration + +# One value read from a config file. +TConfigValueIn = Optional[Union[bool, int, float, str, Iterable[str]]] +TConfigValueOut = Optional[Union[bool, int, float, str, List[str]]] +# An entire config section, mapping option names to values. +TConfigSectionIn = Mapping[str, TConfigValueIn] +TConfigSectionOut = Mapping[str, TConfigValueOut] + +class TConfigurable(Protocol): + """Something that can proxy to the coverage configuration settings.""" + + def get_option(self, option_name: str) -> Optional[TConfigValueOut]: + """Get an option from the configuration. + + `option_name` is a colon-separated string indicating the section and + option name. For example, the ``branch`` option in the ``[run]`` + section of the config file would be indicated with `"run:branch"`. + + Returns the value of the option. + + """ + + def set_option(self, option_name: str, value: Union[TConfigValueIn, TConfigSectionIn]) -> None: + """Set an option in the configuration. + + `option_name` is a colon-separated string indicating the section and + option name. For example, the ``branch`` option in the ``[run]`` + section of the config file would be indicated with `"run:branch"`. + + `value` is the new value for the option. + + """ + +class TPluginConfig(Protocol): + """Something that can provide options to a plugin.""" + + def get_plugin_options(self, plugin: str) -> TConfigSectionOut: + """Get the options for a plugin.""" + + +## Parsing + +TMorf = Union[ModuleType, str] + +TSourceTokenLines = Iterable[List[Tuple[str, str]]] + +## Plugins + +class TPlugin(Protocol): + """What all plugins have in common.""" + _coverage_plugin_name: str + _coverage_enabled: bool + + +## Debugging + +class TWarnFn(Protocol): + """A callable warn() function.""" + def __call__(self, msg: str, slug: Optional[str] = None, once: bool = False) -> None: + ... + + +class TDebugCtl(Protocol): + """A DebugControl object, or something like it.""" + + def should(self, option: str) -> bool: + """Decide whether to output debug information in category `option`.""" + + def write(self, msg: str) -> None: + """Write a line of debug output.""" + + +class TWritable(Protocol): + """Anything that can be written to.""" + + def write(self, msg: str) -> None: + """Write a message.""" diff -Nru python-coverage-6.5.0+dfsg1/coverage/version.py python-coverage-7.2.7+dfsg1/coverage/version.py --- python-coverage-6.5.0+dfsg1/coverage/version.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/version.py 2023-05-29 19:46:30.000000000 +0000 @@ -4,28 +4,47 @@ """The version and URL for coverage.py""" # This file is exec'ed in setup.py, don't import anything! -# Same semantics as sys.version_info. -version_info = (6, 5, 0, "final", 0) +from __future__ import annotations - -def _make_version(major, minor, micro, releaselevel, serial): +# version_info: same semantics as sys.version_info. +# _dev: the .devN suffix if any. +version_info = (7, 2, 7, "final", 0) +_dev = 0 + + +def _make_version( + major: int, + minor: int, + micro: int, + releaselevel: str = "final", + serial: int = 0, + dev: int = 0, +) -> str: """Create a readable version string from version_info tuple components.""" - assert releaselevel in ['alpha', 'beta', 'candidate', 'final'] + assert releaselevel in ["alpha", "beta", "candidate", "final"] version = "%d.%d.%d" % (major, minor, micro) - if releaselevel != 'final': - short = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc'}[releaselevel] + if releaselevel != "final": + short = {"alpha": "a", "beta": "b", "candidate": "rc"}[releaselevel] version += f"{short}{serial}" + if dev != 0: + version += f".dev{dev}" return version -def _make_url(major, minor, micro, releaselevel, serial): +def _make_url( + major: int, + minor: int, + micro: int, + releaselevel: str, + serial: int = 0, + dev: int = 0, +) -> str: """Make the URL people should start at for this version of coverage.py.""" - url = "https://coverage.readthedocs.io" - if releaselevel != 'final': - # For pre-releases, use a version-specific URL. - url += "/en/" + _make_version(major, minor, micro, releaselevel, serial) - return url + return ( + "https://coverage.readthedocs.io/en/" + + _make_version(major, minor, micro, releaselevel, serial, dev) + ) -__version__ = _make_version(*version_info) -__url__ = _make_url(*version_info) +__version__ = _make_version(*version_info, _dev) +__url__ = _make_url(*version_info, _dev) diff -Nru python-coverage-6.5.0+dfsg1/coverage/xmlreport.py python-coverage-7.2.7+dfsg1/coverage/xmlreport.py --- python-coverage-6.5.0+dfsg1/coverage/xmlreport.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage/xmlreport.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,28 +3,55 @@ """XML reporting for coverage.py""" +from __future__ import annotations + import os import os.path import sys import time import xml.dom.minidom -from coverage import __url__, __version__, files +from dataclasses import dataclass +from typing import Any, Dict, IO, Iterable, Optional, TYPE_CHECKING + +from coverage import __version__, files from coverage.misc import isolate_module, human_sorted, human_sorted_items -from coverage.report import get_analysis_to_report +from coverage.plugin import FileReporter +from coverage.report_core import get_analysis_to_report +from coverage.results import Analysis +from coverage.types import TMorf +from coverage.version import __url__ + +if TYPE_CHECKING: + from coverage import Coverage os = isolate_module(os) -DTD_URL = 'https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd' +DTD_URL = "https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd" -def rate(hit, num): +def rate(hit: int, num: int) -> str: """Return the fraction of `hit`/`num`, as a string.""" if num == 0: return "1" else: - return "%.4g" % (float(hit) / num) + return "%.4g" % (hit / num) + + +@dataclass +class PackageData: + """Data we keep about each "package" (in Java terms).""" + elements: Dict[str, xml.dom.minidom.Element] + hits: int + lines: int + br_hits: int + branches: int + + +def appendChild(parent: Any, child: Any) -> None: + """Append a child to a parent, in a way mypy will shut up about.""" + parent.appendChild(child) class XmlReporter: @@ -32,7 +59,7 @@ report_type = "XML report" - def __init__(self, coverage): + def __init__(self, coverage: Coverage) -> None: self.coverage = coverage self.config = self.coverage.config @@ -40,13 +67,15 @@ if self.config.source: for src in self.config.source: if os.path.exists(src): - if not self.config.relative_files: + if self.config.relative_files: + src = src.rstrip(r"\/") + else: src = files.canonical_filename(src) self.source_paths.add(src) - self.packages = {} - self.xml_out = None + self.packages: Dict[str, PackageData] = {} + self.xml_out: xml.dom.minidom.Document - def report(self, morfs, outfile=None): + def report(self, morfs: Optional[Iterable[TMorf]], outfile: Optional[IO[str]] = None) -> float: """Generate a Cobertura-compatible XML report for `morfs`. `morfs` is a list of modules or file names. @@ -60,6 +89,7 @@ # Create the DOM that will store the data. impl = xml.dom.minidom.getDOMImplementation() + assert impl is not None self.xml_out = impl.createDocument(None, "coverage", None) # Write header stuff. @@ -81,9 +111,9 @@ # Populate the XML DOM with the source info. for path in human_sorted(self.source_paths): xsource = self.xml_out.createElement("source") - xsources.appendChild(xsource) + appendChild(xsources, xsource) txt = self.xml_out.createTextNode(path) - xsource.appendChild(txt) + appendChild(xsource, txt) lnum_tot, lhits_tot = 0, 0 bnum_tot, bhits_tot = 0, 0 @@ -93,26 +123,25 @@ # Populate the XML DOM with the package info. for pkg_name, pkg_data in human_sorted_items(self.packages.items()): - class_elts, lhits, lnum, bhits, bnum = pkg_data xpackage = self.xml_out.createElement("package") - xpackages.appendChild(xpackage) + appendChild(xpackages, xpackage) xclasses = self.xml_out.createElement("classes") - xpackage.appendChild(xclasses) - for _, class_elt in human_sorted_items(class_elts.items()): - xclasses.appendChild(class_elt) - xpackage.setAttribute("name", pkg_name.replace(os.sep, '.')) - xpackage.setAttribute("line-rate", rate(lhits, lnum)) + appendChild(xpackage, xclasses) + for _, class_elt in human_sorted_items(pkg_data.elements.items()): + appendChild(xclasses, class_elt) + xpackage.setAttribute("name", pkg_name.replace(os.sep, ".")) + xpackage.setAttribute("line-rate", rate(pkg_data.hits, pkg_data.lines)) if has_arcs: - branch_rate = rate(bhits, bnum) + branch_rate = rate(pkg_data.br_hits, pkg_data.branches) else: branch_rate = "0" xpackage.setAttribute("branch-rate", branch_rate) xpackage.setAttribute("complexity", "0") - lnum_tot += lnum - lhits_tot += lhits - bnum_tot += bnum - bhits_tot += bhits + lhits_tot += pkg_data.hits + lnum_tot += pkg_data.lines + bhits_tot += pkg_data.br_hits + bnum_tot += pkg_data.branches xcoverage.setAttribute("lines-valid", str(lnum_tot)) xcoverage.setAttribute("lines-covered", str(lhits_tot)) @@ -138,37 +167,38 @@ pct = 100.0 * (lhits_tot + bhits_tot) / denom return pct - def xml_file(self, fr, analysis, has_arcs): + def xml_file(self, fr: FileReporter, analysis: Analysis, has_arcs: bool) -> None: """Add to the XML report for a single file.""" if self.config.skip_empty: if analysis.numbers.n_statements == 0: return - # Create the 'lines' and 'package' XML elements, which + # Create the "lines" and "package" XML elements, which # are populated later. Note that a package == a directory. filename = fr.filename.replace("\\", "/") for source_path in self.source_paths: - source_path = files.canonical_filename(source_path) + if not self.config.relative_files: + source_path = files.canonical_filename(source_path) if filename.startswith(source_path.replace("\\", "/") + "/"): rel_name = filename[len(source_path)+1:] break else: - rel_name = fr.relative_filename() + rel_name = fr.relative_filename().replace("\\", "/") self.source_paths.add(fr.filename[:-len(rel_name)].rstrip(r"\/")) dirname = os.path.dirname(rel_name) or "." dirname = "/".join(dirname.split("/")[:self.config.xml_package_depth]) package_name = dirname.replace("/", ".") - package = self.packages.setdefault(package_name, [{}, 0, 0, 0, 0]) + package = self.packages.setdefault(package_name, PackageData({}, 0, 0, 0, 0)) - xclass = self.xml_out.createElement("class") + xclass: xml.dom.minidom.Element = self.xml_out.createElement("class") - xclass.appendChild(self.xml_out.createElement("methods")) + appendChild(xclass, self.xml_out.createElement("methods")) xlines = self.xml_out.createElement("lines") - xclass.appendChild(xlines) + appendChild(xclass, xlines) xclass.setAttribute("name", os.path.relpath(rel_name, dirname)) xclass.setAttribute("filename", rel_name.replace("\\", "/")) @@ -177,7 +207,7 @@ branch_stats = analysis.branch_stats() missing_branch_arcs = analysis.missing_branch_arcs() - # For each statement, create an XML 'line' element. + # For each statement, create an XML "line" element. for line in sorted(analysis.statements): xline = self.xml_out.createElement("line") xline.setAttribute("number", str(line)) @@ -197,7 +227,7 @@ if line in missing_branch_arcs: annlines = ["exit" if b < 0 else str(b) for b in missing_branch_arcs[line]] xline.setAttribute("missing-branches", ",".join(annlines)) - xlines.appendChild(xline) + appendChild(xlines, xline) class_lines = len(analysis.statements) class_hits = class_lines - len(analysis.missing) @@ -207,8 +237,8 @@ missing_branches = sum(t - k for t, k in branch_stats.values()) class_br_hits = class_branches - missing_branches else: - class_branches = 0.0 - class_br_hits = 0.0 + class_branches = 0 + class_br_hits = 0 # Finalize the statistics that are collected in the XML DOM. xclass.setAttribute("line-rate", rate(class_hits, class_lines)) @@ -218,13 +248,13 @@ branch_rate = "0" xclass.setAttribute("branch-rate", branch_rate) - package[0][rel_name] = xclass - package[1] += class_hits - package[2] += class_lines - package[3] += class_br_hits - package[4] += class_branches + package.elements[rel_name] = xclass + package.hits += class_hits + package.lines += class_lines + package.br_hits += class_br_hits + package.branches += class_branches -def serialize_xml(dom): +def serialize_xml(dom: xml.dom.minidom.Document) -> str: """Serialize a minidom node to XML.""" return dom.toprettyxml() diff -Nru python-coverage-6.5.0+dfsg1/coverage.egg-info/PKG-INFO python-coverage-7.2.7+dfsg1/coverage.egg-info/PKG-INFO --- python-coverage-6.5.0+dfsg1/coverage.egg-info/PKG-INFO 2022-09-29 16:36:48.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage.egg-info/PKG-INFO 2023-05-29 19:46:41.000000000 +0000 @@ -1,15 +1,16 @@ Metadata-Version: 2.1 Name: coverage -Version: 6.5.0 +Version: 7.2.7 Summary: Code coverage measurement for Python Home-page: https://github.com/nedbat/coveragepy -Author: Ned Batchelder and 161 others +Author: Ned Batchelder and 213 others Author-email: ned@nedbatchelder.com -License: Apache 2.0 -Project-URL: Documentation, https://coverage.readthedocs.io +License: Apache-2.0 +Project-URL: Documentation, https://coverage.readthedocs.io/en/7.2.7 Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=pypi Project-URL: Issues, https://github.com/nedbat/coveragepy/issues -Project-URL: Twitter, https://twitter.com/coveragepy +Project-URL: Mastodon, https://hachyderm.io/@coveragepy +Project-URL: Mastodon (nedbat), https://hachyderm.io/@nedbat Keywords: code coverage testing Classifier: Environment :: Console Classifier: Intended Audience :: Developers @@ -22,6 +23,7 @@ Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Quality Assurance @@ -51,8 +53,8 @@ | |test-status| |quality-status| |docs| |metacov| | |kit| |downloads| |format| |repos| | |stars| |forks| |contributors| -| |tidelift| |core-infrastructure| |open-ssf| -| |sponsor| |twitter-coveragepy| |twitter-nedbat| +| |core-infrastructure| |open-ssf| |snyk| +| |tidelift| |sponsor| |mastodon-coveragepy| |mastodon-nedbat| Coverage.py measures code coverage, typically during test execution. It uses the code analysis tools and tracing hooks provided in the Python standard @@ -62,17 +64,23 @@ .. PYVERSIONS -* CPython 3.7 through 3.11.0 rc2. -* PyPy3 7.3.8. +* CPython 3.7 through 3.12.0b1 +* PyPy3 7.3.11. Documentation is on `Read the Docs`_. Code repository and issue tracker are on `GitHub`_. -.. _Read the Docs: https://coverage.readthedocs.io/ +.. _Read the Docs: https://coverage.readthedocs.io/en/7.2.7/ .. _GitHub: https://github.com/nedbat/coveragepy +**New in 7.x:** +improved data combining; +``[run] exclude_also`` setting; +``report --format=``; +type annotations. -**New in 6.x:** dropped support for Python 2.7, 3.5, and 3.6; +**New in 6.x:** +dropped support for Python 2.7, 3.5, and 3.6; write data on SIGTERM; added support for 3.10 match/case statements. @@ -99,9 +107,10 @@ Getting Started --------------- -See the `Quick Start section`_ of the docs. +Looking to run ``coverage`` on your test suite? See the `Quick Start section`_ +of the docs. -.. _Quick Start section: https://coverage.readthedocs.io/#quick-start +.. _Quick Start section: https://coverage.readthedocs.io/en/7.2.7/#quick-start Change history @@ -109,7 +118,7 @@ The complete history of changes is on the `change history page`_. -.. _change history page: https://coverage.readthedocs.io/en/latest/changes.html +.. _change history page: https://coverage.readthedocs.io/en/7.2.7/changes.html Code of Conduct @@ -125,9 +134,10 @@ Contributing ------------ -See the `Contributing section`_ of the docs. +Found a bug? Want to help improve the code or documentation? See the +`Contributing section`_ of the docs. -.. _Contributing section: https://coverage.readthedocs.io/en/latest/contributing.html +.. _Contributing section: https://coverage.readthedocs.io/en/7.2.7/contributing.html Security @@ -155,7 +165,7 @@ :target: https://github.com/nedbat/coveragepy/actions/workflows/quality.yml :alt: Quality check status .. |docs| image:: https://readthedocs.org/projects/coverage/badge/?version=latest&style=flat - :target: https://coverage.readthedocs.io/ + :target: https://coverage.readthedocs.io/en/7.2.7/ :alt: Documentation .. |kit| image:: https://badge.fury.io/py/coverage.svg :target: https://pypi.org/project/coverage/ @@ -193,12 +203,12 @@ .. |contributors| image:: https://img.shields.io/github/contributors/nedbat/coveragepy.svg?logo=github :target: https://github.com/nedbat/coveragepy/graphs/contributors :alt: Contributors -.. |twitter-coveragepy| image:: https://img.shields.io/twitter/follow/coveragepy.svg?label=coveragepy&style=flat&logo=twitter&logoColor=4FADFF - :target: https://twitter.com/coveragepy - :alt: coverage.py on Twitter -.. |twitter-nedbat| image:: https://img.shields.io/twitter/follow/nedbat.svg?label=nedbat&style=flat&logo=twitter&logoColor=4FADFF - :target: https://twitter.com/nedbat - :alt: nedbat on Twitter +.. |mastodon-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&link=https%3A%2F%2Fhachyderm.io%2F%40nedbat&url=https%3A%2F%2Fhachyderm.io%2Fusers%2Fnedbat%2Ffollowers.json&query=totalItems&label=@nedbat + :target: https://hachyderm.io/@nedbat + :alt: nedbat on Mastodon +.. |mastodon-coveragepy| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&link=https%3A%2F%2Fhachyderm.io%2F%40coveragepy&url=https%3A%2F%2Fhachyderm.io%2Fusers%2Fcoveragepy%2Ffollowers.json&query=totalItems&label=@coveragepy + :target: https://hachyderm.io/@coveragepy + :alt: coveragepy on Mastodon .. |sponsor| image:: https://img.shields.io/badge/%E2%9D%A4-Sponsor%20me-brightgreen?style=flat&logo=GitHub :target: https://github.com/sponsors/nedbat :alt: Sponsor me on GitHub @@ -208,3 +218,6 @@ .. |open-ssf| image:: https://api.securityscorecards.dev/projects/github.com/nedbat/coveragepy/badge :target: https://deps.dev/pypi/coverage :alt: OpenSSF Scorecard +.. |snyk| image:: https://snyk.io/advisor/python/coverage/badge.svg + :target: https://snyk.io/advisor/python/coverage + :alt: Snyk package health diff -Nru python-coverage-6.5.0+dfsg1/coverage.egg-info/SOURCES.txt python-coverage-7.2.7+dfsg1/coverage.egg-info/SOURCES.txt --- python-coverage-6.5.0+dfsg1/coverage.egg-info/SOURCES.txt 2022-09-29 16:36:48.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/coverage.egg-info/SOURCES.txt 2023-05-29 19:46:41.000000000 +0000 @@ -1,4 +1,5 @@ .editorconfig +.git-blame-ignore-revs .readthedocs.yml CHANGES.rst CONTRIBUTORS.txt @@ -13,7 +14,6 @@ metacov.ini pylintrc pyproject.toml -setup.cfg setup.py tox.ini .github/CODE_OF_CONDUCT.md @@ -33,7 +33,7 @@ ci/README.txt ci/comment_on_fixes.py ci/download_gha_artifacts.py -ci/github_releases.py +ci/ghrel_template.md.j2 ci/parse_relnotes.py ci/trigger_build_kits.py coverage/__init__.py @@ -63,14 +63,17 @@ coverage/phystokens.py coverage/plugin.py coverage/plugin_support.py +coverage/py.typed coverage/python.py coverage/pytracer.py coverage/report.py +coverage/report_core.py coverage/results.py coverage/sqldata.py -coverage/summary.py coverage/templite.py coverage/tomlconfig.py +coverage/tracer.pyi +coverage/types.py coverage/version.py coverage/xmlreport.py coverage.egg-info/PKG-INFO @@ -118,6 +121,7 @@ doc/howitworks.rst doc/index.rst doc/install.rst +doc/migrating.rst doc/plugins.rst doc/python-coverage.1.txt doc/requirements.in @@ -136,7 +140,6 @@ doc/sample_html/keybd_closed.png doc/sample_html/keybd_open.png lab/README.txt -lab/benchmark.py lab/bpo_prelude.py lab/branch_trace.py lab/branches.py @@ -153,10 +156,14 @@ lab/parser.py lab/platform_info.py lab/run_trace.py +lab/select_contexts.py lab/show_ast.py lab/show_platform.py lab/show_pyc.py lab/treetopy.sh +lab/benchmark/benchmark.py +lab/benchmark/empty.py +lab/benchmark/run.py lab/notes/bug1303.txt lab/notes/pypy-738-decorated-functions.txt requirements/dev.in @@ -166,6 +173,8 @@ requirements/light-threads.in requirements/light-threads.pip requirements/lint.pip +requirements/mypy.in +requirements/mypy.pip requirements/pins.pip requirements/pip-tools.in requirements/pip-tools.pip @@ -217,9 +226,10 @@ tests/test_process.py tests/test_python.py tests/test_report.py +tests/test_report_common.py +tests/test_report_core.py tests/test_results.py tests/test_setup.py -tests/test_summary.py tests/test_templite.py tests/test_testing.py tests/test_venv.py @@ -246,6 +256,8 @@ tests/gold/html/b_branch/index.html tests/gold/html/bom/bom_py.html tests/gold/html/bom/index.html +tests/gold/html/contexts/index.html +tests/gold/html/contexts/two_tests_py.html tests/gold/html/isolatin1/index.html tests/gold/html/isolatin1/isolatin1_py.html tests/gold/html/omit_1/index.html diff -Nru python-coverage-6.5.0+dfsg1/debian/changelog python-coverage-7.2.7+dfsg1/debian/changelog --- python-coverage-6.5.0+dfsg1/debian/changelog 2023-07-21 04:59:32.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/debian/changelog 2023-07-23 03:49:58.000000000 +0000 @@ -1,3 +1,20 @@ +python-coverage (7.2.7+dfsg1-1) unstable; urgency=medium + + * The “Adele Puglisi” release. + * Note that release 6.5.0+dfsg1-3 Closes: bug#1040850. + The previous changelog entry gave the incorrect bug report number. + * New upstream version. + Highlights since previous release: + * Changes to filename pattern matching and path remapping. + These might require changing configuration for Coverage.py. + See the “Migrating between versions” section of the documentation. + * New output formats for ‘report’. + * Empty source file no longer fails with ‘--fail-under’. + * Filename parameters in API now also accept a ‘pathlib.Path’ object. + * Refresh patches for new upstream version. + + -- Ben Finney Sun, 23 Jul 2023 13:49:58 +1000 + python-coverage (6.5.0+dfsg1-3) unstable; urgency=medium * Declare conformance to “Standards-Version: 4.6.2”. diff -Nru python-coverage-6.5.0+dfsg1/debian/copyright python-coverage-7.2.7+dfsg1/debian/copyright --- python-coverage-6.5.0+dfsg1/debian/copyright 2023-07-21 04:59:32.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/debian/copyright 2023-07-23 03:49:58.000000000 +0000 @@ -10,7 +10,7 @@ Files: * Copyright: - © 2004–2022 Ned Batchelder + © 2004–2023 Ned Batchelder © 2001 Gareth Rees License: Apache-2 License-Grant: diff -Nru python-coverage-6.5.0+dfsg1/debian/patches/01.omit-resource-files-from-distutils-setup.patch python-coverage-7.2.7+dfsg1/debian/patches/01.omit-resource-files-from-distutils-setup.patch --- python-coverage-6.5.0+dfsg1/debian/patches/01.omit-resource-files-from-distutils-setup.patch 2023-07-21 04:59:32.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/debian/patches/01.omit-resource-files-from-distutils-setup.patch 2023-07-23 03:49:58.000000000 +0000 @@ -5,21 +5,19 @@ of FHS, and these files should instead go to ‘/usr/share/…’. Bug-Debian: http://bugs.debian.org/721676 Author: Ben Finney -Last-Update: 2017-09-08 +Last-Update: 2032-07-23 -diff --git a/setup.py b/setup.py -index 7962c44d..6ac78b0a 100644 ---- a/setup.py -+++ b/setup.py -@@ -76,7 +76,6 @@ setup_args = dict( +diff --git old/setup.py new/setup.py +--- old/setup.py ++++ new/setup.py +@@ -87,7 +87,6 @@ setup_args = dict( package_data={ 'coverage': [ - 'htmlfiles/*.*', 'fullcoverage/*.*', + 'py.typed', ] - }, - Local variables: coding: utf-8 diff -Nru python-coverage-6.5.0+dfsg1/debian/patches/02.rename-public-programs.patch python-coverage-7.2.7+dfsg1/debian/patches/02.rename-public-programs.patch --- python-coverage-6.5.0+dfsg1/debian/patches/02.rename-public-programs.patch 2023-07-21 04:59:32.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/debian/patches/02.rename-public-programs.patch 2023-07-23 03:49:58.000000000 +0000 @@ -6,12 +6,12 @@ Created to work with “entry points” feature of Python's Distutils. Bug: https://bitbucket.org/ned/coveragepy/issue/272/ Author: Ben Finney -Last-Update: 2022-07-09 +Last-Update: 2023-07-23 diff --git old/setup.py new/setup.py --- old/setup.py +++ new/setup.py -@@ -97,12 +97,11 @@ setup_args = dict( +@@ -93,12 +93,11 @@ setup_args = dict( }, entry_points={ @@ -32,15 +32,16 @@ diff --git old/tests/coveragetest.py new/tests/coveragetest.py --- old/tests/coveragetest.py +++ new/tests/coveragetest.py -@@ -323,7 +323,7 @@ class CoverageTest( +@@ -380,7 +380,7 @@ class CoverageTest( # their new command name to the tests. This is here for them to override, # for example: # https://salsa.debian.org/debian/pkg-python-coverage/-/blob/master/debian/patches/02.rename-public-programs.patch - coverage_command = "coverage" + coverage_command = "python3-coverage" - def run_command(self, cmd): + def run_command(self, cmd: str) -> str: """Run the command-line `cmd` in a sub-process. + Local variables: coding: utf-8 diff -Nru python-coverage-6.5.0+dfsg1/doc/changes.rst python-coverage-7.2.7+dfsg1/doc/changes.rst --- python-coverage-6.5.0+dfsg1/doc/changes.rst 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/changes.rst 2023-05-29 19:46:30.000000000 +0000 @@ -6,7 +6,7 @@ .. The recent changes from the top-level file: .. include:: ../CHANGES.rst - :end-before: endchangesinclude + :end-before: scriv-end-here .. Older changes here: @@ -383,7 +383,7 @@ argument, `no_disk` (default: False). Setting it to True prevents writing any data to the disk. This is useful for transient data objects. -- Added the classmethod :meth:`.Coverage.current` to get the latest started +- Added the class method :meth:`.Coverage.current` to get the latest started Coverage instance. - Multiprocessing support in Python 3.8 was broken, but is now fixed. Closes @@ -556,7 +556,7 @@ - Development moved from `Bitbucket`_ to `GitHub`_. -- HTML files no longer have trailing and extra whitespace. +- HTML files no longer have trailing and extra white space. - The sort order in the HTML report is stored in local storage rather than cookies, closing `issue 611`_. Thanks, Federico Bond. @@ -794,7 +794,7 @@ also continue measurement. Both `issue 79`_ and `issue 448`_ described this problem, and have been fixed. -- Plugins can now find unexecuted files if they choose, by implementing the +- Plugins can now find un-executed files if they choose, by implementing the `find_executable_files` method. Thanks, Emil Madsen. - Minimal IronPython support. You should be able to run IronPython programs @@ -1202,7 +1202,7 @@ - The XML report now produces correct package names for modules found in directories specified with ``source=``. Fixes `issue 465`_. -- ``coverage report`` won't produce trailing whitespace. +- ``coverage report`` won't produce trailing white space. .. _issue 465: https://github.com/nedbat/coveragepy/issues/465 .. _issue 466: https://github.com/nedbat/coveragepy/issues/466 @@ -1532,7 +1532,7 @@ - Files with incorrect encoding declaration comments are no longer ignored by the reporting commands, fixing `issue 351`_. -- HTML reports now include a timestamp in the footer, closing `issue 299`_. +- HTML reports now include a time stamp in the footer, closing `issue 299`_. Thanks, Conrad Ho. - HTML reports now begrudgingly use double-quotes rather than single quotes, @@ -1685,7 +1685,7 @@ `issue 328`_. Thanks, Buck Evan. - The regex for matching exclusion pragmas has been fixed to allow more kinds - of whitespace, fixing `issue 334`_. + of white space, fixing `issue 334`_. - Made some PyPy-specific tweaks to improve speed under PyPy. Thanks, Alex Gaynor. @@ -1739,7 +1739,7 @@ `issue 285`_. Thanks, Chris Rose. - HTML reports no longer raise UnicodeDecodeError if a Python file has - undecodable characters, fixing `issue 303`_ and `issue 331`_. + un-decodable characters, fixing `issue 303`_ and `issue 331`_. - The annotate command will now annotate all files, not just ones relative to the current directory, fixing `issue 57`_. @@ -1791,7 +1791,7 @@ - Coverage.py properly supports .pyw files, fixing `issue 261`_. - Omitting files within a tree specified with the ``source`` option would - cause them to be incorrectly marked as unexecuted, as described in + cause them to be incorrectly marked as un-executed, as described in `issue 218`_. This is now fixed. - When specifying paths to alias together during data combining, you can now @@ -1802,7 +1802,7 @@ (``build/$BUILDNUM/src``). - Trying to create an XML report with no files to report on, would cause a - ZeroDivideError, but no longer does, fixing `issue 250`_. + ZeroDivisionError, but no longer does, fixing `issue 250`_. - When running a threaded program under the Python tracer, coverage.py no longer issues a spurious warning about the trace function changing: "Trace @@ -1905,7 +1905,7 @@ Thanks, Marcus Cobden. - Coverage percentage metrics are now computed slightly differently under - branch coverage. This means that completely unexecuted files will now + branch coverage. This means that completely un-executed files will now correctly have 0% coverage, fixing `issue 156`_. This also means that your total coverage numbers will generally now be lower if you are measuring branch coverage. @@ -2068,7 +2068,7 @@ - Now the exit status of your product code is properly used as the process status when running ``python -m coverage run ...``. Thanks, JT Olds. -- When installing into pypy, we no longer attempt (and fail) to compile +- When installing into PyPy, we no longer attempt (and fail) to compile the C tracer function, closing `issue 166`_. .. _issue 142: https://github.com/nedbat/coveragepy/issues/142 @@ -2234,9 +2234,10 @@ Version 3.4b2 — 2010-09-06 -------------------------- -- Completely unexecuted files can now be included in coverage results, reported - as 0% covered. This only happens if the --source option is specified, since - coverage.py needs guidance about where to look for source files. +- Completely un-executed files can now be included in coverage results, + reported as 0% covered. This only happens if the --source option is + specified, since coverage.py needs guidance about where to look for source + files. - The XML report output now properly includes a percentage for branch coverage, fixing `issue 65`_ and `issue 81`_. @@ -2374,7 +2375,7 @@ `config_file=False`. - Fixed a problem with nested loops having their branch possibilities - mischaracterized: `issue 39`_. + mis-characterized: `issue 39`_. - Added coverage.process_start to enable coverage measurement when Python starts. diff -Nru python-coverage-6.5.0+dfsg1/doc/cmd.rst python-coverage-7.2.7+dfsg1/doc/cmd.rst --- python-coverage-6.5.0+dfsg1/doc/cmd.rst 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/cmd.rst 2023-05-29 19:46:30.000000000 +0000 @@ -6,6 +6,12 @@ Running "make prebuild" will bring it up to date. .. [[[cog + # optparse wraps help to the COLUMNS value. Set it here to be sure it's + # consistent regardless of the environment. Has to be set before we + # import cmdline.py, which creates the optparse objects. + import os + os.environ["COLUMNS"] = "80" + import contextlib import io import re @@ -342,7 +348,7 @@ $ coverage combine -You can also name directories or files on the command line:: +You can also name directories or files to be combined on the command line:: $ coverage combine data1.dat windows_data_files/ @@ -364,19 +370,6 @@ runs, use the ``--append`` switch on the **combine** command. This behavior was the default before version 4.2. -To combine data for a source file, coverage has to find its data in each of the -data files. Different test runs may run the same source file from different -locations. For example, different operating systems will use different paths -for the same file, or perhaps each Python version is run from a different -subdirectory. Coverage needs to know that different file paths are actually -the same source file for reporting purposes. - -You can tell coverage.py how different source locations relate with a -``[paths]`` section in your configuration file (see :ref:`config_paths`). -It might be more convenient to use the ``[run] relative_files`` -setting to store relative file paths (see :ref:`relative_files -`). - If any of the data files can't be read, coverage.py will print a warning indicating the file and the problem. @@ -389,11 +382,10 @@ $ coverage combine --help Usage: coverage combine [options] ... - Combine data from multiple coverage files collected with 'run -p'. The - combined results are written to a single file representing the union of the - data. The positional arguments are data files or directories containing data - files. If no paths are provided, data files in the default data file's - directory are combined. + Combine data from multiple coverage files. The combined results are written to + a single file representing the union of the data. The positional arguments are + data files or directories containing data files. If no paths are provided, + data files in the default data file's directory are combined. Options: -a, --append Append coverage data to .coverage, otherwise it starts @@ -409,7 +401,29 @@ --rcfile=RCFILE Specify configuration file. By default '.coveragerc', 'setup.cfg', 'tox.ini', and 'pyproject.toml' are tried. [env: COVERAGE_RCFILE] -.. [[[end]]] (checksum: 0ac91b0781d7146b87953f09090dab92) +.. [[[end]]] (checksum: 0bdd83f647ee76363c955bedd9ddf749) + + +.. _cmd_combine_remapping: + +Re-mapping paths +................ + +To combine data for a source file, coverage has to find its data in each of the +data files. Different test runs may run the same source file from different +locations. For example, different operating systems will use different paths +for the same file, or perhaps each Python version is run from a different +subdirectory. Coverage needs to know that different file paths are actually +the same source file for reporting purposes. + +You can tell coverage.py how different source locations relate with a +``[paths]`` section in your configuration file (see :ref:`config_paths`). +It might be more convenient to use the ``[run] relative_files`` +setting to store relative file paths (see :ref:`relative_files +`). + +If data isn't combining properly, you can see details about the inner workings +with ``--debug=pathmap``. .. _cmd_erase: @@ -510,6 +524,8 @@ file. Defaults to '.coverage'. [env: COVERAGE_FILE] --fail-under=MIN Exit with a status of 2 if the total coverage is less than MIN. + --format=FORMAT Output format, either text (default), markdown, or + total. -i, --ignore-errors Ignore errors while reading source files. --include=PAT1,PAT2,... Include only files whose paths match one of these @@ -532,7 +548,7 @@ --rcfile=RCFILE Specify configuration file. By default '.coveragerc', 'setup.cfg', 'tox.ini', and 'pyproject.toml' are tried. [env: COVERAGE_RCFILE] -.. [[[end]]] (checksum: 2f8dde61bab2f44fbfe837aeae87dfd2) +.. [[[end]]] (checksum: 167272a29d9e7eb017a592a0e0747a06) The ``-m`` flag also shows the line numbers of missing statements:: @@ -583,6 +599,12 @@ The ``--sort`` option is the name of a column to sort the report by. +The ``--format`` option controls the style of the report. ``--format=text`` +creates plain text tables as shown above. ``--format=markdown`` creates +Markdown tables. ``--format=total`` writes out a single number, the total +coverage percentage as shown at the end of the tables, but without a percent +sign. + Other common reporting options are described above in :ref:`cmd_reporting`. These options can also be set in your .coveragerc file. See :ref:`Configuration: [report] `. @@ -602,9 +624,10 @@ __ https://nedbatchelder.com/files/sample_coverage_html/index.html -Lines are highlighted green for executed, red for missing, and gray for -excluded. The counts at the top of the file are buttons to turn on and off -the highlighting. +Lines are highlighted: green for executed, red for missing, and gray for +excluded. If you've used branch coverage, partial branches are yellow. The +colored counts at the top of the file are buttons to turn on and off the +highlighting. A number of keyboard shortcuts are available for navigating the report. Click the keyboard icon in the upper right to see the complete list. @@ -1001,7 +1024,7 @@ * ``multiproc``: log the start and stop of multiprocessing processes. * ``pathmap``: log the remapping of paths that happens during ``coverage - combine`` due to the ``[paths]`` setting. See :ref:`config_paths`. + combine``. See :ref:`config_paths`. * ``pid``: annotate all warnings and debug output with the process and thread ids. @@ -1009,7 +1032,8 @@ * ``plugin``: print information about plugin operations. * ``process``: show process creation information, and changes in the current - directory. + directory. This also writes a time stamp and command arguments into the data + file. * ``pybehave``: show the values of `internal flags `_ describing the behavior of the current version of Python. @@ -1033,7 +1057,9 @@ a comma-separated list of these options, or in the :ref:`config_run_debug` section of the .coveragerc file. -The debug output goes to stderr, unless the ``COVERAGE_DEBUG_FILE`` environment -variable names a different file, which will be appended to. -``COVERAGE_DEBUG_FILE`` accepts the special names ``stdout`` and ``stderr`` to -write to those destinations. +The debug output goes to stderr, unless the :ref:`config_run_debug_file` +setting or the ``COVERAGE_DEBUG_FILE`` environment variable names a different +file, which will be appended to. This can be useful because many test runners +capture output, which could hide important details. ``COVERAGE_DEBUG_FILE`` +accepts the special names ``stdout`` and ``stderr`` to write to those +destinations. diff -Nru python-coverage-6.5.0+dfsg1/doc/config.rst python-coverage-7.2.7+dfsg1/doc/config.rst --- python-coverage-6.5.0+dfsg1/doc/config.rst 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/config.rst 2023-05-29 19:46:30.000000000 +0000 @@ -31,10 +31,14 @@ configuration file is used. It will automatically read from "setup.cfg" or "tox.ini" if they exist. In this case, the section names have "coverage:" prefixed, so the ``[run]`` options described below will be found in the -``[coverage:run]`` section of the file. If coverage.py is installed with the -``toml`` extra (``pip install coverage[toml]``), it will automatically read -from "pyproject.toml". Configuration must be within the ``[tool.coverage]`` -section, for example, ``[tool.coverage.run]``. +``[coverage:run]`` section of the file. + +Coverage.py will read from "pyproject.toml" if TOML support is available, +either because you are running on Python 3.11 or later, or because you +installed with the ``toml`` extra (``pip install coverage[toml]``). +Configuration must be within the ``[tool.coverage]`` section, for example, +``[tool.coverage.run]``. Environment variable expansion in values is +available, but only within quoted strings, even for non-string values. Syntax @@ -75,10 +79,7 @@ [report] # Regexes for lines to exclude from consideration - exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover - + exclude_also = # Don't complain about missing debug-only code: def __repr__ if self\.debug @@ -199,6 +200,15 @@ ` for details. +.. _config_run_debug_file: + +[run] debug_file +................ + +(string) A file name to write debug output to. See :ref:`the run --debug +option ` for details. + + .. _config_run_dynamic_context: [run] dynamic_context @@ -218,14 +228,6 @@ details. -.. _config_run_note: - -[run] note -.......... - -(string) This is now obsolete. - - .. _config_run_omit: [run] omit @@ -259,9 +261,9 @@ [run] relative_files .................... -(*experimental*, boolean, default False) store relative file paths in the data -file. This makes it easier to measure code in one (or multiple) environments, -and then report in another. See :ref:`cmd_combine` for details. +(boolean, default False) store relative file paths in the data file. This +makes it easier to measure code in one (or multiple) environments, and then +report in another. See :ref:`cmd_combine` for details. Note that setting ``source`` has to be done in the configuration file rather than the command line for this option to work, since the reporting commands @@ -348,12 +350,18 @@ against the source file found at "src/module.py". If you specify more than one list of paths, they will be considered in order. -The first list that has a match will be used. +A file path will only be remapped if the result exists. If a path matches a +list, but the result doesn't exist, the next list will be tried. The first +list that has an existing result will be used. + +Remapping will also be done during reporting, but only within the single data +file being reported. Combining multiple files requires the ``combine`` +command. The ``--debug=pathmap`` option can be used to log details of the re-mapping of paths. See :ref:`the --debug option `. -See :ref:`cmd_combine` for more information. +See :ref:`cmd_combine_remapping` and :ref:`source_glob` for more information. .. _config_report: @@ -364,16 +372,31 @@ Settings common to many kinds of reporting. +.. _config_report_exclude_also: + +[report] exclude_also +..................... + +(multi-string) A list of regular expressions. This setting is similar to +:ref:`config_report_exclude_lines`: it specifies patterns for lines to exclude +from reporting. This setting is preferred, because it will preserve the +default exclude patterns instead of overwriting them. + +.. versionadded:: 7.2.0 + + .. _config_report_exclude_lines: [report] exclude_lines ...................... (multi-string) A list of regular expressions. Any line of your source code -containing a match for one of these regexes is excluded from being reported as +containing a match for one of these regexes is excluded from being reported as missing. More details are in :ref:`excluding`. If you use this option, you are replacing all the exclude regexes, so you'll need to also supply the -"pragma: no cover" regex if you still want to use it. +"pragma: no cover" regex if you still want to use it. The +:ref:`config_report_exclude_also` setting can be used to specify patterns +without overwriting the default set. You can exclude lines introducing blocks, and the entire block is excluded. If you exclude a ``def`` line or decorator line, the entire function is excluded. @@ -389,7 +412,7 @@ [report] fail_under ................... -(float) A target coverage percentage. If the total coverage measurement is +(float) A target coverage percentage. If the total coverage measurement is under this value, then exit with a status code of 2. If you specify a non-integral value, you must also set ``[report] precision`` properly to make use of the decimal places. A setting of 100 will fail any value under 100, @@ -414,6 +437,20 @@ See :ref:`source` for details. +.. _config_include_namespace_packages: + +[report] include_namespace_packages +................................... + +(boolean, default False) When searching for completely un-executed files, +include directories without ``__init__.py`` files. These are `implicit +namespace packages`_, and are usually skipped. + +.. _implicit namespace packages: https://peps.python.org/pep-0420/ + +.. versionadded:: 7.0 + + .. _config_report_omit: [report] omit @@ -603,7 +640,7 @@ [json] pretty_print ................... -(boolean, default false) Controls if the JSON is outputted with whitespace +(boolean, default false) Controls if the JSON is outputted with white space formatted for human consumption (True) or for minimum file size (False). diff -Nru python-coverage-6.5.0+dfsg1/doc/conf.py python-coverage-7.2.7+dfsg1/doc/conf.py --- python-coverage-6.5.0+dfsg1/doc/conf.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/conf.py 2023-05-29 19:46:30.000000000 +0000 @@ -36,13 +36,14 @@ 'sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.ifconfig', - 'sphinxcontrib.spelling', 'sphinx.ext.intersphinx', 'sphinxcontrib.restbuilder', 'sphinx.ext.napoleon', #'sphinx_tabs.tabs', ] +autodoc_typehints = "description" + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -57,18 +58,20 @@ # General information about the project. project = 'Coverage.py' -copyright = '2009\N{EN DASH}2022, Ned Batchelder' # CHANGEME # pylint: disable=redefined-builtin # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -# -# The short X.Y.Z version. # CHANGEME -version = "6.5.0" -# The full version, including alpha/beta/rc tags. # CHANGEME -release = "6.5.0" -# The date of release, in "monthname day, year" format. # CHANGEME -release_date = "September 29, 2022" + +# @@@ editable +copyright = "2009–2023, Ned Batchelder" # pylint: disable=redefined-builtin +# The short X.Y.Z version. +version = "7.2.7" +# The full version, including alpha/beta/rc tags. +release = "7.2.7" +# The date of release, in "monthname day, year" format. +release_date = "May 29, 2023" +# @@@ end rst_epilog = """ .. |release_date| replace:: {release_date} @@ -121,6 +124,19 @@ 'python': ('https://docs.python.org/3', None), } +nitpick_ignore = [ + ("py:class", "frame"), + ("py:class", "module"), + ("py:class", "DefaultValue"), + ("py:class", "FilePath"), + ("py:class", "TWarnFn"), + ("py:class", "TDebugCtl"), +] + +nitpick_ignore_regex = [ + (r"py:class", r"coverage\..*\..*"), +] + # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with @@ -203,6 +219,9 @@ # -- Spelling --- if any("spell" in arg for arg in sys.argv): + # sphinxcontrib.spelling needs the native "enchant" library, which often is + # missing, so only use the extension if we are specifically spell-checking. + extensions += ['sphinxcontrib.spelling'] names_file = tempfile.NamedTemporaryFile(mode='w', prefix="coverage_names_", suffix=".txt") with open("../CONTRIBUTORS.txt") as contributors: names = set(re.split(r"[^\w']", contributors.read())) diff -Nru python-coverage-6.5.0+dfsg1/doc/contributing.rst python-coverage-7.2.7+dfsg1/doc/contributing.rst --- python-coverage-6.5.0+dfsg1/doc/contributing.rst 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/contributing.rst 2023-05-29 19:46:30.000000000 +0000 @@ -37,23 +37,24 @@ https://github.com/nedbat/coveragepy. To get a working environment, follow these steps: -.. minimum of PYVERSIONS: +#. `Fork the repo`_ into your own GitHub account. The coverage.py code will + then be copied into a GitHub repository at + ``https://github.com/GITHUB_USER/coveragepy`` where GITHUB_USER is your + GitHub username. -#. Create a Python 3.7 virtualenv to work in, and activate it. +#. (Optional) Create a virtualenv to work in, and activate it. There + are a number of ways to do this. Use the method you are comfortable with. #. Clone the repository:: - $ git clone https://github.com/nedbat/coveragepy + $ git clone https://github.com/GITHUB_USER/coveragepy $ cd coveragepy #. Install the requirements:: - $ pip install -r requirements/dev.pip + $ python3 -m pip install -r requirements/dev.in -#. Install a number of versions of Python. Coverage.py supports a range - of Python versions. The more you can test with, the more easily your code - can be used as-is. If you only have one version, that's OK too, but may - mean more work integrating your contribution. + Note: You may need to upgrade pip to install the requirements. Running the tests @@ -62,57 +63,91 @@ The tests are written mostly as standard unittest-style tests, and are run with pytest running under `tox`_:: - $ tox - py37 create: /Users/nedbat/coverage/trunk/.tox/py37 - py37 installdeps: -rrequirements/pip.pip, -rrequirements/pytest.pip, eventlet==0.25.1, greenlet==0.4.15 - py37 develop-inst: /Users/nedbat/coverage/trunk - py37 installed: apipkg==1.5,appdirs==1.4.4,attrs==20.3.0,backports.functools-lru-cache==1.6.4,-e git+git@github.com:nedbat/coveragepy.git@36ef0e03c0439159c2245d38de70734fa08cddb4#egg=coverage,decorator==5.0.7,distlib==0.3.1,dnspython==2.1.0,eventlet==0.25.1,execnet==1.8.0,filelock==3.0.12,flaky==3.7.0,future==0.18.2,greenlet==0.4.15,hypothesis==6.10.1,importlib-metadata==4.0.1,iniconfig==1.1.1,monotonic==1.6,packaging==20.9,pluggy==0.13.1,py==1.10.0,PyContracts @ git+https://github.com/slorg1/contracts@c5a6da27d4dc9985f68e574d20d86000880919c3,pyparsing==2.4.7,pytest==6.2.3,pytest-forked==1.3.0,pytest-xdist==2.2.1,qualname==0.1.0,six==1.15.0,sortedcontainers==2.3.0,toml==0.10.2,typing-extensions==3.10.0.0,virtualenv==20.4.4,zipp==3.4.1 - py37 run-test-pre: PYTHONHASHSEED='376882681' - py37 run-test: commands[0] | python setup.py --quiet clean develop - py37 run-test: commands[1] | python igor.py zip_mods remove_extension - py37 run-test: commands[2] | python igor.py test_with_tracer py - === CPython 3.7.10 with Python tracer (.tox/py37/bin/python) === + % python3 -m tox + ROOT: tox-gh won't override envlist because tox is not running in GitHub Actions + .pkg: _optional_hooks> python /usr/local/virtualenvs/coverage/lib/python3.7/site-packages/pyproject_api/_backend.py True setuptools.build_meta + .pkg: get_requires_for_build_editable> python /usr/local/virtualenvs/coverage/lib/python3.7/site-packages/pyproject_api/_backend.py True setuptools.build_meta + .pkg: build_editable> python /usr/local/virtualenvs/coverage/lib/python3.7/site-packages/pyproject_api/_backend.py True setuptools.build_meta + py37: install_package> python -m pip install -U --force-reinstall --no-deps .tox/.tmp/package/87/coverage-7.2.3a0.dev1-0.editable-cp37-cp37m-macosx_10_15_x86_64.whl + py37: commands[0]> python igor.py zip_mods + py37: commands[1]> python setup.py --quiet build_ext --inplace + py37: commands[2]> python -m pip install -q -e . + py37: commands[3]> python igor.py test_with_tracer c + === CPython 3.7.15 with C tracer (.tox/py37/bin/python) === bringing up nodes... - ........................................................................................................................................................... [ 15%] - ........................................................................................................................................................... [ 31%] - ...........................................................................................................................................s............... [ 47%] - ...........................................s...................................................................................sss.sssssssssssssssssss..... [ 63%] - ........................................................................................................................................................s.. [ 79%] - ......................................s..................................s................................................................................. [ 95%] - ........................................ss...... [100%] - 949 passed, 29 skipped in 40.56s - py37 run-test: commands[3] | python setup.py --quiet build_ext --inplace - py37 run-test: commands[4] | python igor.py test_with_tracer c - === CPython 3.7.10 with C tracer (.tox/py37/bin/python) === + .........................................................................................................................x.................s....s....... [ 11%] + ..s.....x.............................................s................................................................................................. [ 22%] + ........................................................................................................................................................ [ 34%] + ........................................................................................................................................................ [ 45%] + ........................................................................................................................................................ [ 57%] + .........s....................................................................................................................s......................... [ 68%] + .................................s..............................s...............s..................................s.................................... [ 80%] + ........................................................s............................................................................................... [ 91%] + ......................................s......................................................................... [100%] + 1316 passed, 12 skipped, 2 xfailed in 36.42s + py37: commands[4]> python igor.py remove_extension + py37: commands[5]> python igor.py test_with_tracer py + === CPython 3.7.15 with Python tracer (.tox/py37/bin/python) === bringing up nodes... - ........................................................................................................................................................... [ 15%] - ........................................................................................................................................................... [ 31%] - ......................................................................s.................................................................................... [ 47%] - ........................................................................................................................................................... [ 63%] - ..........................s................................................s............................................................................... [ 79%] - .................................................................................s......................................................................... [ 95%] - ......................................s......... [100%] - 973 passed, 5 skipped in 41.36s - ____________________________________________________________________________ summary _____________________________________________________________________________ - py37: commands succeeded - congratulations :) + ................................................................................................x...........................x.................s......... [ 11%] + .....s.............s.s.....................................................s..............ss............................s.ss....ss.ss................... [ 22%] + ......................................................................................................................................s................. [ 34%] + ..................................................................................................................s..................................... [ 45%] + ...................s.ss.....................................................................................s....................s.ss................... [ 57%] + ..................s.s................................................................................................................................... [ 68%] + ..........................s.........................................ssss...............s.................s...sss..................s...ss...ssss.s....... [ 80%] + .......................................................................................................................................................s [ 91%] + .........................................................................s.................................ss.... [100%] + 1281 passed, 47 skipped, 2 xfailed in 33.86s + .pkg: _exit> python /usr/local/virtualenvs/coverage/lib/python3.7/site-packages/pyproject_api/_backend.py True setuptools.build_meta + py37: OK (82.38=setup[2.80]+cmd[0.20,0.35,7.30,37.20,0.21,34.32] seconds) + congratulations :) (83.61 seconds) Tox runs the complete test suite twice for each version of Python you have -installed. The first run uses the Python implementation of the trace function, -the second uses the C implementation. +installed. The first run uses the C implementation of the trace function, +the second uses the Python implementation. To limit tox to just a few versions of Python, use the ``-e`` switch:: - $ tox -e py37,py39 - -To run just a few tests, you can use `pytest test selectors`_:: + $ python3 -m tox -e py37,py39 - $ tox tests/test_misc.py - $ tox tests/test_misc.py::HasherTest - $ tox tests/test_misc.py::HasherTest::test_string_hashing +On the tox command line, options after ``--`` are passed to pytest. To run +just a few tests, you can use `pytest test selectors`_:: -These command run the tests in one file, one class, and just one test, -respectively. + $ python3 -m tox -- tests/test_misc.py + $ python3 -m tox -- tests/test_misc.py::HasherTest + $ python3 -m tox -- tests/test_misc.py::HasherTest::test_string_hashing + +These commands run the tests in one file, one class, and just one test, +respectively. The pytest ``-k`` option selects tests based on a word in their +name, which can be very convenient for ad-hoc test selection. Of course you +can combine tox and pytest options:: + + $ python3 -m tox -q -e py37 -- -n 0 -vv -k hash + === CPython 3.7.15 with C tracer (.tox/py37/bin/python) === + ======================================= test session starts ======================================== + platform darwin -- Python 3.7.15, pytest-7.2.2, pluggy-1.0.0 -- /Users/nedbat/coverage/.tox/py37/bin/python + cachedir: .tox/py37/.pytest_cache + rootdir: /Users/nedbat/coverage, configfile: setup.cfg + plugins: flaky-3.7.0, hypothesis-6.70.0, xdist-3.2.1 + collected 1330 items / 1320 deselected / 10 selected + run-last-failure: no previously failed tests, not deselecting items. + + tests/test_data.py::CoverageDataTest::test_add_to_hash_with_lines PASSED [ 10%] + tests/test_data.py::CoverageDataTest::test_add_to_hash_with_arcs PASSED [ 20%] + tests/test_data.py::CoverageDataTest::test_add_to_lines_hash_with_missing_file PASSED [ 30%] + tests/test_data.py::CoverageDataTest::test_add_to_arcs_hash_with_missing_file PASSED [ 40%] + tests/test_execfile.py::RunPycFileTest::test_running_hashed_pyc PASSED [ 50%] + tests/test_misc.py::HasherTest::test_string_hashing PASSED [ 60%] + tests/test_misc.py::HasherTest::test_bytes_hashing PASSED [ 70%] + tests/test_misc.py::HasherTest::test_unicode_hashing PASSED [ 80%] + tests/test_misc.py::HasherTest::test_dict_hashing PASSED [ 90%] + tests/test_misc.py::HasherTest::test_dict_collision PASSED [100%] + + =============================== 10 passed, 1320 deselected in 1.88s ================================ + Skipping tests with Python tracer: Only one tracer: no Python tracer for CPython + py37: OK (12.22=setup[2.19]+cmd[0.20,0.36,6.57,2.51,0.20,0.19] seconds) + congratulations :) (13.10 seconds) You can also affect the test runs with environment variables. Define any of these as 1 to use them: @@ -151,7 +186,8 @@ keep you from sending patches. I can clean them up. Lines should be kept to a 100-character maximum length. I recommend an -`editorconfig.org`_ plugin for your editor of choice. +`editorconfig.org`_ plugin for your editor of choice, which will also help with +indentation, line endings and so on. Other style questions are best answered by looking at the existing code. Formatting of docstrings, comments, long lines, and so on, should match the @@ -215,6 +251,7 @@ fixes. If you need help writing tests, please ask. +.. _fork the repo: https://docs.github.com/en/get-started/quickstart/fork-a-repo .. _editorconfig.org: http://editorconfig.org .. _tox: https://tox.readthedocs.io/ .. _black: https://pypi.org/project/black/ diff -Nru python-coverage-6.5.0+dfsg1/doc/dbschema.rst python-coverage-7.2.7+dfsg1/doc/dbschema.rst --- python-coverage-6.5.0+dfsg1/doc/dbschema.rst 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/dbschema.rst 2023-05-29 19:46:30.000000000 +0000 @@ -66,7 +66,7 @@ key text, value text, unique (key) - -- Keys: + -- Possible keys: -- 'has_arcs' boolean -- Is this data recording branches? -- 'sys_argv' text -- The coverage command line that recorded the data. -- 'version' text -- The version of coverage.py that made the file. @@ -116,7 +116,7 @@ foreign key (file_id) references file (id) ); -.. [[[end]]] (checksum: cfce1df016afbb43a5ff94306db56657) +.. [[[end]]] (checksum: 6a04d14b07f08f86cccf43056328dcb7) .. _numbits: diff -Nru python-coverage-6.5.0+dfsg1/doc/dict.txt python-coverage-7.2.7+dfsg1/doc/dict.txt --- python-coverage-6.5.0+dfsg1/doc/dict.txt 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/dict.txt 2023-05-29 19:46:30.000000000 +0000 @@ -1,18 +1,36 @@ +API +BOM +BTW +CPython +CTracer +Cobertura +Consolas +Cython +DOCTYPE +DOM +HTML +Jinja +Mako +OK +PYTHONPATH +TODO +Tidelift +URL +UTF +XML activestate -api apache -API +api args argv ascii +async basename basenames bitbucket -BOM bom boolean booleans -BTW btw builtin builtins @@ -27,7 +45,6 @@ chdir'd clickable cmdline -Cobertura codecs colorsys combinable @@ -38,17 +55,16 @@ configurability's configurer configurers -Consolas cov coveragepy coveragerc covhtml -CPython css -CTracer -Cython +dataio datetime deallocating +debounce +decodable dedent defaultdict deserialize @@ -62,8 +78,6 @@ docstrings doctest doctests -DOCTYPE -DOM encodable encodings endfor @@ -75,6 +89,7 @@ execfile executability executable's +execv expr extensibility favicon @@ -96,10 +111,10 @@ gitignore globals greenlet +hintedness hotkey hotkeys html -HTML htmlcov http https @@ -111,15 +126,13 @@ invariants iterable iterables -Jinja -jquery jQuery +jquery json jython kwargs lcov localStorage -Mako manylinux matcher matchers @@ -136,8 +149,10 @@ morf morfs multi +multiproc mumbo mycode +mypy namespace namespaces nano @@ -145,13 +160,14 @@ ned nedbat nedbatchelder +newb +nocover nosetests nullary num numbits numpy ok -OK opcode opcodes optparse @@ -161,13 +177,15 @@ parallelizing parsable parsers +pathlib pathnames plugin plugins pragma -pragmas pragma'd +pragmas pre +premain prepended prepending programmability @@ -175,17 +193,19 @@ py py's pyc +pyenv pyexpat +pylib pylint pyproject pypy pytest pythonpath -PYTHONPATH pyw rcfile readme readthedocs +realpath recordable refactored refactoring @@ -194,9 +214,11 @@ regexes reimplemented renderer +rootname runnable runtime scrollbar +septatrix serializable settrace setuptools @@ -217,13 +239,10 @@ symlinks syntaxes sys -templite templating +templite testability -Tidelift -timestamp todo -TODO tokenization tokenize tokenized @@ -241,7 +260,6 @@ ubuntu undecodable unexecutable -unexecuted unicode uninstall unittest @@ -249,19 +267,17 @@ unrunnable unsubscriptable untokenizable +usecache username -URL -UTF utf vendored versionadded virtualenv -whitespace wikipedia wildcard wildcards www +xdist xml -XML xrange xyzzy diff -Nru python-coverage-6.5.0+dfsg1/doc/excluding.rst python-coverage-7.2.7+dfsg1/doc/excluding.rst --- python-coverage-6.5.0+dfsg1/doc/excluding.rst 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/excluding.rst 2023-05-29 19:46:30.000000000 +0000 @@ -80,14 +80,13 @@ all of them by adding a regex to the exclusion list:: [report] - exclude_lines = + exclude_also = def __repr__ For example, here's a list of exclusions I've used:: [report] - exclude_lines = - pragma: no cover + exclude_also = def __repr__ if self.debug: if settings.DEBUG @@ -95,12 +94,14 @@ raise NotImplementedError if 0: if __name__ == .__main__.: + if TYPE_CHECKING: class .*\bProtocol\): @(abc\.)?abstractmethod -Note that when using the ``exclude_lines`` option in a configuration file, you -are taking control of the entire list of regexes, so you need to re-specify the -default "pragma: no cover" match if you still want it to apply. +The :ref:`config_report_exclude_also` option adds regexes to the built-in +default list so that you can add your own exclusions. The older +:ref:`config_report_exclude_lines` option completely overwrites the list of +regexes. The regexes only have to match part of a line. Be careful not to over-match. A value of ``...`` will match any line with more than three characters in it. diff -Nru python-coverage-6.5.0+dfsg1/doc/faq.rst python-coverage-7.2.7+dfsg1/doc/faq.rst --- python-coverage-6.5.0+dfsg1/doc/faq.rst 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/faq.rst 2023-05-29 19:46:30.000000000 +0000 @@ -11,6 +11,22 @@ Frequently asked questions -------------------------- +Q: Why are some of my files not measured? +......................................... + +Coverage.py has a number of mechanisms for deciding which files to measure and +which to skip. If your files aren't being measured, use the ``--debug=trace`` +:ref:`option `, also settable as ``[run] debug=trace`` in the +:ref:`settings file `, or as ``COVERAGE_DEBUG=trace`` in an +environment variable. + +This will write a line for each file considered, indicating whether it is +traced or not, and if not, why not. Be careful though: the output might be +swallowed by your test runner. If so, a ``COVERAGE_DEBUG_FILE=/tmp/cov.out`` +environment variable can direct the output to a file instead to ensure you see +everything. + + Q: Why do unexecutable lines show up as executed? ................................................. @@ -23,11 +39,24 @@ to clean out the old data. +Q: Why are my function definitions marked as run when I haven't tested them? +............................................................................ + +The ``def`` and ``class`` lines in your Python file are executed when the file +is imported. Those are the lines that define your functions and classes. They +run even if you never call the functions. It's the body of the functions that +will be marked as not executed if you don't test them, not the ``def`` lines. + +This can mean that your code has a moderate coverage total even if no tests +have been written or run. This might seem surprising, but it is accurate: the +``def`` lines have actually been run. + + Q: Why do the bodies of functions show as executed, but the def lines do not? ............................................................................. -This happens because coverage.py is started after the functions are defined. -The definition lines are executed without coverage measurement, then +If this happens, it's because coverage.py has started after the functions are +defined. The definition lines are executed without coverage measurement, then coverage.py is started, then the function is called. This means the body is measured, but the definition of the function itself is not. @@ -92,7 +121,7 @@ implementations of the trace function. The C implementation runs much faster. To see what you are running, use ``coverage debug sys``. The output contains details of the environment, including a line that says either -``CTrace: available`` or ``CTracer: unavailable``. If it says unavailable, +``CTracer: available`` or ``CTracer: unavailable``. If it says unavailable, then you are using the slow Python implementation. Try re-installing coverage.py to see what happened and if you get the CTracer @@ -117,9 +146,9 @@ .. _trialcoverage: https://pypi.org/project/trialcoverage/ - - `pytest-coverage`_ + - `pytest-cov`_ - .. _pytest-coverage: https://pypi.org/project/pytest-coverage/ + .. _pytest-cov: https://pypi.org/project/pytest-cov/ - `django-coverage`_ for use with Django. @@ -129,10 +158,11 @@ Q: Where can I get more help with coverage.py? .............................................. -You can discuss coverage.py or get help using it on the `Testing In Python`_ -mailing list. +You can discuss coverage.py or get help using it on the `Python discussion +forums`_. If you ping me (``@nedbat``), there's a higher chance I'll see the +post. -.. _Testing In Python: http://lists.idyll.org/listinfo/testing-in-python +.. _Python discussion forums: https://discuss.python.org/ Bug reports are gladly accepted at the `GitHub issue tracker`_. @@ -151,6 +181,6 @@ Since 2004, `Ned Batchelder`_ has extended and maintained it with the help of `many others`_. The :ref:`change history ` has all the details. -.. _Gareth Rees: http://garethrees.org/ +.. _Gareth Rees: http://garethrees.org/ .. _Ned Batchelder: https://nedbatchelder.com -.. _many others: https://github.com/nedbat/coveragepy/blob/master/CONTRIBUTORS.txt +.. _many others: https://github.com/nedbat/coveragepy/blob/master/CONTRIBUTORS.txt diff -Nru python-coverage-6.5.0+dfsg1/doc/index.rst python-coverage-7.2.7+dfsg1/doc/index.rst --- python-coverage-6.5.0+dfsg1/doc/index.rst 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/index.rst 2023-05-29 19:46:30.000000000 +0000 @@ -18,18 +18,17 @@ .. PYVERSIONS -* Python versions 3.7 through 3.11.0 rc2. - -* PyPy3 7.3.8. +* Python versions 3.7 through 3.12.0b1. +* PyPy3 7.3.11. .. ifconfig:: prerelease **This is a pre-release build. The usual warnings about possible bugs - apply.** The latest stable version is coverage.py 6.4, `described here`_. - + apply.** The latest stable version is coverage.py 6.5.0, `described here`_. .. _described here: http://coverage.readthedocs.io/ + For Enterprise -------------- @@ -57,16 +56,22 @@ #. Install coverage.py:: - $ pip install coverage + $ python3 -m pip install coverage For more details, see :ref:`install`. #. Use ``coverage run`` to run your test suite and gather data. However you - normally run your test suite, you can run your test runner under coverage. - If your test runner command starts with "python", just replace the initial - "python" with "coverage run". + normally run your test suite, you can use your test runner under coverage. + + .. tip:: + If your test runner command starts with "python", just replace the initial + "python" with "coverage run". + + ``python something.py`` becomes ``coverage run something.py`` + + ``python -m amodule`` becomes ``coverage run -m amodule`` - Instructions for specific test runners: + Other instructions for specific test runners: - **pytest** @@ -183,9 +188,10 @@ ------------ If the :ref:`FAQ ` doesn't answer your question, you can discuss -coverage.py or get help using it on the `Testing In Python`_ mailing list. +coverage.py or get help using it on the `Python discussion forums`_. If you +ping me (``@nedbat``), there's a higher chance I'll see the post. -.. _Testing In Python: http://lists.idyll.org/listinfo/testing-in-python +.. _Python discussion forums: https://discuss.python.org/ Bug reports are gladly accepted at the `GitHub issue tracker`_. GitHub also hosts the `code repository`_. @@ -201,7 +207,10 @@ .. _I can be reached: https://nedbatchelder.com/site/aboutned.html +.. raw:: html +

For news and other chatter, follow the project on Mastodon: + @coveragepy@hachyderm.io.

More information ---------------- @@ -225,4 +234,5 @@ trouble faq Change history + migrating sleepy diff -Nru python-coverage-6.5.0+dfsg1/doc/install.rst python-coverage-7.2.7+dfsg1/doc/install.rst --- python-coverage-6.5.0+dfsg1/doc/install.rst 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/install.rst 2023-05-29 19:46:30.000000000 +0000 @@ -15,19 +15,19 @@ You can install coverage.py in the usual ways. The simplest way is with pip:: - $ pip install coverage + $ python3 -m pip install coverage .. ifconfig:: prerelease To install a pre-release version, you will need to specify ``--pre``:: - $ pip install --pre coverage + $ python3 -m pip install --pre coverage or the exact version you want to install: .. parsed-literal:: - $ pip install |coverage-equals-release| + $ python3 -m pip install |coverage-equals-release| .. _install_extension: diff -Nru python-coverage-6.5.0+dfsg1/doc/migrating.rst python-coverage-7.2.7+dfsg1/doc/migrating.rst --- python-coverage-6.5.0+dfsg1/doc/migrating.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/migrating.rst 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,54 @@ +.. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +.. _migrating: + +========================== +Migrating between versions +========================== + +New versions of coverage.py or Python might require you to adjust your +settings, options, or other aspects how you use coverage.py. This page details +those changes. + +.. _migrating_cov7: + +Migrating to coverage.py 7.x +---------------------------- + +Consider these changes when migrating to coverage.py 7.x: + +- The way that wildcards when specifying file paths work in certain cases has + changed in 7.x: + + - Previously, ``*`` would incorrectly match directory separators, making + precise matching difficult. Patterns such as ``*tests/*`` + will need to be changed to ``*/tests/*``. + + - ``**`` now matches any number of nested directories. If you wish to retain + the behavior of ``**/tests/*`` in previous versions then ``*/**/tests/*`` + can be used instead. + +- When remapping file paths with ``[paths]``, a path will be remapped only if + the resulting path exists. Ensure that remapped ``[paths]`` exist when + upgrading as this is now being enforced. + +- The :ref:`config_report_exclude_also` setting is new in 7.2.0. It adds + exclusion regexes while keeping the default built-in set. It's better than + the older :ref:`config_report_exclude_lines` setting, which overwrote the + entire list. Newer versions of coverage.py will be adding to the default set + of exclusions. Using ``exclude_also`` will let you benefit from those + updates. + + +.. _migrating_py312: + +Migrating to Python 3.12 +------------------------ + +Keep these things in mind when running under Python 3.12: + +- Python 3.12 now inlines list, dict, and set comprehensions. Previously, they + were compiled as functions that were called internally. Coverage.py would + warn you if comprehensions weren't fully completed, but this no longer + happens with Python 3.12. diff -Nru python-coverage-6.5.0+dfsg1/doc/plugins.rst python-coverage-7.2.7+dfsg1/doc/plugins.rst --- python-coverage-6.5.0+dfsg1/doc/plugins.rst 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/plugins.rst 2023-05-29 19:46:30.000000000 +0000 @@ -29,7 +29,7 @@ .. code-block:: sh - $ pip install something + $ python3 -m pip install something #. Configure coverage.py to use the plug-in. You do this by editing (or creating) your .coveragerc file, as described in :ref:`config`. The diff -Nru python-coverage-6.5.0+dfsg1/doc/python-coverage.1.txt python-coverage-7.2.7+dfsg1/doc/python-coverage.1.txt --- python-coverage-6.5.0+dfsg1/doc/python-coverage.1.txt 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/python-coverage.1.txt 2023-05-29 19:46:30.000000000 +0000 @@ -8,7 +8,7 @@ :Author: Ned Batchelder :Author: |author| -:Date: 2022-01-25 +:Date: 2022-12-03 :Copyright: Apache 2.0 license, attribution and disclaimer required. :Manual section: 1 :Manual group: Coverage.py @@ -299,6 +299,9 @@ \--fail-under `MIN` Exit with a status of 2 if the total coverage is less than `MIN`. + \--format `FORMAT` + Output format, either text (default), markdown, or total. + \-i, --ignore-errors Ignore errors while reading source files. diff -Nru python-coverage-6.5.0+dfsg1/doc/requirements.in python-coverage-7.2.7+dfsg1/doc/requirements.in --- python-coverage-6.5.0+dfsg1/doc/requirements.in 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/requirements.in 2023-05-29 19:46:30.000000000 +0000 @@ -9,6 +9,7 @@ cogapp #doc8 pyenchant +scriv # for writing GitHub releases sphinx sphinx-autobuild sphinx_rtd_theme diff -Nru python-coverage-6.5.0+dfsg1/doc/requirements.pip python-coverage-7.2.7+dfsg1/doc/requirements.pip --- python-coverage-6.5.0+dfsg1/doc/requirements.pip 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/requirements.pip 2023-05-29 19:46:30.000000000 +0000 @@ -1,211 +1,106 @@ # -# This file is autogenerated by pip-compile with python 3.7 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: # # make upgrade # -alabaster==0.7.12 \ - --hash=sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359 \ - --hash=sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02 - # via sphinx -babel==2.10.3 \ - --hash=sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51 \ - --hash=sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb - # via sphinx -certifi==2022.5.18.1 \ - --hash=sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7 \ - --hash=sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a - # via - # -c doc/../requirements/pins.pip - # requests -charset-normalizer==2.1.1 \ - --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \ - --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f +alabaster==0.7.13 + # via sphinx +attrs==23.1.0 + # via scriv +babel==2.12.1 + # via sphinx +certifi==2023.5.7 # via requests -cogapp==3.3.0 \ - --hash=sha256:1be95183f70282422d594fa42426be6923070a4bd8335621f6347f3aeee81db0 \ - --hash=sha256:8b5b5f6063d8ee231961c05da010cb27c30876b2279e23ad0eae5f8f09460d50 - # via -r doc/requirements.in -colorama==0.4.5 \ - --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \ - --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4 +charset-normalizer==3.1.0 + # via requests +click==8.1.3 + # via + # click-log + # scriv +click-log==0.4.0 + # via scriv +cogapp==3.3.0 + # via -r doc/requirements.in +colorama==0.4.6 # via sphinx-autobuild -docutils==0.17.1 \ - --hash=sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125 \ - --hash=sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61 +docutils==0.18.1 # via # sphinx # sphinx-rtd-theme -idna==3.4 \ - --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ - --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 +idna==3.4 # via requests -imagesize==1.4.1 \ - --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ - --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a - # via sphinx -importlib-metadata==4.12.0 \ - --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \ - --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23 +imagesize==1.4.1 + # via sphinx +importlib-metadata==6.6.0 # via + # attrs + # click # sphinx # sphinxcontrib-spelling -jinja2==3.1.2 \ - --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ - --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 - # via sphinx -livereload==2.6.3 \ - --hash=sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869 +jinja2==3.1.2 + # via + # scriv + # sphinx +livereload==2.6.3 # via sphinx-autobuild -markupsafe==2.1.1 \ - --hash=sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003 \ - --hash=sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88 \ - --hash=sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5 \ - --hash=sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7 \ - --hash=sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a \ - --hash=sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603 \ - --hash=sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1 \ - --hash=sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135 \ - --hash=sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247 \ - --hash=sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6 \ - --hash=sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601 \ - --hash=sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77 \ - --hash=sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02 \ - --hash=sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e \ - --hash=sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63 \ - --hash=sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f \ - --hash=sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980 \ - --hash=sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b \ - --hash=sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812 \ - --hash=sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff \ - --hash=sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96 \ - --hash=sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1 \ - --hash=sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925 \ - --hash=sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a \ - --hash=sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6 \ - --hash=sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e \ - --hash=sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f \ - --hash=sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4 \ - --hash=sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f \ - --hash=sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3 \ - --hash=sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c \ - --hash=sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a \ - --hash=sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417 \ - --hash=sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a \ - --hash=sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a \ - --hash=sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37 \ - --hash=sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452 \ - --hash=sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933 \ - --hash=sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a \ - --hash=sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7 +markupsafe==2.1.2 # via jinja2 -packaging==21.3 \ - --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ - --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 - # via sphinx -pyenchant==3.2.2 \ - --hash=sha256:1cf830c6614362a78aab78d50eaf7c6c93831369c52e1bb64ffae1df0341e637 \ - --hash=sha256:5a636832987eaf26efe971968f4d1b78e81f62bca2bde0a9da210c7de43c3bce \ - --hash=sha256:5facc821ece957208a81423af7d6ec7810dad29697cb0d77aae81e4e11c8e5a6 \ - --hash=sha256:6153f521852e23a5add923dbacfbf4bebbb8d70c4e4bad609a8e0f9faeb915d1 +packaging==23.1 + # via sphinx +pyenchant==3.2.2 # via # -r doc/requirements.in # sphinxcontrib-spelling -pygments==2.13.0 \ - --hash=sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1 \ - --hash=sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42 - # via sphinx -pyparsing==3.0.9 \ - --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \ - --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc - # via packaging -pytz==2022.2.1 \ - --hash=sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197 \ - --hash=sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5 +pygments==2.15.1 + # via sphinx +pytz==2023.3 # via babel -requests==2.28.1 \ - --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \ - --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349 - # via sphinx -six==1.16.0 \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 +requests==2.31.0 + # via + # scriv + # sphinx +scriv==1.3.1 + # via -r doc/requirements.in +six==1.16.0 # via livereload -snowballstemmer==2.2.0 \ - --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ - --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a - # via sphinx -sphinx==5.2.1 \ - --hash=sha256:3dcf00fcf82cf91118db9b7177edea4fc01998976f893928d0ab0c58c54be2ca \ - --hash=sha256:c009bb2e9ac5db487bcf53f015504005a330ff7c631bb6ab2604e0d65bae8b54 +snowballstemmer==2.2.0 + # via sphinx +sphinx==5.3.0 # via # -r doc/requirements.in # sphinx-autobuild # sphinx-rtd-theme + # sphinxcontrib-jquery # sphinxcontrib-restbuilder # sphinxcontrib-spelling -sphinx-autobuild==2021.3.14 \ - --hash=sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac \ - --hash=sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05 - # via -r doc/requirements.in -sphinx-rtd-theme==1.0.0 \ - --hash=sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8 \ - --hash=sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c - # via -r doc/requirements.in -sphinxcontrib-applehelp==1.0.2 \ - --hash=sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a \ - --hash=sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58 - # via sphinx -sphinxcontrib-devhelp==1.0.2 \ - --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \ - --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4 - # via sphinx -sphinxcontrib-htmlhelp==2.0.0 \ - --hash=sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07 \ - --hash=sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2 - # via sphinx -sphinxcontrib-jsmath==1.0.1 \ - --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ - --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 - # via sphinx -sphinxcontrib-qthelp==1.0.3 \ - --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \ - --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6 - # via sphinx -sphinxcontrib-restbuilder==0.3 \ - --hash=sha256:6b3ee9394b5ec5e73e6afb34d223530d0b9098cb7562f9c5e364e6d6b41410ce \ - --hash=sha256:6ba2ddc7a87d845c075c1b2e00d541bd1c8400488e50e32c9b4169ccdd9f30cb - # via -r doc/requirements.in -sphinxcontrib-serializinghtml==1.1.5 \ - --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \ - --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 - # via sphinx -sphinxcontrib-spelling==7.6.0 \ - --hash=sha256:292cd7e1f73a763451693b4d48c9bded151084f6a91e5337733e9fa8715d20ec \ - --hash=sha256:6c1313618412511109f7b76029fbd60df5aa4acf67a2dc9cd1b1016d15e882ff - # via -r doc/requirements.in -tornado==6.2 \ - --hash=sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca \ - --hash=sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72 \ - --hash=sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23 \ - --hash=sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8 \ - --hash=sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b \ - --hash=sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9 \ - --hash=sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13 \ - --hash=sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75 \ - --hash=sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac \ - --hash=sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e \ - --hash=sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b +sphinx-autobuild==2021.3.14 + # via -r doc/requirements.in +sphinx-rtd-theme==1.2.1 + # via -r doc/requirements.in +sphinxcontrib-applehelp==1.0.2 + # via sphinx +sphinxcontrib-devhelp==1.0.2 + # via sphinx +sphinxcontrib-htmlhelp==2.0.0 + # via sphinx +sphinxcontrib-jquery==4.1 + # via sphinx-rtd-theme +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==1.0.3 + # via sphinx +sphinxcontrib-restbuilder==0.3 + # via -r doc/requirements.in +sphinxcontrib-serializinghtml==1.1.5 + # via sphinx +sphinxcontrib-spelling==8.0.0 + # via -r doc/requirements.in +tornado==6.2 # via livereload -typing-extensions==4.3.0 \ - --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \ - --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6 +typing-extensions==4.6.2 # via importlib-metadata -urllib3==1.26.12 \ - --hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \ - --hash=sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997 +urllib3==2.0.2 # via requests -zipp==3.8.1 \ - --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \ - --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009 +zipp==3.15.0 # via importlib-metadata diff -Nru python-coverage-6.5.0+dfsg1/doc/source.rst python-coverage-7.2.7+dfsg1/doc/source.rst --- python-coverage-6.5.0+dfsg1/doc/source.rst 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/source.rst 2023-05-29 19:46:30.000000000 +0000 @@ -32,7 +32,7 @@ If the source option is specified, only code in those locations will be measured. Specifying the source option also enables coverage.py to report on -unexecuted files, since it can search the source tree for files that haven't +un-executed files, since it can search the source tree for files that haven't been measured at all. Only importable files (ones at the root of the tree, or in directories with a ``__init__.py`` file) will be considered. Files with unusual punctuation in their names will be skipped (they are assumed to be @@ -59,10 +59,10 @@ .. highlight:: ini -The ``include`` and ``omit`` file name patterns follow typical shell syntax: -``*`` matches any number of characters and ``?`` matches a single character. -Patterns that start with a wildcard character are used as-is, other patterns -are interpreted relative to the current directory:: +The ``include`` and ``omit`` file name patterns follow common shell syntax, +described below in :ref:`source_glob`. Patterns that start with a wildcard +character are used as-is, other patterns are interpreted relative to the +current directory:: [run] omit = @@ -77,7 +77,7 @@ the source that will be measured. If both ``source`` and ``include`` are set, the ``include`` value is ignored -and a warning is printed on the standard output. +and a warning is issued. .. _source_reporting: @@ -103,3 +103,22 @@ Note that these are ways of specifying files to measure. You can also exclude individual source lines. See :ref:`excluding` for details. + + +.. _source_glob: + +File patterns +------------- + +File path patterns are used for include and omit, and for combining path +remapping. They follow common shell syntax: + +- ``*`` matches any number of file name characters, not including the directory + separator. + +- ``?`` matches a single file name character. + +- ``**`` matches any number of nested directory names, including none. + +- Both ``/`` and ``\`` will match either a slash or a backslash, to make + cross-platform matching easier. diff -Nru python-coverage-6.5.0+dfsg1/doc/trouble.rst python-coverage-7.2.7+dfsg1/doc/trouble.rst --- python-coverage-6.5.0+dfsg1/doc/trouble.rst 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/doc/trouble.rst 2023-05-29 19:46:30.000000000 +0000 @@ -25,8 +25,8 @@ Things that don't work ---------------------- -There are a number of popular modules, packages, and libraries that prevent -coverage.py from working properly: +There are a few modules or functions that prevent coverage.py from working +properly: * `execv`_, or one of its variants. These end the current program and replace it with a new one. This doesn't save the collected coverage data, so your diff -Nru python-coverage-6.5.0+dfsg1/.editorconfig python-coverage-7.2.7+dfsg1/.editorconfig --- python-coverage-6.5.0+dfsg1/.editorconfig 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/.editorconfig 2023-05-29 19:46:30.000000000 +0000 @@ -18,6 +18,9 @@ [*.py] max_line_length = 100 +[*.pyi] +max_line_length = 100 + [*.c] max_line_length = 100 @@ -30,6 +33,12 @@ [*.rst] max_line_length = 79 +[*.tok] +trim_trailing_whitespace = false + +[*_dos.tok] +end_of_line = crlf + [Makefile] indent_style = tab indent_size = 8 diff -Nru python-coverage-6.5.0+dfsg1/.git-blame-ignore-revs python-coverage-7.2.7+dfsg1/.git-blame-ignore-revs --- python-coverage-6.5.0+dfsg1/.git-blame-ignore-revs 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/.git-blame-ignore-revs 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,14 @@ +# Commits to ignore when doing git-blame. + +# 2023-01-05 style: use good style for annotated defaults parameters +78444f4c06df6a634fa67dd99ee7c07b6b633d9e + +# 2023-01-06 style(perf): blacken lab/benchmark.py +bf6c12f5da54db7c5c0cc47cbf22c70f686e8236 + +# 2023-03-22 style: use double-quotes +16abd82b6e87753184e8308c4b2606ff3979f8d3 +b7be64538aa480fce641349d3053e9a84862d571 + +# 2023-04-01 style: use double-quotes in JavaScript +b03ab92bae24c54f1d5a98baa3af6b9a18de4d36 diff -Nru python-coverage-6.5.0+dfsg1/.github/ISSUE_TEMPLATE/bug_report.md python-coverage-7.2.7+dfsg1/.github/ISSUE_TEMPLATE/bug_report.md --- python-coverage-6.5.0+dfsg1/.github/ISSUE_TEMPLATE/bug_report.md 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/.github/ISSUE_TEMPLATE/bug_report.md 2023-05-29 19:46:30.000000000 +0000 @@ -16,7 +16,7 @@ 1. What version of coverage.py shows the problem? The output of `coverage debug sys` is helpful. 1. What versions of what packages do you have installed? The output of `pip freeze` is helpful. 1. What code shows the problem? Give us a specific commit of a specific repo that we can check out. If you've already worked around the problem, please provide a commit before that fix. -1. What commands did you run? +1. What commands should we run to reproduce the problem? *Be specific*. Include everything, even `git clone`, `pip install`, and so on. Explain like we're five! **Expected behavior** A clear and concise description of what you expected to happen. diff -Nru python-coverage-6.5.0+dfsg1/.github/ISSUE_TEMPLATE/config.yml python-coverage-7.2.7+dfsg1/.github/ISSUE_TEMPLATE/config.yml --- python-coverage-6.5.0+dfsg1/.github/ISSUE_TEMPLATE/config.yml 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/.github/ISSUE_TEMPLATE/config.yml 2023-05-29 19:46:30.000000000 +0000 @@ -8,9 +8,6 @@ - name: Frequently Asked Questions url: https://coverage.readthedocs.io/en/latest/faq.html about: Some common problems are described here. - - name: Testing in Python mailing list - url: http://lists.idyll.org/listinfo/testing-in-python - about: Ask questions about using coverage.py here. - name: Tidelift security contact url: https://tidelift.com/security about: Please report security vulnerabilities here. diff -Nru python-coverage-6.5.0+dfsg1/.github/workflows/coverage.yml python-coverage-7.2.7+dfsg1/.github/workflows/coverage.yml --- python-coverage-6.5.0+dfsg1/.github/workflows/coverage.yml 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/.github/workflows/coverage.yml 2023-05-29 19:46:30.000000000 +0000 @@ -18,6 +18,7 @@ env: PIP_DISABLE_PIP_VERSION_CHECK: 1 + FORCE_COLOR: 1 # Get colored pytest output permissions: contents: read @@ -28,17 +29,17 @@ jobs: coverage: - name: "Python ${{ matrix.python-version }} on ${{ matrix.os }}" - runs-on: "${{ matrix.os }}" + name: "${{ matrix.python-version }} on ${{ matrix.os }}" + runs-on: "${{ matrix.os }}-latest" strategy: matrix: os: - - ubuntu-latest - - macos-latest - - windows-latest + - ubuntu + - macos + - windows python-version: - # When changing this list, be sure to check the [gh-actions] list in + # When changing this list, be sure to check the [gh] list in # tox.ini so that tox will run properly. PYVERSIONS # Available versions: # https://github.com/actions/python-versions/blob/main/versions-manifest.json @@ -46,15 +47,26 @@ - "3.8" - "3.9" - "3.10" - - "3.11.0-rc.2" + - "3.11" + - "3.12" - "pypy-3.7" + - "pypy-3.8" + - "pypy-3.9" exclude: # Windows PyPy doesn't seem to work? - os: windows-latest python-version: "pypy-3.7" + - os: windows-latest + python-version: "pypy-3.8" + - os: windows-latest + python-version: "pypy-3.9" # Mac PyPy always takes the longest, and doesn't add anything. - os: macos-latest python-version: "pypy-3.7" + - os: macos-latest + python-version: "pypy-3.8" + - os: macos-latest + python-version: "pypy-3.9" # If one job fails, stop the whole thing. fail-fast: true @@ -66,6 +78,7 @@ uses: "actions/setup-python@v4" with: python-version: "${{ matrix.python-version }}" + allow-prereleases: true cache: pip cache-dependency-path: 'requirements/*.pip' @@ -74,7 +87,7 @@ set -xe python -VV python -m site - python -m pip install --require-hashes -r requirements/tox.pip + python -m pip install -r requirements/tox.pip - name: "Run tox coverage for ${{ matrix.python-version }}" env: @@ -84,6 +97,13 @@ set -xe python -m tox + - name: "Combine data" + env: + COVERAGE_RCFILE: "metacov.ini" + run: | + python -m coverage combine + mv .metacov .metacov.${{ matrix.python-version }}.${{ matrix.os }} + - name: "Upload coverage data" uses: actions/upload-artifact@v3 with: @@ -94,6 +114,10 @@ name: "Combine coverage data" needs: coverage runs-on: ubuntu-latest + outputs: + total: ${{ steps.total.outputs.total }} + env: + COVERAGE_RCFILE: "metacov.ini" steps: - name: "Check out the repo" @@ -102,7 +126,7 @@ - name: "Set up Python" uses: "actions/setup-python@v4" with: - python-version: "3.8" + python-version: "3.7" # Minimum of PYVERSIONS cache: pip cache-dependency-path: 'requirements/*.pip' @@ -122,13 +146,10 @@ - name: "Combine and report" id: combine env: - COVERAGE_RCFILE: "metacov.ini" - COVERAGE_METAFILE: ".metacov" COVERAGE_CONTEXT: "yes" run: | set -xe - python -m igor combine_html - python -m coverage json + python igor.py combine_html - name: "Upload HTML report" uses: actions/upload-artifact@v3 @@ -136,11 +157,10 @@ name: html_report path: htmlcov - - name: "Upload JSON report" - uses: actions/upload-artifact@v3 - with: - name: json_report - path: coverage.json + - name: "Get total" + id: total + run: | + echo "total=$(python -m coverage report --format=total)" >> $GITHUB_OUTPUT publish: name: "Publish coverage report" @@ -148,45 +168,46 @@ runs-on: ubuntu-latest steps: - - name: "Checkout reports repo" - run: | - set -xe - git clone --depth=1 --no-checkout https://${{ secrets.COVERAGE_REPORTS_TOKEN }}@github.com/nedbat/coverage-reports reports_repo - cd reports_repo - git sparse-checkout init --cone - git sparse-checkout set --skip-checks '/*' '!/reports' - git config user.name nedbat - git config user.email ned@nedbatchelder.com - git checkout main - - - name: "Download coverage JSON report" - uses: actions/download-artifact@v3 - with: - name: json_report - - name: "Compute info for later steps" id: info run: | set -xe - export TOTAL=$(python -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])") export SHA10=$(echo ${{ github.sha }} | cut -c 1-10) export SLUG=$(date +'%Y%m%d')_$SHA10 export REPORT_DIR=reports/$SLUG/htmlcov export REF="${{ github.ref }}" - echo "total=$TOTAL" >> $GITHUB_ENV + echo "total=${{ needs.combine.outputs.total }}" >> $GITHUB_ENV echo "sha10=$SHA10" >> $GITHUB_ENV echo "slug=$SLUG" >> $GITHUB_ENV echo "report_dir=$REPORT_DIR" >> $GITHUB_ENV - echo "url=https://nedbat.github.io/coverage-reports/$REPORT_DIR" >> $GITHUB_ENV + echo "url=https://htmlpreview.github.io/?https://github.com/nedbat/coverage-reports/blob/main/reports/$SLUG/htmlcov/index.html" >> $GITHUB_ENV echo "branch=${REF#refs/heads/}" >> $GITHUB_ENV + - name: "Summarize" + run: | + echo '### Total coverage: ${{ env.total }}%' >> $GITHUB_STEP_SUMMARY + + - name: "Checkout reports repo" + if: ${{ github.ref == 'refs/heads/master' }} + run: | + set -xe + git clone --depth=1 --no-checkout https://${{ secrets.COVERAGE_REPORTS_TOKEN }}@github.com/nedbat/coverage-reports reports_repo + cd reports_repo + git sparse-checkout init --cone + git sparse-checkout set --skip-checks '/*' '!/reports' + git config user.name nedbat + git config user.email ned@nedbatchelder.com + git checkout main + - name: "Download coverage HTML report" + if: ${{ github.ref == 'refs/heads/master' }} uses: actions/download-artifact@v3 with: name: html_report path: reports_repo/${{ env.report_dir }} - name: "Push to report repo" + if: ${{ github.ref == 'refs/heads/master' }} env: COMMIT_MESSAGE: ${{ github.event.head_commit.message }} run: | @@ -198,6 +219,8 @@ # Make the commit message. echo "${{ env.total }}% - $COMMIT_MESSAGE" > commit.txt echo "" >> commit.txt + echo "[View the report](${{ env.url }})" >> commit.txt + echo "" >> commit.txt echo "${{ env.url }}" >> commit.txt echo "${{ env.sha10 }}: ${{ env.branch }}" >> commit.txt # Commit. @@ -207,11 +230,12 @@ git add ${{ env.report_dir }} latest.html git commit --file=../commit.txt git push + echo '[${{ env.url }}](${{ env.url }})' >> $GITHUB_STEP_SUMMARY - name: "Create badge" + if: ${{ github.ref == 'refs/heads/master' }} # https://gist.githubusercontent.com/nedbat/8c6980f77988a327348f9b02bbaf67f5 - # uses: schneegans/dynamic-badges-action@v1.4.0 - uses: schneegans/dynamic-badges-action@54d929a33e7521ab6bf19d323d28fb7b876c53f7 + uses: schneegans/dynamic-badges-action@5d424ad4060f866e4d1dab8f8da0456e6b1c4f56 with: auth: ${{ secrets.METACOV_GIST_SECRET }} gistID: 8c6980f77988a327348f9b02bbaf67f5 @@ -221,8 +245,3 @@ minColorRange: 60 maxColorRange: 95 valColorRange: ${{ env.total }} - - - name: "Create summary" - run: | - echo '### Total coverage: ${{ env.total }}%' >> $GITHUB_STEP_SUMMARY - echo '[${{ env.url }}](${{ env.url }})' >> $GITHUB_STEP_SUMMARY diff -Nru python-coverage-6.5.0+dfsg1/.github/workflows/dependency-review.yml python-coverage-7.2.7+dfsg1/.github/workflows/dependency-review.yml --- python-coverage-6.5.0+dfsg1/.github/workflows/dependency-review.yml 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/.github/workflows/dependency-review.yml 2023-05-29 19:46:30.000000000 +0000 @@ -4,8 +4,15 @@ # # Source repository: https://github.com/actions/dependency-review-action # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement + name: 'Dependency Review' -on: [pull_request] +on: + push: + branches: + - master + - nedbat/* + pull_request: + workflow_dispatch: permissions: contents: read @@ -17,4 +24,7 @@ - name: 'Checkout Repository' uses: actions/checkout@v3 - name: 'Dependency Review' - uses: actions/dependency-review-action@v2 + uses: actions/dependency-review-action@v3 + with: + base-ref: ${{ github.event.pull_request.base.sha || 'master' }} + head-ref: ${{ github.event.pull_request.head.sha || github.ref }} diff -Nru python-coverage-6.5.0+dfsg1/.github/workflows/kit.yml python-coverage-7.2.7+dfsg1/.github/workflows/kit.yml --- python-coverage-6.5.0+dfsg1/.github/workflows/kit.yml 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/.github/workflows/kit.yml 2023-05-29 19:46:30.000000000 +0000 @@ -47,7 +47,7 @@ jobs: wheels: - name: "Build ${{ matrix.os }} ${{ matrix.py }} ${{ matrix.arch }} wheels" + name: "${{ matrix.py }} ${{ matrix.os }} ${{ matrix.arch }} wheels" runs-on: ${{ matrix.os }}-latest strategy: matrix: @@ -77,11 +77,12 @@ # } # # PYVERSIONS. Available versions: # # https://github.com/actions/python-versions/blob/main/versions-manifest.json - # pys = ["cp37", "cp38", "cp39", "cp310", "cp311"] + # # PyPy versions are handled further below in the "pypy" step. + # pys = ["cp37", "cp38", "cp39", "cp310", "cp311", "cp312"] # # # Some OS/arch combinations need overrides for the Python versions: # os_arch_pys = { - # ("macos", "arm64"): ["cp38", "cp39", "cp310"], + # ("macos", "arm64"): ["cp38", "cp39", "cp310", "cp311"], # } # # #----- ^^^ ---------------------- ^^^ ----- @@ -102,42 +103,48 @@ - {"os": "ubuntu", "py": "cp39", "arch": "x86_64"} - {"os": "ubuntu", "py": "cp310", "arch": "x86_64"} - {"os": "ubuntu", "py": "cp311", "arch": "x86_64"} + - {"os": "ubuntu", "py": "cp312", "arch": "x86_64"} - {"os": "ubuntu", "py": "cp37", "arch": "i686"} - {"os": "ubuntu", "py": "cp38", "arch": "i686"} - {"os": "ubuntu", "py": "cp39", "arch": "i686"} - {"os": "ubuntu", "py": "cp310", "arch": "i686"} - {"os": "ubuntu", "py": "cp311", "arch": "i686"} + - {"os": "ubuntu", "py": "cp312", "arch": "i686"} - {"os": "ubuntu", "py": "cp37", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp38", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp39", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp310", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp311", "arch": "aarch64"} + - {"os": "ubuntu", "py": "cp312", "arch": "aarch64"} - {"os": "macos", "py": "cp38", "arch": "arm64"} - {"os": "macos", "py": "cp39", "arch": "arm64"} - {"os": "macos", "py": "cp310", "arch": "arm64"} + - {"os": "macos", "py": "cp311", "arch": "arm64"} - {"os": "macos", "py": "cp37", "arch": "x86_64"} - {"os": "macos", "py": "cp38", "arch": "x86_64"} - {"os": "macos", "py": "cp39", "arch": "x86_64"} - {"os": "macos", "py": "cp310", "arch": "x86_64"} - {"os": "macos", "py": "cp311", "arch": "x86_64"} + - {"os": "macos", "py": "cp312", "arch": "x86_64"} - {"os": "windows", "py": "cp37", "arch": "x86"} - {"os": "windows", "py": "cp38", "arch": "x86"} - {"os": "windows", "py": "cp39", "arch": "x86"} - {"os": "windows", "py": "cp310", "arch": "x86"} - {"os": "windows", "py": "cp311", "arch": "x86"} + - {"os": "windows", "py": "cp312", "arch": "x86"} - {"os": "windows", "py": "cp37", "arch": "AMD64"} - {"os": "windows", "py": "cp38", "arch": "AMD64"} - {"os": "windows", "py": "cp39", "arch": "AMD64"} - {"os": "windows", "py": "cp310", "arch": "AMD64"} - {"os": "windows", "py": "cp311", "arch": "AMD64"} - # [[[end]]] (checksum: 428e5138336453464dde968cc3149f4f) + - {"os": "windows", "py": "cp312", "arch": "AMD64"} + # [[[end]]] (checksum: 5e62f362263935c1e3a21299f8a1b649) fail-fast: false steps: - name: "Setup QEMU" if: matrix.os == 'ubuntu' - # uses: docker/setup-qemu-action@v2 - uses: docker/setup-qemu-action@8b122486cedac8393e77aa9734c3528886e4a1a8 + uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 with: platforms: arm64 @@ -147,19 +154,21 @@ - name: "Install Python 3.8" uses: actions/setup-python@v4 with: + # PYVERSIONS python-version: "3.8" cache: pip cache-dependency-path: 'requirements/*.pip' - name: "Install tools" run: | - python -m pip install --require-hashes -r requirements/kit.pip + python -m pip install -r requirements/kit.pip - name: "Build wheels" env: CIBW_BUILD: ${{ matrix.py }}-* CIBW_ARCHS: ${{ matrix.arch }} CIBW_ENVIRONMENT: PIP_DISABLE_PIP_VERSION_CHECK=1 + CIBW_PRERELEASE_PYTHONS: True CIBW_TEST_COMMAND: python -c "from coverage.tracer import CTracer; print('CTracer OK!')" run: | python -m cibuildwheel --output-dir wheelhouse @@ -173,9 +182,10 @@ with: name: dist path: wheelhouse/*.whl + retention-days: 7 sdist: - name: "Build source distribution" + name: "Source distribution" runs-on: ubuntu-latest steps: - name: "Check out the repo" @@ -184,13 +194,14 @@ - name: "Install Python 3.8" uses: actions/setup-python@v4 with: + # PYVERSIONS python-version: "3.8" cache: pip cache-dependency-path: 'requirements/*.pip' - name: "Install tools" run: | - python -m pip install --require-hashes -r requirements/kit.pip + python -m pip install -r requirements/kit.pip - name: "Build sdist" run: | @@ -205,9 +216,10 @@ with: name: dist path: dist/*.tar.gz + retention-days: 7 pypy: - name: "Build PyPy wheel" + name: "PyPy wheel" runs-on: ubuntu-latest steps: - name: "Check out the repo" @@ -216,8 +228,7 @@ - name: "Install PyPy" uses: actions/setup-python@v4 with: - # PYVERSIONS - python-version: "pypy-3.7" + python-version: "pypy-3.7" # Minimum of PyPy PYVERSIONS cache: pip cache-dependency-path: 'requirements/*.pip' @@ -227,9 +238,9 @@ - name: "Build wheel" run: | - # One wheel works for all PyPy versions. + # One wheel works for all PyPy versions. PYVERSIONS # yes, this is weird syntax: https://github.com/pypa/build/issues/202 - pypy3 -m build -w -C="--global-option=--python-tag" -C="--global-option=pp36.pp37.pp38" + pypy3 -m build -w -C="--global-option=--python-tag" -C="--global-option=pp37.pp38.pp39" - name: "List wheels" run: | @@ -240,3 +251,40 @@ with: name: dist path: dist/*.whl + retention-days: 7 + + sign: + # This signs our artifacts, but we don't use the signatures for anything + # yet. Someday maybe PyPI will have a way to upload and verify them. + name: "Sign artifacts" + needs: + - wheels + - sdist + - pypy + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - name: "Download artifacts" + uses: actions/download-artifact@v3 + with: + name: dist + + - name: "Sign artifacts" + uses: sigstore/gh-action-sigstore-python@v1.2.3 + with: + inputs: coverage-*.* + + - name: "List files" + run: | + ls -alR + + - name: "Upload signatures" + uses: actions/upload-artifact@v3 + with: + name: signatures + path: | + *.crt + *.sig + *.sigstore + retention-days: 7 diff -Nru python-coverage-6.5.0+dfsg1/.github/workflows/python-nightly.yml python-coverage-7.2.7+dfsg1/.github/workflows/python-nightly.yml --- python-coverage-6.5.0+dfsg1/.github/workflows/python-nightly.yml 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/.github/workflows/python-nightly.yml 2023-05-29 19:46:30.000000000 +0000 @@ -31,23 +31,29 @@ jobs: tests: - name: "Python ${{ matrix.python-version }}" - runs-on: ubuntu-latest + name: "${{ matrix.python-version }}" + # Choose a recent Ubuntu that deadsnakes still builds all the versions for. + # For example, deadsnakes doesn't provide 3.10 nightly for 22.04 (jammy) + # because jammy ships 3.10, and deadsnakes doesn't want to clobber it. + # https://launchpad.net/~deadsnakes/+archive/ubuntu/nightly/+packages + # https://github.com/deadsnakes/issues/issues/234 + runs-on: ubuntu-20.04 strategy: matrix: python-version: - # When changing this list, be sure to check the [gh-actions] list in + # When changing this list, be sure to check the [gh] list in # tox.ini so that tox will run properly. PYVERSIONS # Available versions: # https://launchpad.net/~deadsnakes/+archive/ubuntu/nightly/+packages - - "3.9-dev" - "3.10-dev" - "3.11-dev" + - "3.12-dev" # https://github.com/actions/setup-python#available-versions-of-pypy - "pypy-3.7-nightly" - "pypy-3.8-nightly" - "pypy-3.9-nightly" + - "pypy-3.10-nightly" fail-fast: false steps: @@ -55,8 +61,7 @@ uses: "actions/checkout@v3" - name: "Install ${{ matrix.python-version }} with deadsnakes" - # uses: deadsnakes/action@v2.1.1 - uses: deadsnakes/action@7ab8819e223c70d2bdedd692dfcea75824e0a617 + uses: deadsnakes/action@e3117c2981fd8afe4af79f3e1be80066c82b70f5 if: "!startsWith(matrix.python-version, 'pypy-')" with: python-version: "${{ matrix.python-version }}" @@ -77,7 +82,7 @@ - name: "Install dependencies" run: | - python -m pip install --require-hashes -r requirements/tox.pip + python -m pip install -r requirements/tox.pip - name: "Run tox" run: | diff -Nru python-coverage-6.5.0+dfsg1/.github/workflows/quality.yml python-coverage-7.2.7+dfsg1/.github/workflows/quality.yml --- python-coverage-6.5.0+dfsg1/.github/workflows/quality.yml 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/.github/workflows/quality.yml 2023-05-29 19:46:30.000000000 +0000 @@ -46,15 +46,37 @@ - name: "Install dependencies" run: | - set -xe - python -VV - python -m site - python -m pip install --require-hashes -r requirements/tox.pip + python -m pip install -r requirements/tox.pip - name: "Tox lint" run: | python -m tox -e lint + mypy: + name: "Check types" + runs-on: ubuntu-latest + + steps: + - name: "Check out the repo" + uses: "actions/checkout@v3" + + - name: "Install Python" + uses: "actions/setup-python@v4" + with: + python-version: "3.8" # Minimum of PYVERSIONS, but at least 3.8 + cache: pip + cache-dependency-path: 'requirements/*.pip' + + - name: "Install dependencies" + run: | + # We run on 3.8, but the pins were made on 3.7, so don't insist on + # hashes, which won't match. + python -m pip install -r requirements/tox.pip + + - name: "Tox mypy" + run: | + python -m tox -e mypy + doc: name: "Build docs" runs-on: ubuntu-latest @@ -75,7 +97,7 @@ set -xe python -VV python -m site - python -m pip install --require-hashes -r requirements/tox.pip + python -m pip install -r requirements/tox.pip - name: "Tox doc" run: | diff -Nru python-coverage-6.5.0+dfsg1/.github/workflows/testsuite.yml python-coverage-7.2.7+dfsg1/.github/workflows/testsuite.yml --- python-coverage-6.5.0+dfsg1/.github/workflows/testsuite.yml 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/.github/workflows/testsuite.yml 2023-05-29 19:46:30.000000000 +0000 @@ -18,6 +18,7 @@ env: PIP_DISABLE_PIP_VERSION_CHECK: 1 COVERAGE_IGOR_VERBOSE: 1 + FORCE_COLOR: 1 # Get colored pytest output permissions: contents: read @@ -38,7 +39,7 @@ - macos - windows python-version: - # When changing this list, be sure to check the [gh-actions] list in + # When changing this list, be sure to check the [gh] list in # tox.ini so that tox will run properly. PYVERSIONS # Available versions: # https://github.com/actions/python-versions/blob/main/versions-manifest.json @@ -47,9 +48,14 @@ - "3.8" - "3.9" - "3.10" - - "3.11.0-rc.2" + - "3.11" + - "3.12" - "pypy-3.7" - "pypy-3.9" + exclude: + # Windows PyPy-3.9 always gets killed. + - os: windows + python-version: "pypy-3.9" fail-fast: false steps: @@ -60,6 +66,7 @@ uses: "actions/setup-python@v4" with: python-version: "${{ matrix.python-version }}" + allow-prereleases: true cache: pip cache-dependency-path: 'requirements/*.pip' @@ -68,36 +75,31 @@ set -xe python -VV python -m site - python -m pip install --require-hashes -r requirements/tox.pip + python -m pip install -r requirements/tox.pip # For extreme debugging: # python -c "import urllib.request as r; exec(r.urlopen('https://bit.ly/pydoctor').read())" - name: "Run tox for ${{ matrix.python-version }}" - continue-on-error: true - id: tox1 run: | python -m tox -- -rfsEX - name: "Retry tox for ${{ matrix.python-version }}" - id: tox2 - if: steps.tox1.outcome == 'failure' + if: failure() run: | - python -m tox -- -rfsEX - - - name: "Set status" - if: always() - run: | - if ${{ steps.tox1.outcome != 'success' && steps.tox2.outcome != 'success' }}; then - exit 1 - fi + # `exit 1` makes sure that the job remains red with flaky runs + python -m tox -- -rfsEX --lf -vvvvv && exit 1 - # A final step to give a simple name for required status checks. + # This job aggregates test results. It's the required check for branch protection. + # https://github.com/marketplace/actions/alls-green#why # https://github.com/orgs/community/discussions/33579 success: - needs: tests - runs-on: ubuntu-latest name: Tests successful + if: always() + needs: + - tests + runs-on: ubuntu-latest steps: - - name: "Success" - run: | - echo Tests successful + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe + with: + jobs: ${{ toJSON(needs) }} diff -Nru python-coverage-6.5.0+dfsg1/howto.txt python-coverage-7.2.7+dfsg1/howto.txt --- python-coverage-6.5.0+dfsg1/howto.txt 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/howto.txt 2023-05-29 19:46:30.000000000 +0000 @@ -1,55 +1,63 @@ * Release checklist - Check that the current virtualenv matches the current coverage branch. -- Version number in coverage/version.py +- start branch for release work + $ make relbranch +- Edit version number in coverage/version.py version_info = (4, 0, 2, "alpha", 1) version_info = (4, 0, 2, "beta", 1) version_info = (4, 0, 2, "candidate", 1) version_info = (4, 0, 2, "final", 0) -- Supported Python version numbers. Search for "PYVERSIONS". -- Copyright date in NOTICE.txt -- run `python igor.py cheats` to get useful snippets for next steps. -- Update CHANGES.rst, including release date. - - don't forget the jump target + - make sure: _dev = 0 +- Edit supported Python version numbers. Search for "PYVERSIONS". + - Especially README.rst and doc/index.rst +- Update source files with release facts: + $ make edit_for_release +- Get useful snippets for next steps, and beyond, in cheats.txt + $ make cheats +- Look over CHANGES.rst - Update README.rst - "New in x.y:" - - Python versions supported - Update docs - - Python versions in doc/index.rst - IF PRE-RELEASE: - Version of latest stable release in doc/index.rst - - Version, release, release_date and copyright date in doc/conf.py - - Look for CHANGEME comments - Make sure the docs are cogged: $ make prebuild - Don't forget the man page: doc/python-coverage.1.txt - Check that the docs build correctly: $ tox -e doc - commit the release-prep changes + $ make relcommit1 - Generate new sample_html to get the latest, incl footer version number: - IF PRE-RELEASE: $ make sample_html_beta - IF NOT PRE-RELEASE: $ make sample_html - check in the new sample html -- Done with changes to source files, check them in. - $ git push + - check in the new sample html + $ make relcommit2 +- Done with changes to source files + - check them in on the release prep branch + - wait for ci to finish + - merge to master + - git push +- Start the kits: + - Trigger the kit GitHub Action + $ make build_kits - Build and publish docs: - IF PRE-RELEASE: $ make publishbeta - ELSE: $ make publish + - commit and publish nedbatchelder.com - Kits: - - Trigger the kit GitHub Action - $ make build_kits - - wait for it to finish: - - https://github.com/nedbat/coveragepy/actions/workflows/kit.yml + - Wait for kits to finish: + - https://github.com/nedbat/coveragepy/actions/workflows/kit.yml - Download and check built kits from GitHub Actions: $ make clean download_kits check_kits - examine the dist directory, and remove anything that looks malformed. + - opvars - test the pypi upload: $ make test_upload -- Update PyPI: - upload kits: $ make kit_upload - Tag the tree @@ -59,33 +67,25 @@ $ make update_stable - Update GitHub releases: $ make clean github_releases +- Visit the fixed issues on GitHub and mention the version it was fixed in. + $ make comment_on_fixes +- unopvars - Bump version: - - coverage/version.py - - increment version number - - IF NOT PRE-RELEASE: - - set to alpha-0 if just released. - - CHANGES.rst - - add an "Unreleased" section to the top. - $ git push + $ make bump_version - Update readthedocs - @ https://readthedocs.org/projects/coverage/versions/ - find the latest tag in the inactive list, edit it, make it active. - readthedocs won't find the tag until a commit is made on master. - keep just the latest version of each x.y release, make the rest active but hidden. + - pre-releases should be hidden - IF NOT PRE-RELEASE: - @ https://readthedocs.org/projects/coverage/builds/ - wait for the new tag build to finish successfully. - @ https://readthedocs.org/dashboard/coverage/advanced/ - change the default version to the new version -- Visit the fixed issues on GitHub and mention the version it was fixed in. - $ make comment_on_fixes - - "This is now released as part of [coverage 5.2](https://pypi.org/project/coverage/5.2)." -- Announce: - - twitter @coveragepy - - nedbatchelder.com blog post? +- Once CI passes, merge the bump-version branch to master and push it + - things to automate: - - url to link to latest changes in docs - - next version.py line - readthedocs api to do the readthedocs changes diff -Nru python-coverage-6.5.0+dfsg1/igor.py python-coverage-7.2.7+dfsg1/igor.py --- python-coverage-6.5.0+dfsg1/igor.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/igor.py 2023-05-29 19:46:30.000000000 +0000 @@ -10,15 +10,18 @@ import contextlib import datetime -import fnmatch import glob import inspect +import itertools import os import platform +import pprint +import re import subprocess import sys import sysconfig import textwrap +import types import warnings import zipfile @@ -75,10 +78,11 @@ "-c", "import coverage; print(coverage.__file__)" ], encoding="utf-8").strip()) + roots = [root] else: - root = "coverage" + roots = ["coverage", "build/*/coverage"] - for pattern in so_patterns: + for root, pattern in itertools.product(roots, so_patterns): pattern = os.path.join(root, pattern.strip()) if VERBOSITY: print(f"Searching for {pattern}") @@ -160,6 +164,8 @@ os.environ['COVERAGE_HOME'] = os.getcwd() context = os.environ.get('COVERAGE_CONTEXT') if context: + if context[0] == "$": + context = os.environ[context[1:]] os.environ['COVERAGE_CONTEXT'] = context + "." + tracer # Create the .pth file that will let us measure coverage in sub-processes. @@ -203,25 +209,24 @@ cov.stop() os.remove(pth_path) - cov.combine() cov.save() - return status def do_combine_html(): - """Combine data from a meta-coverage run, and make the HTML and XML reports.""" + """Combine data from a meta-coverage run, and make the HTML report.""" import coverage os.environ['COVERAGE_HOME'] = os.getcwd() - os.environ['COVERAGE_METAFILE'] = os.path.abspath(".metacov") cov = coverage.Coverage(config_file="metacov.ini") cov.load() cov.combine() cov.save() + # A new Coverage to turn on messages. Better would be to have tighter + # control over message verbosity... + cov = coverage.Coverage(config_file="metacov.ini", messages=True) + cov.load() show_contexts = bool(os.environ.get('COVERAGE_DYNCTX') or os.environ.get('COVERAGE_CONTEXT')) cov.html_report(show_contexts=show_contexts) - cov.xml_report() - cov.json_report(pretty_print=True) def do_test_with_tracer(tracer, *runner_args): @@ -277,77 +282,6 @@ zf.write("coverage/__main__.py", "__main__.py") -def do_check_eol(): - """Check files for incorrect newlines and trailing whitespace.""" - - ignore_dirs = [ - '.svn', '.hg', '.git', - '.tox*', - '*.egg-info', - '_build', - '_spell', - 'tmp', - 'help', - ] - checked = set() - - def check_file(fname, crlf=True, trail_white=True): - """Check a single file for whitespace abuse.""" - fname = os.path.relpath(fname) - if fname in checked: - return - checked.add(fname) - - line = None - with open(fname, "rb") as f: - for n, line in enumerate(f, start=1): - if crlf: - if b"\r" in line: - print(f"{fname}@{n}: CR found") - return - if trail_white: - line = line[:-1] - if not crlf: - line = line.rstrip(b'\r') - if line.rstrip() != line: - print(f"{fname}@{n}: trailing whitespace found") - return - - if line is not None and not line.strip(): - print(f"{fname}: final blank line") - - def check_files(root, patterns, **kwargs): - """Check a number of files for whitespace abuse.""" - for where, dirs, files in os.walk(root): - for f in files: - fname = os.path.join(where, f) - for p in patterns: - if fnmatch.fnmatch(fname, p): - check_file(fname, **kwargs) - break - for ignore_dir in ignore_dirs: - ignored = [] - for dir_name in dirs: - if fnmatch.fnmatch(dir_name, ignore_dir): - ignored.append(dir_name) - for dir_name in ignored: - dirs.remove(dir_name) - - check_files("coverage", ["*.py"]) - check_files("coverage/ctracer", ["*.c", "*.h"]) - check_files("coverage/htmlfiles", ["*.html", "*.scss", "*.css", "*.js"]) - check_files("tests", ["*.py"]) - check_files("tests", ["*,cover"], trail_white=False) - check_files("tests/js", ["*.js", "*.html"]) - check_file("setup.py") - check_file("igor.py") - check_file("Makefile") - check_files(".", ["*.rst", "*.txt"]) - check_files(".", ["*.pip"]) - check_files(".github", ["*"]) - check_files("ci", ["*"]) - - def print_banner(label): """Print the version of Python.""" try: @@ -380,56 +314,113 @@ return proc.returncode -def do_cheats(): - """Show a cheatsheet of useful things during releasing.""" +def get_release_facts(): + """Return an object with facts about the current release.""" import coverage - ver = coverage.__version__ - vi = coverage.version_info - shortver = f"{vi[0]}.{vi[1]}.{vi[2]}" - anchor = shortver.replace(".", "-") - if vi[3] != "final": - anchor += f"{vi[3][0]}{vi[4]}" - now = datetime.datetime.now() - branch = subprocess.getoutput("git rev-parse --abbrev-ref @") - print(f"Coverage version is {ver}") + import coverage.version + facts = types.SimpleNamespace() + facts.ver = coverage.__version__ + mjr, mnr, mcr, rel, ser = facts.vi = coverage.version_info + facts.dev = coverage.version._dev + facts.shortver = f"{mjr}.{mnr}.{mcr}" + facts.anchor = facts.shortver.replace(".", "-") + if rel == "final": + facts.next_vi = (mjr, mnr, mcr+1, "alpha", 0) + else: + facts.anchor += f"{rel[0]}{ser}" + facts.next_vi = (mjr, mnr, mcr, rel, ser + 1) - print(f"pip install git+https://github.com/nedbat/coveragepy@{branch}") - print(f"https://coverage.readthedocs.io/en/{ver}/changes.html#changes-{anchor}") + facts.now = datetime.datetime.now() + facts.branch = subprocess.getoutput("git rev-parse --abbrev-ref @") + facts.sha = subprocess.getoutput("git rev-parse @") + return facts + + +def update_file(fname, pattern, replacement): + """Update the contents of a file, replacing pattern with replacement.""" + with open(fname) as fobj: + old_text = fobj.read() + + new_text = re.sub(pattern, replacement, old_text, count=1) + + if new_text != old_text: + print(f"Updating {fname}") + with open(fname, "w") as fobj: + fobj.write(new_text) + +UNRELEASED = "Unreleased\n----------" +SCRIV_START = ".. scriv-start-here\n\n" + +def do_edit_for_release(): + """Edit a few files in preparation for a release.""" + facts = get_release_facts() + + if facts.dev: + print(f"**\n** This is a dev release: {facts.ver}\n**\n\nNo edits") + return + + # NOTICE.txt + update_file("NOTICE.txt", r"Copyright 2004.*? Ned", f"Copyright 2004-{facts.now:%Y} Ned") + + # CHANGES.rst + title = f"Version {facts.ver} — {facts.now:%Y-%m-%d}" + rule = "-" * len(title) + new_head = f".. _changes_{facts.anchor}:\n\n{title}\n{rule}" + + update_file("CHANGES.rst", re.escape(SCRIV_START), "") + update_file("CHANGES.rst", re.escape(UNRELEASED), SCRIV_START + new_head) + + # doc/conf.py + new_conf = textwrap.dedent(f"""\ + # @@@ editable + copyright = "2009\N{EN DASH}{facts.now:%Y}, Ned Batchelder" # pylint: disable=redefined-builtin + # The short X.Y.Z version. + version = "{facts.shortver}" + # The full version, including alpha/beta/rc tags. + release = "{facts.ver}" + # The date of release, in "monthname day, year" format. + release_date = "{facts.now:%B %-d, %Y}" + # @@@ end + """) + update_file("doc/conf.py", r"(?s)# @@@ editable\n.*# @@@ end\n", new_conf) + + +def do_bump_version(): + """Edit a few files right after a release to bump the version.""" + facts = get_release_facts() + + # CHANGES.rst + update_file( + "CHANGES.rst", + re.escape(SCRIV_START), + f"{UNRELEASED}\n\nNothing yet.\n\n\n" + SCRIV_START, + ) - print("\n## for CHANGES.rst before release:") - print(f".. _changes_{anchor}:") + # coverage/version.py + next_version = f"version_info = {facts.next_vi}\n_dev = 1".replace("'", '"') + update_file("coverage/version.py", r"(?m)^version_info = .*\n_dev = \d+$", next_version) + + +def do_cheats(): + """Show a cheatsheet of useful things during releasing.""" + facts = get_release_facts() + pprint.pprint(facts.__dict__) print() - head = f"Version {ver} — {now:%Y-%m-%d}" - print(head) - print("-" * len(head)) - - print("\n## For doc/conf.py before release:") - print("\n".join([ - '# The short X.Y.Z version. # CHANGEME', - f'version = "{shortver}"', - '# The full version, including alpha/beta/rc tags. # CHANGEME', - f'release = "{ver}"', - '# The date of release, in "monthname day, year" format. # CHANGEME', - f'release_date = "{now:%B %-d, %Y}"', - ])) + print(f"Coverage version is {facts.ver}") + + egg = "egg=coverage==0.0" # to force a re-install + if facts.branch == "master": + print(f"pip install git+https://github.com/nedbat/coveragepy#{egg}") + else: + print(f"pip install git+https://github.com/nedbat/coveragepy@{facts.branch}#{egg}") + print(f"pip install git+https://github.com/nedbat/coveragepy@{facts.sha}#{egg}") + print(f"https://coverage.readthedocs.io/en/{facts.ver}/changes.html#changes-{facts.anchor}") print( "\n## For GitHub commenting:\n" + "This is now released as part of " + - f"[coverage {ver}](https://pypi.org/project/coverage/{ver})." + f"[coverage {facts.ver}](https://pypi.org/project/coverage/{facts.ver})." ) - print("\n## For version.py next:") - next_vi = (vi[0], vi[1], vi[2]+1, "alpha", 0) - print(f"version_info = {next_vi}".replace("'", '"')) - print("\n## For CHANGES.rst after release:") - print(textwrap.dedent("""\ - Unreleased - ---------- - - Nothing yet. - - - """)) def do_help(): diff -Nru python-coverage-6.5.0+dfsg1/lab/benchmark/benchmark.py python-coverage-7.2.7+dfsg1/lab/benchmark/benchmark.py --- python-coverage-6.5.0+dfsg1/lab/benchmark/benchmark.py 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/lab/benchmark/benchmark.py 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,584 @@ +"""Run performance comparisons for versions of coverage""" + +import collections +import contextlib +import dataclasses +import itertools +import os +import random +import shutil +import statistics +import subprocess +import sys +import time +from pathlib import Path + +from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple + + +class ShellSession: + """A logged shell session. + + The duration of the last command is available as .last_duration. + """ + + def __init__(self, output_filename: str): + self.output_filename = output_filename + self.last_duration: float = 0 + self.foutput = None + + def __enter__(self): + self.foutput = open(self.output_filename, "a", encoding="utf-8") + print(f"Logging output to {os.path.abspath(self.output_filename)}") + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.foutput.close() + + def print(self, *args, **kwargs): + """Print a message to this shell's log.""" + print(*args, **kwargs, file=self.foutput) + + def run_command(self, cmd: str) -> str: + """ + Run a command line (with a shell). + + Returns: + str: the output of the command. + + """ + self.print(f"\n========================\n$ {cmd}") + start = time.perf_counter() + proc = subprocess.run( + cmd, + shell=True, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + output = proc.stdout.decode("utf-8") + self.last_duration = time.perf_counter() - start + self.print(output, end="") + self.print(f"(was: {cmd})") + self.print(f"(in {os.getcwd()}, duration: {self.last_duration:.3f}s)") + + if proc.returncode != 0: + self.print(f"ERROR: command returned {proc.returncode}") + raise Exception( + f"Command failed ({proc.returncode}): {cmd!r}, output was:\n{output}" + ) + + return output.strip() + + +def rmrf(path: Path) -> None: + """ + Remove a directory tree. It's OK if it doesn't exist. + """ + if path.exists(): + shutil.rmtree(path) + + +@contextlib.contextmanager +def change_dir(newdir: Path) -> Iterator[Path]: + """ + Change to a new directory, and then change back. + + Will make the directory if needed. + """ + old_dir = os.getcwd() + newdir.mkdir(parents=True, exist_ok=True) + os.chdir(newdir) + try: + yield newdir + finally: + os.chdir(old_dir) + + +@contextlib.contextmanager +def file_replace(file_name: Path, old_text: str, new_text: str) -> Iterator[None]: + """ + Replace some text in `file_name`, and change it back. + """ + if old_text: + file_text = file_name.read_text() + if old_text not in file_text: + raise Exception("Old text {old_text!r} not found in {file_name}") + updated_text = file_text.replace(old_text, new_text) + file_name.write_text(updated_text) + try: + yield + finally: + if old_text: + file_name.write_text(file_text) + + +class ProjectToTest: + """Information about a project to use as a test case.""" + + # Where can we clone the project from? + git_url: Optional[str] = None + slug: Optional[str] = None + + def __init__(self): + if not self.slug: + if self.git_url: + self.slug = self.git_url.split("/")[-1] + + def shell(self): + return ShellSession(f"output_{self.slug}.log") + + def make_dir(self): + self.dir = Path(f"work_{self.slug}") + if self.dir.exists(): + rmrf(self.dir) + + def get_source(self, shell): + """Get the source of the project.""" + shell.run_command(f"git clone {self.git_url} {self.dir}") + + def prep_environment(self, env): + """Prepare the environment to run the test suite. + + This is not timed. + """ + pass + + def tweak_coverage_settings( + self, settings: Iterable[Tuple[str, Any]] + ) -> Iterator[None]: + """Tweak the coverage settings. + + NOTE: This is not properly factored, and is only used by ToxProject now!!! + """ + pass + + def run_no_coverage(self, env): + """Run the test suite with no coverage measurement.""" + pass + + def run_with_coverage(self, env, pip_args, cov_tweaks): + """Run the test suite with coverage measurement.""" + pass + + +class EmptyProject(ProjectToTest): + """A dummy project for testing other parts of this code.""" + + def __init__(self, slug: str = "empty", fake_durations: Iterable[float] = (1.23,)): + self.slug = slug + self.durations = iter(itertools.cycle(fake_durations)) + + def get_source(self, shell): + pass + + def run_with_coverage(self, env, pip_args, cov_tweaks): + """Run the test suite with coverage measurement.""" + return next(self.durations) + + +class ToxProject(ProjectToTest): + """A project using tox to run the test suite.""" + + def prep_environment(self, env): + env.shell.run_command(f"{env.python} -m pip install 'tox<4'") + self.run_tox(env, env.pyver.toxenv, "--notest") + + def run_tox(self, env, toxenv, toxargs=""): + """Run a tox command. Return the duration.""" + env.shell.run_command(f"{env.python} -m tox -e {toxenv} {toxargs}") + return env.shell.last_duration + + def run_no_coverage(self, env): + return self.run_tox(env, env.pyver.toxenv, "--skip-pkg-install") + + def run_with_coverage(self, env, pip_args, cov_tweaks): + self.run_tox(env, env.pyver.toxenv, "--notest") + env.shell.run_command( + f".tox/{env.pyver.toxenv}/bin/python -m pip install {pip_args}" + ) + with self.tweak_coverage_settings(cov_tweaks): + self.pre_check(env) # NOTE: Not properly factored, and only used from here. + duration = self.run_tox(env, env.pyver.toxenv, "--skip-pkg-install") + self.post_check( + env + ) # NOTE: Not properly factored, and only used from here. + return duration + + +class ProjectPytestHtml(ToxProject): + """pytest-dev/pytest-html""" + + git_url = "https://github.com/pytest-dev/pytest-html" + + def run_with_coverage(self, env, pip_args, cov_tweaks): + raise Exception("This doesn't work because options changed to tweaks") + covenv = env.pyver.toxenv + "-cov" + self.run_tox(env, covenv, "--notest") + env.shell.run_command(f".tox/{covenv}/bin/python -m pip install {pip_args}") + if cov_tweaks: + replace = ("# reference: https", f"[run]\n{cov_tweaks}\n#") + else: + replace = ("", "") + with file_replace(Path(".coveragerc"), *replace): + env.shell.run_command("cat .coveragerc") + env.shell.run_command(f".tox/{covenv}/bin/python -m coverage debug sys") + return self.run_tox(env, covenv, "--skip-pkg-install") + + +class ProjectDateutil(ToxProject): + """dateutil/dateutil""" + + git_url = "https://github.com/dateutil/dateutil" + + def prep_environment(self, env): + super().prep_environment(env) + env.shell.run_command(f"{env.python} updatezinfo.py") + + def run_no_coverage(self, env): + env.shell.run_command("echo No option to run without coverage") + return 0 + + +class ProjectAttrs(ToxProject): + """python-attrs/attrs""" + + git_url = "https://github.com/python-attrs/attrs" + + def tweak_coverage_settings( + self, tweaks: Iterable[Tuple[str, Any]] + ) -> Iterator[None]: + return tweak_toml_coverage_settings("pyproject.toml", tweaks) + + def pre_check(self, env): + env.shell.run_command("cat pyproject.toml") + + def post_check(self, env): + env.shell.run_command("ls -al") + + +def tweak_toml_coverage_settings( + toml_file: str, tweaks: Iterable[Tuple[str, Any]] +) -> Iterator[None]: + if tweaks: + toml_inserts = [] + for name, value in tweaks: + if isinstance(value, bool): + toml_inserts.append(f"{name} = {str(value).lower()}") + elif isinstance(value, str): + toml_inserts.append(f"{name} = '{value}'") + else: + raise Exception(f"Can't tweak toml setting: {name} = {value!r}") + header = "[tool.coverage.run]\n" + insert = header + "\n".join(toml_inserts) + "\n" + else: + header = insert = "" + return file_replace(Path(toml_file), header, insert) + + +class AdHocProject(ProjectToTest): + """A standalone program to run locally.""" + + def __init__(self, python_file, cur_dir=None, pip_args=None): + super().__init__() + self.python_file = Path(python_file) + if not self.python_file.exists(): + raise ValueError(f"Couldn't find {self.python_file} to run ad-hoc.") + self.cur_dir = Path(cur_dir or self.python_file.parent) + if not self.cur_dir.exists(): + raise ValueError(f"Couldn't find {self.cur_dir} to run in.") + self.pip_args = pip_args + self.slug = self.python_file.name + + def get_source(self, shell): + pass + + def prep_environment(self, env): + env.shell.run_command(f"{env.python} -m pip install {self.pip_args}") + + def run_no_coverage(self, env): + with change_dir(self.cur_dir): + env.shell.run_command(f"{env.python} {self.python_file}") + return env.shell.last_duration + + def run_with_coverage(self, env, pip_args, cov_tweaks): + env.shell.run_command(f"{env.python} -m pip install {pip_args}") + with change_dir(self.cur_dir): + env.shell.run_command(f"{env.python} -m coverage run {self.python_file}") + return env.shell.last_duration + + +class SlipcoverBenchmark(AdHocProject): + """ + For running code from the Slipcover benchmarks. + + Clone https://github.com/plasma-umass/slipcover to /src/slipcover + + """ + + def __init__(self, python_file): + super().__init__( + python_file=f"/src/slipcover/benchmarks/{python_file}", + cur_dir="/src/slipcover", + pip_args="six pyperf", + ) + + +class PyVersion: + """A version of Python to use.""" + + # The command to run this Python + command: str + # Short word for messages, directories, etc + slug: str + # The tox environment to run this Python + toxenv: str + + +class Python(PyVersion): + """A version of CPython to use.""" + + def __init__(self, major, minor): + self.command = self.slug = f"python{major}.{minor}" + self.toxenv = f"py{major}{minor}" + + +class PyPy(PyVersion): + """A version of PyPy to use.""" + + def __init__(self, major, minor): + self.command = self.slug = f"pypy{major}.{minor}" + self.toxenv = f"pypy{major}{minor}" + + +class AdHocPython(PyVersion): + """A custom build of Python to use.""" + + def __init__(self, path, slug): + self.command = f"{path}/bin/python3" + self.slug = slug + self.toxenv = None + + +@dataclasses.dataclass +class Coverage: + """A version of coverage.py to use, maybe None.""" + + # Short word for messages, directories, etc + slug: str + # Arguments for "pip install ..." + pip_args: Optional[str] = None + # Tweaks to the .coveragerc file + tweaks: Optional[Iterable[Tuple[str, Any]]] = None + + +class CoveragePR(Coverage): + """A version of coverage.py from a pull request.""" + + def __init__(self, number, tweaks=None): + super().__init__( + slug=f"#{number}", + pip_args=f"git+https://github.com/nedbat/coveragepy.git@refs/pull/{number}/merge", + tweaks=tweaks, + ) + + +class CoverageCommit(Coverage): + """A version of coverage.py from a specific commit.""" + + def __init__(self, sha, tweaks=None): + super().__init__( + slug=sha, + pip_args=f"git+https://github.com/nedbat/coveragepy.git@{sha}", + tweaks=tweaks, + ) + + +class CoverageSource(Coverage): + """The coverage.py in a working tree.""" + + def __init__(self, directory, tweaks=None): + super().__init__( + slug="source", + pip_args=directory, + tweaks=tweaks, + ) + + +@dataclasses.dataclass +class Env: + """An environment to run a test suite in.""" + + pyver: PyVersion + python: Path + shell: ShellSession + + +ResultKey = Tuple[str, str, str] + +DIMENSION_NAMES = ["proj", "pyver", "cov"] + + +class Experiment: + """A particular time experiment to run.""" + + def __init__( + self, + py_versions: List[PyVersion], + cov_versions: List[Coverage], + projects: List[ProjectToTest], + ): + self.py_versions = py_versions + self.cov_versions = cov_versions + self.projects = projects + self.result_data: Dict[ResultKey, List[float]] = {} + + def run(self, num_runs: int = 3) -> None: + total_runs = ( + len(self.projects) + * len(self.py_versions) + * len(self.cov_versions) + * num_runs + ) + total_run_nums = iter(itertools.count(start=1)) + + all_runs = [] + + for proj in self.projects: + print(f"Prepping project {proj.slug}") + with proj.shell() as shell: + proj.make_dir() + proj.get_source(shell) + + for pyver in self.py_versions: + print(f"Making venv for {proj.slug} {pyver.slug}") + venv_dir = f"venv_{proj.slug}_{pyver.slug}" + shell.run_command(f"{pyver.command} -m venv {venv_dir}") + python = Path.cwd() / f"{venv_dir}/bin/python" + shell.run_command(f"{python} -V") + env = Env(pyver, python, shell) + + with change_dir(proj.dir): + print(f"Prepping for {proj.slug} {pyver.slug}") + proj.prep_environment(env) + for cov_ver in self.cov_versions: + all_runs.append((proj, pyver, cov_ver, env)) + + all_runs *= num_runs + random.shuffle(all_runs) + + run_data: Dict[ResultKey, List[float]] = collections.defaultdict(list) + + for proj, pyver, cov_ver, env in all_runs: + total_run_num = next(total_run_nums) + print( + "Running tests: " + + f"{proj.slug}, {pyver.slug}, cov={cov_ver.slug}, " + + f"{total_run_num} of {total_runs}" + ) + with env.shell: + with change_dir(proj.dir): + if cov_ver.pip_args is None: + dur = proj.run_no_coverage(env) + else: + dur = proj.run_with_coverage( + env, + cov_ver.pip_args, + cov_ver.tweaks, + ) + print(f"Tests took {dur:.3f}s") + result_key = (proj.slug, pyver.slug, cov_ver.slug) + run_data[result_key].append(dur) + + # Summarize and collect the data. + print("# Results") + for proj in self.projects: + for pyver in self.py_versions: + for cov_ver in self.cov_versions: + result_key = (proj.slug, pyver.slug, cov_ver.slug) + med = statistics.median(run_data[result_key]) + self.result_data[result_key] = med + print( + f"Median for {proj.slug}, {pyver.slug}, " + + f"cov={cov_ver.slug}: {med:.3f}s" + ) + + def show_results( + self, + rows: List[str], + column: str, + ratios: Iterable[Tuple[str, str, str]] = (), + ) -> None: + dimensions = { + "cov": [cov_ver.slug for cov_ver in self.cov_versions], + "pyver": [pyver.slug for pyver in self.py_versions], + "proj": [proj.slug for proj in self.projects], + } + + table_axes = [dimensions[rowname] for rowname in rows] + data_order = [*rows, column] + remap = [data_order.index(datum) for datum in DIMENSION_NAMES] + + WIDTH = 20 + + def as_table_row(vals): + return "| " + " | ".join(v.ljust(WIDTH) for v in vals) + " |" + + header = [] + header.extend(rows) + header.extend(dimensions[column]) + header.extend(slug for slug, _, _ in ratios) + + print() + print(as_table_row(header)) + dashes = [":---"] * len(rows) + ["---:"] * (len(header) - len(rows)) + print(as_table_row(dashes)) + for tup in itertools.product(*table_axes): + row = [] + row.extend(tup) + col_data = {} + for col in dimensions[column]: + key = (*tup, col) + key = tuple(key[i] for i in remap) + result_time = self.result_data[key] # type: ignore + row.append(f"{result_time:.1f} s") + col_data[col] = result_time + for _, num, denom in ratios: + ratio = col_data[num] / col_data[denom] + row.append(f"{ratio * 100:.0f}%") + print(as_table_row(row)) + + +PERF_DIR = Path("/tmp/covperf") + + +def run_experiment( + py_versions: List[PyVersion], + cov_versions: List[Coverage], + projects: List[ProjectToTest], + rows: List[str], + column: str, + ratios: Iterable[Tuple[str, str, str]] = (), +): + slugs = [v.slug for v in py_versions + cov_versions + projects] + if len(set(slugs)) != len(slugs): + raise Exception(f"Slugs must be unique: {slugs}") + if any(" " in slug for slug in slugs): + raise Exception(f"No spaces in slugs please: {slugs}") + ratio_slugs = [rslug for ratio in ratios for rslug in ratio[1:]] + if any(rslug not in slugs for rslug in ratio_slugs): + raise Exception(f"Ratio slug doesn't match a slug: {ratio_slugs}, {slugs}") + if set(rows + [column]) != set(DIMENSION_NAMES): + raise Exception( + f"All of these must be in rows or column: {', '.join(DIMENSION_NAMES)}" + ) + + print(f"Removing and re-making {PERF_DIR}") + rmrf(PERF_DIR) + + with change_dir(PERF_DIR): + exp = Experiment( + py_versions=py_versions, cov_versions=cov_versions, projects=projects + ) + exp.run(num_runs=int(sys.argv[1])) + exp.show_results(rows=rows, column=column, ratios=ratios) diff -Nru python-coverage-6.5.0+dfsg1/lab/benchmark/empty.py python-coverage-7.2.7+dfsg1/lab/benchmark/empty.py --- python-coverage-6.5.0+dfsg1/lab/benchmark/empty.py 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/lab/benchmark/empty.py 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,29 @@ +from benchmark import * + +run_experiment( + py_versions=[ + Python(3, 9), + Python(3, 11), + ], + cov_versions=[ + Coverage("701", "coverage==7.0.1"), + Coverage( + "701.dynctx", "coverage==7.0.1", [("dynamic_context", "test_function")] + ), + Coverage("702", "coverage==7.0.2"), + Coverage( + "702.dynctx", "coverage==7.0.2", [("dynamic_context", "test_function")] + ), + ], + projects=[ + EmptyProject("empty", [1.2, 3.4]), + EmptyProject("dummy", [6.9, 7.1]), + ], + rows=["proj", "pyver"], + column="cov", + ratios=[ + (".2 vs .1", "702", "701"), + (".1 dynctx cost", "701.dynctx", "701"), + (".2 dynctx cost", "702.dynctx", "702"), + ], +) diff -Nru python-coverage-6.5.0+dfsg1/lab/benchmark/run.py python-coverage-7.2.7+dfsg1/lab/benchmark/run.py --- python-coverage-6.5.0+dfsg1/lab/benchmark/run.py 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/lab/benchmark/run.py 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,54 @@ +from benchmark import * + +if 0: + run_experiment( + py_versions=[ + # Python(3, 11), + AdHocPython("/usr/local/cpython/v3.10.5", "v3.10.5"), + AdHocPython("/usr/local/cpython/v3.11.0b3", "v3.11.0b3"), + AdHocPython("/usr/local/cpython/94231", "94231"), + ], + cov_versions=[ + Coverage("6.4.1", "coverage==6.4.1"), + ], + projects=[ + AdHocProject("/src/bugs/bug1339/bug1339.py"), + SlipcoverBenchmark("bm_sudoku.py"), + SlipcoverBenchmark("bm_spectral_norm.py"), + ], + rows=["cov", "proj"], + column="pyver", + ratios=[ + ("3.11b3 vs 3.10", "v3.11.0b3", "v3.10.5"), + ("94231 vs 3.10", "94231", "v3.10.5"), + ], + ) + + +if 1: + run_experiment( + py_versions=[ + Python(3, 9), + Python(3, 11), + ], + cov_versions=[ + Coverage("701", "coverage==7.0.1"), + Coverage( + "701.dynctx", "coverage==7.0.1", [("dynamic_context", "test_function")] + ), + Coverage("702", "coverage==7.0.2"), + Coverage( + "702.dynctx", "coverage==7.0.2", [("dynamic_context", "test_function")] + ), + ], + projects=[ + ProjectAttrs(), + ], + rows=["proj", "pyver"], + column="cov", + ratios=[ + (".2 vs .1", "702", "701"), + (".1 dynctx cost", "701.dynctx", "701"), + (".2 dynctx cost", "702.dynctx", "702"), + ], + ) diff -Nru python-coverage-6.5.0+dfsg1/lab/benchmark.py python-coverage-7.2.7+dfsg1/lab/benchmark.py --- python-coverage-6.5.0+dfsg1/lab/benchmark.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/lab/benchmark.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,491 +0,0 @@ -"""Run performance comparisons for versions of coverage""" - -import contextlib -import dataclasses -import itertools -import os -import shutil -import statistics -import subprocess -import sys -import time -from pathlib import Path - -from typing import Dict, Iterable, Iterator, List, Optional, Tuple - - -class ShellSession: - """A logged shell session. - - The duration of the last command is available as .last_duration. - """ - - def __init__(self, output_filename: str): - self.output_filename = output_filename - self.last_duration: float = 0 - self.foutput = None - - def __enter__(self): - self.foutput = open(self.output_filename, "a", encoding="utf-8") - print(f"Logging output to {os.path.abspath(self.output_filename)}") - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.foutput.close() - - def print(self, *args, **kwargs): - """Print a message to this shell's log.""" - print(*args, **kwargs, file=self.foutput) - - def run_command(self, cmd: str) -> str: - """ - Run a command line (with a shell). - - Returns: - str: the output of the command. - - """ - self.print(f"\n========================\n$ {cmd}") - start = time.perf_counter() - proc = subprocess.run( - cmd, - shell=True, - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - output = proc.stdout.decode("utf-8") - self.last_duration = time.perf_counter() - start - self.print(output, end="") - self.print(f"(was: {cmd})") - self.print(f"(in {os.getcwd()}, duration: {self.last_duration:.3f}s)") - - if proc.returncode != 0: - self.print(f"ERROR: command returned {proc.returncode}") - raise Exception( - f"Command failed ({proc.returncode}): {cmd!r}, output was:\n{output}" - ) - - return output.strip() - - -def rmrf(path: Path) -> None: - """ - Remove a directory tree. It's OK if it doesn't exist. - """ - if path.exists(): - shutil.rmtree(path) - - -@contextlib.contextmanager -def change_dir(newdir: Path) -> Iterator[Path]: - """ - Change to a new directory, and then change back. - - Will make the directory if needed. - """ - old_dir = os.getcwd() - newdir.mkdir(parents=True, exist_ok=True) - os.chdir(newdir) - try: - yield newdir - finally: - os.chdir(old_dir) - - -@contextlib.contextmanager -def file_replace(file_name: Path, old_text: str, new_text: str) -> Iterator[None]: - """ - Replace some text in `file_name`, and change it back. - """ - if old_text: - file_text = file_name.read_text() - if old_text not in file_text: - raise Exception("Old text {old_text!r} not found in {file_name}") - updated_text = file_text.replace(old_text, new_text) - file_name.write_text(updated_text) - try: - yield - finally: - if old_text: - file_name.write_text(file_text) - - -class ProjectToTest: - """Information about a project to use as a test case.""" - - # Where can we clone the project from? - git_url: Optional[str] = None - - def __init__(self): - if self.git_url: - self.slug = self.git_url.split("/")[-1] - self.dir = Path(self.slug) - - def get_source(self, shell): - """Get the source of the project.""" - if self.dir.exists(): - rmrf(self.dir) - shell.run_command(f"git clone {self.git_url}") - - def prep_environment(self, env): - """Prepare the environment to run the test suite. - - This is not timed. - """ - pass - - def run_no_coverage(self, env): - """Run the test suite with no coverage measurement.""" - pass - - def run_with_coverage(self, env, pip_args, cov_options): - """Run the test suite with coverage measurement.""" - pass - - -class ToxProject(ProjectToTest): - """A project using tox to run the test suite.""" - - def prep_environment(self, env): - env.shell.run_command(f"{env.python} -m pip install tox") - self.run_tox(env, env.pyver.toxenv, "--notest") - - def run_tox(self, env, toxenv, toxargs=""): - """Run a tox command. Return the duration.""" - env.shell.run_command(f"{env.python} -m tox -e {toxenv} {toxargs}") - return env.shell.last_duration - - def run_no_coverage(self, env): - return self.run_tox(env, env.pyver.toxenv, "--skip-pkg-install") - - def run_with_coverage(self, env, pip_args, cov_options): - assert not cov_options, f"ToxProject.run_with_coverage can't take cov_options={cov_options!r}" - self.run_tox(env, env.pyver.toxenv, "--notest") - env.shell.run_command( - f".tox/{env.pyver.toxenv}/bin/python -m pip install {pip_args}" - ) - return self.run_tox(env, env.pyver.toxenv, "--skip-pkg-install") - - -class ProjectPytestHtml(ToxProject): - """pytest-dev/pytest-html""" - - git_url = "https://github.com/pytest-dev/pytest-html" - - def run_with_coverage(self, env, pip_args, cov_options): - covenv = env.pyver.toxenv + "-cov" - self.run_tox(env, covenv, "--notest") - env.shell.run_command(f".tox/{covenv}/bin/python -m pip install {pip_args}") - if cov_options: - replace = ("# reference: https", f"[run]\n{cov_options}\n#") - else: - replace = ("", "") - with file_replace(Path(".coveragerc"), *replace): - env.shell.run_command("cat .coveragerc") - env.shell.run_command(f".tox/{covenv}/bin/python -m coverage debug sys") - return self.run_tox(env, covenv, "--skip-pkg-install") - - -class ProjectDateutil(ToxProject): - """dateutil/dateutil""" - - git_url = "https://github.com/dateutil/dateutil" - - def prep_environment(self, env): - super().prep_environment(env) - env.shell.run_command(f"{env.python} updatezinfo.py") - - def run_no_coverage(self, env): - env.shell.run_command("echo No option to run without coverage") - return 0 - - -class ProjectAttrs(ToxProject): - """python-attrs/attrs""" - - git_url = "https://github.com/python-attrs/attrs" - - -class AdHocProject(ProjectToTest): - """A standalone program to run locally.""" - - def __init__(self, python_file, cur_dir=None, pip_args=None): - super().__init__() - self.python_file = Path(python_file) - if not self.python_file.exists(): - raise ValueError(f"Couldn't find {self.python_file} to run ad-hoc.") - self.cur_dir = Path(cur_dir or self.python_file.parent) - if not self.cur_dir.exists(): - raise ValueError(f"Couldn't find {self.cur_dir} to run in.") - self.pip_args = pip_args - self.slug = self.python_file.name - - def get_source(self, shell): - pass - - def prep_environment(self, env): - env.shell.run_command(f"{env.python} -m pip install {self.pip_args}") - - def run_no_coverage(self, env): - with change_dir(self.cur_dir): - env.shell.run_command(f"{env.python} {self.python_file}") - return env.shell.last_duration - - def run_with_coverage(self, env, pip_args, cov_options): - env.shell.run_command(f"{env.python} -m pip install {pip_args}") - with change_dir(self.cur_dir): - env.shell.run_command( - f"{env.python} -m coverage run {self.python_file}" - ) - return env.shell.last_duration - - -class SlipcoverBenchmark(AdHocProject): - """ - For running code from the Slipcover benchmarks. - - Clone https://github.com/plasma-umass/slipcover to /src/slipcover - - """ - def __init__(self, python_file): - super().__init__( - python_file=f"/src/slipcover/benchmarks/{python_file}", - cur_dir="/src/slipcover", - pip_args="six pyperf", - ) - -class PyVersion: - """A version of Python to use.""" - - # The command to run this Python - command: str - # Short word for messages, directories, etc - slug: str - # The tox environment to run this Python - toxenv: str - - -class Python(PyVersion): - """A version of CPython to use.""" - - def __init__(self, major, minor): - self.command = self.slug = f"python{major}.{minor}" - self.toxenv = f"py{major}{minor}" - - -class PyPy(PyVersion): - """A version of PyPy to use.""" - - def __init__(self, major, minor): - self.command = self.slug = f"pypy{major}.{minor}" - self.toxenv = f"pypy{major}{minor}" - -class AdHocPython(PyVersion): - """A custom build of Python to use.""" - def __init__(self, path, slug): - self.command = f"{path}/bin/python3" - self.slug = slug - self.toxenv = None - -@dataclasses.dataclass -class Coverage: - """A version of coverage.py to use, maybe None.""" - # Short word for messages, directories, etc - slug: str - # Arguments for "pip install ..." - pip_args: Optional[str] = None - # Tweaks to the .coveragerc file - options: Optional[str] = None - -class CoveragePR(Coverage): - """A version of coverage.py from a pull request.""" - def __init__(self, number, options=None): - super().__init__( - slug=f"#{number}", - pip_args=f"git+https://github.com/nedbat/coveragepy.git@refs/pull/{number}/merge", - options=options, - ) - -class CoverageCommit(Coverage): - """A version of coverage.py from a specific commit.""" - def __init__(self, sha, options=None): - super().__init__( - slug=sha, - pip_args=f"git+https://github.com/nedbat/coveragepy.git@{sha}", - options=options, - ) - -class CoverageSource(Coverage): - """The coverage.py in a working tree.""" - def __init__(self, directory, options=None): - super().__init__( - slug="source", - pip_args=directory, - options=options, - ) - - -@dataclasses.dataclass -class Env: - """An environment to run a test suite in.""" - - pyver: PyVersion - python: Path - shell: ShellSession - - -ResultData = Dict[Tuple[str, str, str], float] - -class Experiment: - """A particular time experiment to run.""" - - def __init__( - self, - py_versions: List[PyVersion], - cov_versions: List[Coverage], - projects: List[ProjectToTest], - ): - self.py_versions = py_versions - self.cov_versions = cov_versions - self.projects = projects - self.result_data: ResultData = {} - - def run(self, num_runs: int = 3) -> None: - results = [] - for proj in self.projects: - print(f"Testing with {proj.slug}") - with ShellSession(f"output_{proj.slug}.log") as shell: - proj.get_source(shell) - - for pyver in self.py_versions: - print(f"Making venv for {proj.slug} {pyver.slug}") - venv_dir = f"venv_{proj.slug}_{pyver.slug}" - shell.run_command(f"{pyver.command} -m venv {venv_dir}") - python = Path.cwd() / f"{venv_dir}/bin/python" - shell.run_command(f"{python} -V") - env = Env(pyver, python, shell) - - with change_dir(Path(proj.slug)): - print(f"Prepping for {proj.slug} {pyver.slug}") - proj.prep_environment(env) - for cov_ver in self.cov_versions: - durations = [] - for run_num in range(num_runs): - print( - f"Running tests, cov={cov_ver.slug}, {run_num+1} of {num_runs}" - ) - if cov_ver.pip_args is None: - dur = proj.run_no_coverage(env) - else: - dur = proj.run_with_coverage( - env, cov_ver.pip_args, cov_ver.options, - ) - print(f"Tests took {dur:.3f}s") - durations.append(dur) - med = statistics.median(durations) - result = ( - f"Median for {proj.slug}, {pyver.slug}, " - + f"cov={cov_ver.slug}: {med:.3f}s" - ) - print(f"## {result}") - results.append(result) - result_key = (proj.slug, pyver.slug, cov_ver.slug) - self.result_data[result_key] = med - - print("# Results") - for result in results: - print(result) - - def show_results( - self, - rows: List[str], - column: str, - ratios: Iterable[Tuple[str, str, str]] = (), - ) -> None: - dimensions = { - "cov": [cov_ver.slug for cov_ver in self.cov_versions], - "pyver": [pyver.slug for pyver in self.py_versions], - "proj": [proj.slug for proj in self.projects], - } - - table_axes = [dimensions[rowname] for rowname in rows] - data_order = [*rows, column] - remap = [data_order.index(datum) for datum in ["proj", "pyver", "cov"]] - - WIDTH = 20 - def as_table_row(vals): - return "| " + " | ".join(v.ljust(WIDTH) for v in vals) + " |" - - header = [] - header.extend(rows) - header.extend(dimensions[column]) - header.extend(slug for slug, _, _ in ratios) - - print() - print(as_table_row(header)) - dashes = [":---"] * len(rows) + ["---:"] * (len(header) - len(rows)) - print(as_table_row(dashes)) - for tup in itertools.product(*table_axes): - row = [] - row.extend(tup) - col_data = {} - for col in dimensions[column]: - key = (*tup, col) - key = tuple(key[i] for i in remap) - result_time = self.result_data[key] # type: ignore - row.append(f"{result_time:.3f} s") - col_data[col] = result_time - for _, num, denom in ratios: - ratio = col_data[num] / col_data[denom] - row.append(f"{ratio * 100:.2f}%") - print(as_table_row(row)) - - -PERF_DIR = Path("/tmp/covperf") - -def run_experiment( - py_versions: List[PyVersion], cov_versions: List[Coverage], projects: List[ProjectToTest], - rows: List[str], column: str, ratios: Iterable[Tuple[str, str, str]] = (), -): - slugs = [v.slug for v in py_versions + cov_versions + projects] - if len(set(slugs)) != len(slugs): - raise Exception(f"Slugs must be unique: {slugs}") - if any(" " in slug for slug in slugs): - raise Exception(f"No spaces in slugs please: {slugs}") - ratio_slugs = [rslug for ratio in ratios for rslug in ratio[1:]] - if any(rslug not in slugs for rslug in ratio_slugs): - raise Exception(f"Ratio slug doesn't match a slug: {ratio_slugs}, {slugs}") - - print(f"Removing and re-making {PERF_DIR}") - rmrf(PERF_DIR) - - with change_dir(PERF_DIR): - exp = Experiment(py_versions=py_versions, cov_versions=cov_versions, projects=projects) - exp.run(num_runs=int(sys.argv[1])) - exp.show_results(rows=rows, column=column, ratios=ratios) - - -if 1: - run_experiment( - py_versions=[ - #Python(3, 11), - AdHocPython("/usr/local/cpython/v3.10.5", "v3.10.5"), - AdHocPython("/usr/local/cpython/v3.11.0b3", "v3.11.0b3"), - AdHocPython("/usr/local/cpython/94231", "94231"), - ], - cov_versions=[ - Coverage("6.4.1", "coverage==6.4.1"), - ], - projects=[ - AdHocProject("/src/bugs/bug1339/bug1339.py"), - SlipcoverBenchmark("bm_sudoku.py"), - SlipcoverBenchmark("bm_spectral_norm.py"), - ], - rows=["cov", "proj"], - column="pyver", - ratios=[ - ("3.11b3 vs 3.10", "v3.11.0b3", "v3.10.5"), - ("94231 vs 3.10", "94231", "v3.10.5"), - ], - ) diff -Nru python-coverage-6.5.0+dfsg1/lab/genpy.py python-coverage-7.2.7+dfsg1/lab/genpy.py --- python-coverage-6.5.0+dfsg1/lab/genpy.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/lab/genpy.py 2023-05-29 19:46:30.000000000 +0000 @@ -231,7 +231,7 @@ source = PythonSpinner.generate_python(maker.make_body("def")) try: print("-"*80, "\n", source, sep="") - compile(source, "", "exec") + compile(source, "", "exec", dont_inherit=True) except Exception as ex: print(f"Oops: {ex}\n{source}") if len(source) > len(longest): diff -Nru python-coverage-6.5.0+dfsg1/lab/parser.py python-coverage-7.2.7+dfsg1/lab/parser.py --- python-coverage-6.5.0+dfsg1/lab/parser.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/lab/parser.py 2023-05-29 19:46:30.000000000 +0000 @@ -177,7 +177,7 @@ def disassemble(pyparser): """Disassemble code, for ad-hoc experimenting.""" - code = compile(pyparser.text, "", "exec") + code = compile(pyparser.text, "", "exec", dont_inherit=True) for code_obj in all_code_objects(code): if pyparser.text: srclines = pyparser.text.splitlines() diff -Nru python-coverage-6.5.0+dfsg1/lab/select_contexts.py python-coverage-7.2.7+dfsg1/lab/select_contexts.py --- python-coverage-6.5.0+dfsg1/lab/select_contexts.py 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/lab/select_contexts.py 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,66 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""\ +Select certain contexts from a coverage.py data file. +""" + +import argparse +import re +import sys + +import coverage + + +def main(argv): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--include", type=str, help="Regex for contexts to keep") + parser.add_argument("--exclude", type=str, help="Regex for contexts to discard") + args = parser.parse_args(argv) + + print("** Note: this is a proof-of-concept. Support is not promised. **") + print("Feedback is appreciated: https://github.com/nedbat/coveragepy/issues/668") + + cov_in = coverage.Coverage() + cov_in.load() + data_in = cov_in.get_data() + print(f"Contexts in {data_in.data_filename()}:") + for ctx in sorted(data_in.measured_contexts()): + print(f" {ctx}") + + if args.include is None and args.exclude is None: + print("Nothing to do, no output written.") + return + + out_file = "output.data" + file_names = data_in.measured_files() + print(f"{len(file_names)} measured files") + print(f"Writing to {out_file}") + cov_out = coverage.Coverage(data_file=out_file) + data_out = cov_out.get_data() + + for ctx in sorted(data_in.measured_contexts()): + if args.include is not None: + if not re.search(args.include, ctx): + print(f"Skipping context {ctx}, not included") + continue + if args.exclude is not None: + if re.search(args.exclude, ctx): + print(f"Skipping context {ctx}, excluded") + continue + print(f"Keeping context {ctx}") + data_in.set_query_context(ctx) + data_out.set_context(ctx) + if data_in.has_arcs(): + data_out.add_arcs({f: data_in.arcs(f) for f in file_names}) + else: + data_out.add_lines({f: data_in.lines(f) for f in file_names}) + + for fname in file_names: + data_out.touch_file(fname, data_in.file_tracer(fname)) + + cov_out.save() + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff -Nru python-coverage-6.5.0+dfsg1/lab/show_pyc.py python-coverage-7.2.7+dfsg1/lab/show_pyc.py --- python-coverage-6.5.0+dfsg1/lab/show_pyc.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/lab/show_pyc.py 2023-05-29 19:46:30.000000000 +0000 @@ -48,7 +48,7 @@ show_py_text(text, fname=fname) def show_py_text(text, fname=""): - code = compile(text, fname, "exec") + code = compile(text, fname, "exec", dont_inherit=True) show_code(code) CO_FLAGS = [ diff -Nru python-coverage-6.5.0+dfsg1/Makefile python-coverage-7.2.7+dfsg1/Makefile --- python-coverage-6.5.0+dfsg1/Makefile 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/Makefile 2023-05-29 19:46:30.000000000 +0000 @@ -11,17 +11,19 @@ clean_platform: @rm -f *.so */*.so + @rm -f *.pyd */*.pyd @rm -rf __pycache__ */__pycache__ */*/__pycache__ */*/*/__pycache__ */*/*/*/__pycache__ */*/*/*/*/__pycache__ @rm -f *.pyc */*.pyc */*/*.pyc */*/*/*.pyc */*/*/*/*.pyc */*/*/*/*/*.pyc @rm -f *.pyo */*.pyo */*/*.pyo */*/*/*.pyo */*/*/*/*.pyo */*/*/*/*/*.pyo + @rm -f *$$py.class */*$$py.class */*/*$$py.class */*/*/*$$py.class */*/*/*/*$$py.class */*/*/*/*/*$$py.class clean: clean_platform ## Remove artifacts of test execution, installation, etc. @echo "Cleaning..." @-pip uninstall -yq coverage - @rm -f *.pyd */*.pyd + @mkdir -p build # so the chmod won't fail if build doesn't exist + @chmod -R 777 build @rm -rf build coverage.egg-info dist htmlcov @rm -f *.bak */*.bak */*/*.bak */*/*/*.bak */*/*/*/*.bak */*/*/*/*/*.bak - @rm -f *$$py.class */*$$py.class */*/*$$py.class */*/*/*$$py.class */*/*/*/*$$py.class */*/*/*/*/*$$py.class @rm -f coverage/*,cover @rm -f MANIFEST @rm -f .coverage .coverage.* coverage.xml coverage.json .metacov* @@ -30,12 +32,13 @@ @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 .hypothesis .mypy_cache .pytest_cache + @rm -rf .cache .hypothesis .*_cache @rm -rf tests/actual @-make -C tests/gold/html clean sterile: clean ## Remove all non-controlled content, even if expensive. rm -rf .tox + rm -f cheats.txt help: ## Show this help. @# Adapted from https://www.thapaliya.com/en/writings/well-documented-makefiles/ @@ -83,7 +86,7 @@ .PHONY: upgrade -PIP_COMPILE = pip-compile --upgrade --allow-unsafe --generate-hashes +PIP_COMPILE = pip-compile --upgrade --allow-unsafe --resolver=backtracking upgrade: export CUSTOM_COMPILE_COMMAND=make upgrade upgrade: ## Update the *.pip files with the latest packages satisfying *.in files. pip install -q -r requirements/pip-tools.pip @@ -96,7 +99,16 @@ $(PIP_COMPILE) -o requirements/light-threads.pip requirements/light-threads.in $(PIP_COMPILE) -o doc/requirements.pip doc/requirements.in $(PIP_COMPILE) -o requirements/lint.pip doc/requirements.in requirements/dev.in + $(PIP_COMPILE) -o requirements/mypy.pip requirements/mypy.in +diff_upgrade: ## Summarize the last `make upgrade` + @# The sort flags sort by the package name first, then by the -/+, and + @# sort by version numbers, so we get a summary with lines like this: + @# -bashlex==0.16 + @# +bashlex==0.17 + @# -build==0.9.0 + @# +build==0.10.0 + @git diff -U0 | grep -v '^@' | grep == | sort -k1.2,1.99 -k1.1,1.1r -u -V ##@ Pre-builds for prepping the code @@ -122,7 +134,7 @@ _sample_cog_html: clean python -m pip install -e . - cd ~/cog/trunk; \ + cd ~/cog; \ rm -rf htmlcov; \ PYTEST_ADDOPTS= coverage run --branch --source=cogapp -m pytest -k CogTestsInMemory; \ coverage combine; \ @@ -130,12 +142,12 @@ sample_html: _sample_cog_html ## Generate sample HTML report. rm -f doc/sample_html/*.* - cp -r ~/cog/trunk/htmlcov/ doc/sample_html/ + cp -r ~/cog/htmlcov/ doc/sample_html/ rm doc/sample_html/.gitignore sample_html_beta: _sample_cog_html ## Generate sample HTML report for a beta release. rm -f doc/sample_html_beta/*.* - cp -r ~/cog/trunk/htmlcov/ doc/sample_html_beta/ + cp -r ~/cog/htmlcov/ doc/sample_html_beta/ rm doc/sample_html_beta/.gitignore @@ -146,6 +158,21 @@ REPO_OWNER = nedbat/coveragepy +edit_for_release: ## Edit sources to insert release facts. + python igor.py edit_for_release + +cheats: ## Create some useful snippets for releasing. + python igor.py cheats | tee cheats.txt + +relbranch: ## Create the branch for releasing. + git switch -c nedbat/release-$$(date +%Y%m%d) + +relcommit1: ## Commit the first release changes. + git commit -am "docs: prep for $$(python setup.py --version)" + +relcommit2: ## Commit the latest sample HTML report. + git commit -am "docs: sample HTML for $$(python setup.py --version)" + kit: ## Make the source distribution. python -m build @@ -181,6 +208,12 @@ git branch -f stable $$(python setup.py --version) git push origin stable +bump_version: ## Edit sources to bump the version after a release. + git switch -c nedbat/bump-version + python igor.py bump_version + git commit -a -m "build: bump version" + git push -u origin @ + ##@ Documentation @@ -235,8 +268,8 @@ $(RELNOTES_JSON): $(CHANGES_MD) $(DOCBIN)/python ci/parse_relnotes.py tmp/rst_rst/changes.md $(RELNOTES_JSON) -github_releases: $(RELNOTES_JSON) ## Update GitHub releases. - $(DOCBIN)/python ci/github_releases.py $(RELNOTES_JSON) $(REPO_OWNER) +github_releases: $(DOCBIN) ## Update GitHub releases. + $(DOCBIN)/python -m scriv github-release comment_on_fixes: $(RELNOTES_JSON) ## Add a comment to issues that were fixed. python ci/comment_on_fixes.py $(REPO_OWNER) diff -Nru python-coverage-6.5.0+dfsg1/MANIFEST.in python-coverage-7.2.7+dfsg1/MANIFEST.in --- python-coverage-6.5.0+dfsg1/MANIFEST.in 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/MANIFEST.in 2023-05-29 19:46:30.000000000 +0000 @@ -23,12 +23,14 @@ include setup.py include tox.ini include .editorconfig +include .git-blame-ignore-revs include .readthedocs.yml recursive-include ci * recursive-include lab * recursive-include .github * +recursive-include coverage *.pyi recursive-include coverage/fullcoverage *.py recursive-include coverage/ctracer *.c *.h diff -Nru python-coverage-6.5.0+dfsg1/metacov.ini python-coverage-7.2.7+dfsg1/metacov.ini --- python-coverage-6.5.0+dfsg1/metacov.ini 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/metacov.ini 2023-05-29 19:46:30.000000000 +0000 @@ -8,18 +8,19 @@ [run] branch = true -data_file = ${COVERAGE_METAFILE?} +data_file = ${COVERAGE_METAFILE-.metacov} parallel = true +relative_files = true source = ${COVERAGE_HOME-.}/coverage ${COVERAGE_HOME-.}/tests # $set_env.py: COVERAGE_DYNCTX - Set to 'test_function' for who-tests-what dynamic_context = ${COVERAGE_DYNCTX-none} -# $set_env.py: COVERAGE_CONTEXT - Set to a static context for this run +# $set_env.py: COVERAGE_CONTEXT - Static context for this run (or $ENV_VAR like $TOX_ENV_NAME) context = ${COVERAGE_CONTEXT-none} [report] -# We set a different pragmas so our code won't be confused with test code, and +# We set different pragmas so our code won't be confused with test code, and # we use different pragmas for different reasons that the lines won't be # measured. exclude_lines = @@ -58,6 +59,10 @@ raise AssertionError pragma: only failure + # Not-real code for type checking + if TYPE_CHECKING: + class .*\(Protocol\): + # OS error conditions that we can't (or don't care to) replicate. pragma: cant happen @@ -65,23 +70,15 @@ # longer tested. pragma: obscure - # Jython needs special care. - pragma: only jython - if env.JYTHON - - # IronPython isn't included in metacoverage. - pragma: only ironpython - if env.IRONPYTHON - partial_branches = pragma: part covered # A for-loop that always hits its break statement pragma: always breaks pragma: part started + # If we're asserting that any() is true, it didn't finish. + assert any\( if env.TESTING: if env.METACOV: - if .* env.JYTHON - if .* env.IRONPYTHON precision = 3 @@ -91,11 +88,9 @@ [paths] source = . - *\coverage\trunk */coverage/trunk - *\coveragepy - /io # GitHub Actions on Ubuntu uses /home/runner/work/coveragepy # GitHub Actions on Mac uses /Users/runner/work/coveragepy # GitHub Actions on Window uses D:\a\coveragepy\coveragepy + *\coveragepy */coveragepy diff -Nru python-coverage-6.5.0+dfsg1/NOTICE.txt python-coverage-7.2.7+dfsg1/NOTICE.txt --- python-coverage-6.5.0+dfsg1/NOTICE.txt 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/NOTICE.txt 2023-05-29 19:46:30.000000000 +0000 @@ -1,5 +1,5 @@ Copyright 2001 Gareth Rees. All rights reserved. -Copyright 2004-2022 Ned Batchelder. All rights reserved. +Copyright 2004-2023 Ned Batchelder. All rights reserved. Except where noted otherwise, this software is licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in diff -Nru python-coverage-6.5.0+dfsg1/PKG-INFO python-coverage-7.2.7+dfsg1/PKG-INFO --- python-coverage-6.5.0+dfsg1/PKG-INFO 2022-09-29 16:36:48.202740000 +0000 +++ python-coverage-7.2.7+dfsg1/PKG-INFO 2023-05-29 19:46:41.742986000 +0000 @@ -1,15 +1,16 @@ Metadata-Version: 2.1 Name: coverage -Version: 6.5.0 +Version: 7.2.7 Summary: Code coverage measurement for Python Home-page: https://github.com/nedbat/coveragepy -Author: Ned Batchelder and 161 others +Author: Ned Batchelder and 213 others Author-email: ned@nedbatchelder.com -License: Apache 2.0 -Project-URL: Documentation, https://coverage.readthedocs.io +License: Apache-2.0 +Project-URL: Documentation, https://coverage.readthedocs.io/en/7.2.7 Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=pypi Project-URL: Issues, https://github.com/nedbat/coveragepy/issues -Project-URL: Twitter, https://twitter.com/coveragepy +Project-URL: Mastodon, https://hachyderm.io/@coveragepy +Project-URL: Mastodon (nedbat), https://hachyderm.io/@nedbat Keywords: code coverage testing Classifier: Environment :: Console Classifier: Intended Audience :: Developers @@ -22,6 +23,7 @@ Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Quality Assurance @@ -51,8 +53,8 @@ | |test-status| |quality-status| |docs| |metacov| | |kit| |downloads| |format| |repos| | |stars| |forks| |contributors| -| |tidelift| |core-infrastructure| |open-ssf| -| |sponsor| |twitter-coveragepy| |twitter-nedbat| +| |core-infrastructure| |open-ssf| |snyk| +| |tidelift| |sponsor| |mastodon-coveragepy| |mastodon-nedbat| Coverage.py measures code coverage, typically during test execution. It uses the code analysis tools and tracing hooks provided in the Python standard @@ -62,17 +64,23 @@ .. PYVERSIONS -* CPython 3.7 through 3.11.0 rc2. -* PyPy3 7.3.8. +* CPython 3.7 through 3.12.0b1 +* PyPy3 7.3.11. Documentation is on `Read the Docs`_. Code repository and issue tracker are on `GitHub`_. -.. _Read the Docs: https://coverage.readthedocs.io/ +.. _Read the Docs: https://coverage.readthedocs.io/en/7.2.7/ .. _GitHub: https://github.com/nedbat/coveragepy +**New in 7.x:** +improved data combining; +``[run] exclude_also`` setting; +``report --format=``; +type annotations. -**New in 6.x:** dropped support for Python 2.7, 3.5, and 3.6; +**New in 6.x:** +dropped support for Python 2.7, 3.5, and 3.6; write data on SIGTERM; added support for 3.10 match/case statements. @@ -99,9 +107,10 @@ Getting Started --------------- -See the `Quick Start section`_ of the docs. +Looking to run ``coverage`` on your test suite? See the `Quick Start section`_ +of the docs. -.. _Quick Start section: https://coverage.readthedocs.io/#quick-start +.. _Quick Start section: https://coverage.readthedocs.io/en/7.2.7/#quick-start Change history @@ -109,7 +118,7 @@ The complete history of changes is on the `change history page`_. -.. _change history page: https://coverage.readthedocs.io/en/latest/changes.html +.. _change history page: https://coverage.readthedocs.io/en/7.2.7/changes.html Code of Conduct @@ -125,9 +134,10 @@ Contributing ------------ -See the `Contributing section`_ of the docs. +Found a bug? Want to help improve the code or documentation? See the +`Contributing section`_ of the docs. -.. _Contributing section: https://coverage.readthedocs.io/en/latest/contributing.html +.. _Contributing section: https://coverage.readthedocs.io/en/7.2.7/contributing.html Security @@ -155,7 +165,7 @@ :target: https://github.com/nedbat/coveragepy/actions/workflows/quality.yml :alt: Quality check status .. |docs| image:: https://readthedocs.org/projects/coverage/badge/?version=latest&style=flat - :target: https://coverage.readthedocs.io/ + :target: https://coverage.readthedocs.io/en/7.2.7/ :alt: Documentation .. |kit| image:: https://badge.fury.io/py/coverage.svg :target: https://pypi.org/project/coverage/ @@ -193,12 +203,12 @@ .. |contributors| image:: https://img.shields.io/github/contributors/nedbat/coveragepy.svg?logo=github :target: https://github.com/nedbat/coveragepy/graphs/contributors :alt: Contributors -.. |twitter-coveragepy| image:: https://img.shields.io/twitter/follow/coveragepy.svg?label=coveragepy&style=flat&logo=twitter&logoColor=4FADFF - :target: https://twitter.com/coveragepy - :alt: coverage.py on Twitter -.. |twitter-nedbat| image:: https://img.shields.io/twitter/follow/nedbat.svg?label=nedbat&style=flat&logo=twitter&logoColor=4FADFF - :target: https://twitter.com/nedbat - :alt: nedbat on Twitter +.. |mastodon-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&link=https%3A%2F%2Fhachyderm.io%2F%40nedbat&url=https%3A%2F%2Fhachyderm.io%2Fusers%2Fnedbat%2Ffollowers.json&query=totalItems&label=@nedbat + :target: https://hachyderm.io/@nedbat + :alt: nedbat on Mastodon +.. |mastodon-coveragepy| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&link=https%3A%2F%2Fhachyderm.io%2F%40coveragepy&url=https%3A%2F%2Fhachyderm.io%2Fusers%2Fcoveragepy%2Ffollowers.json&query=totalItems&label=@coveragepy + :target: https://hachyderm.io/@coveragepy + :alt: coveragepy on Mastodon .. |sponsor| image:: https://img.shields.io/badge/%E2%9D%A4-Sponsor%20me-brightgreen?style=flat&logo=GitHub :target: https://github.com/sponsors/nedbat :alt: Sponsor me on GitHub @@ -208,3 +218,6 @@ .. |open-ssf| image:: https://api.securityscorecards.dev/projects/github.com/nedbat/coveragepy/badge :target: https://deps.dev/pypi/coverage :alt: OpenSSF Scorecard +.. |snyk| image:: https://snyk.io/advisor/python/coverage/badge.svg + :target: https://snyk.io/advisor/python/coverage + :alt: Snyk package health diff -Nru python-coverage-6.5.0+dfsg1/pylintrc python-coverage-7.2.7+dfsg1/pylintrc --- python-coverage-6.5.0+dfsg1/pylintrc 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/pylintrc 2023-05-29 19:46:30.000000000 +0000 @@ -62,6 +62,7 @@ broad-except, no-else-return, subprocess-run-check, + use-dict-literal, # Messages that may be silly: no-member, using-constant-test, @@ -75,6 +76,7 @@ self-assigning-variable, consider-using-with, missing-timeout, + use-implicit-booleaness-not-comparison, # Formatting stuff superfluous-parens, # Messages that are noisy for now, eventually maybe we'll turn them on: diff -Nru python-coverage-6.5.0+dfsg1/pyproject.toml python-coverage-7.2.7+dfsg1/pyproject.toml --- python-coverage-6.5.0+dfsg1/pyproject.toml 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/pyproject.toml 2023-05-29 19:46:30.000000000 +0000 @@ -2,5 +2,65 @@ # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt [build-system] -requires = ['setuptools', 'wheel'] +requires = ['setuptools'] build-backend = 'setuptools.build_meta' + +[tool.mypy] +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +follow_imports = "silent" +ignore_missing_imports = true +no_implicit_optional = true +show_error_codes = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true + +exclude = """(?x)( + ^coverage/fullcoverage/encodings\\.py$ # can't import things into it. + | ^tests/balance_xdist_plugin\\.py$ # not part of our test suite. + )""" + +[tool.pytest.ini_options] +addopts = "-q -n auto -p no:legacypath --strict-markers --no-flaky-report -rfEX --failed-first" +python_classes = "*Test" +markers = [ + "expensive: too slow to run during \"make smoke\"", +] + +# How come these warnings are suppressed successfully here, but not in conftest.py?? +filterwarnings = [ + "ignore:the imp module is deprecated in favour of importlib:DeprecationWarning", + "ignore:distutils Version classes are deprecated:DeprecationWarning", + "ignore:The distutils package is deprecated and slated for removal in Python 3.12:DeprecationWarning", +] + +# xfail tests that pass should fail the test suite +xfail_strict = true + +balanced_clumps = [ + # Because of expensive session-scoped fixture: + "VirtualenvTest", + # Because of shared-file manipulations (~/tests/actual/testing): + "CompareTest", + # No idea why this one fails if run on separate workers: + "GetZipBytesTest", +] + +[tool.ruff] +line-length = 100 + +[tool.scriv] +# Changelog management: https://pypi.org/project/scriv/ +format = "rst" +output_file = "CHANGES.rst" +insert_marker = "scriv-start-here" +end_marker = "scriv-end-here" +ghrel_template = "file: ci/ghrel_template.md.j2" diff -Nru python-coverage-6.5.0+dfsg1/README.rst python-coverage-7.2.7+dfsg1/README.rst --- python-coverage-6.5.0+dfsg1/README.rst 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/README.rst 2023-05-29 19:46:30.000000000 +0000 @@ -17,8 +17,8 @@ | |test-status| |quality-status| |docs| |metacov| | |kit| |downloads| |format| |repos| | |stars| |forks| |contributors| -| |tidelift| |core-infrastructure| |open-ssf| -| |sponsor| |twitter-coveragepy| |twitter-nedbat| +| |core-infrastructure| |open-ssf| |snyk| +| |tidelift| |sponsor| |mastodon-coveragepy| |mastodon-nedbat| Coverage.py measures code coverage, typically during test execution. It uses the code analysis tools and tracing hooks provided in the Python standard @@ -28,8 +28,8 @@ .. PYVERSIONS -* CPython 3.7 through 3.11.0 rc2. -* PyPy3 7.3.8. +* CPython 3.7 through 3.12.0b1 +* PyPy3 7.3.11. Documentation is on `Read the Docs`_. Code repository and issue tracker are on `GitHub`_. @@ -37,8 +37,14 @@ .. _Read the Docs: https://coverage.readthedocs.io/ .. _GitHub: https://github.com/nedbat/coveragepy +**New in 7.x:** +improved data combining; +``[run] exclude_also`` setting; +``report --format=``; +type annotations. -**New in 6.x:** dropped support for Python 2.7, 3.5, and 3.6; +**New in 6.x:** +dropped support for Python 2.7, 3.5, and 3.6; write data on SIGTERM; added support for 3.10 match/case statements. @@ -65,7 +71,8 @@ Getting Started --------------- -See the `Quick Start section`_ of the docs. +Looking to run ``coverage`` on your test suite? See the `Quick Start section`_ +of the docs. .. _Quick Start section: https://coverage.readthedocs.io/#quick-start @@ -91,7 +98,8 @@ Contributing ------------ -See the `Contributing section`_ of the docs. +Found a bug? Want to help improve the code or documentation? See the +`Contributing section`_ of the docs. .. _Contributing section: https://coverage.readthedocs.io/en/latest/contributing.html @@ -159,12 +167,12 @@ .. |contributors| image:: https://img.shields.io/github/contributors/nedbat/coveragepy.svg?logo=github :target: https://github.com/nedbat/coveragepy/graphs/contributors :alt: Contributors -.. |twitter-coveragepy| image:: https://img.shields.io/twitter/follow/coveragepy.svg?label=coveragepy&style=flat&logo=twitter&logoColor=4FADFF - :target: https://twitter.com/coveragepy - :alt: coverage.py on Twitter -.. |twitter-nedbat| image:: https://img.shields.io/twitter/follow/nedbat.svg?label=nedbat&style=flat&logo=twitter&logoColor=4FADFF - :target: https://twitter.com/nedbat - :alt: nedbat on Twitter +.. |mastodon-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&link=https%3A%2F%2Fhachyderm.io%2F%40nedbat&url=https%3A%2F%2Fhachyderm.io%2Fusers%2Fnedbat%2Ffollowers.json&query=totalItems&label=@nedbat + :target: https://hachyderm.io/@nedbat + :alt: nedbat on Mastodon +.. |mastodon-coveragepy| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&link=https%3A%2F%2Fhachyderm.io%2F%40coveragepy&url=https%3A%2F%2Fhachyderm.io%2Fusers%2Fcoveragepy%2Ffollowers.json&query=totalItems&label=@coveragepy + :target: https://hachyderm.io/@coveragepy + :alt: coveragepy on Mastodon .. |sponsor| image:: https://img.shields.io/badge/%E2%9D%A4-Sponsor%20me-brightgreen?style=flat&logo=GitHub :target: https://github.com/sponsors/nedbat :alt: Sponsor me on GitHub @@ -174,3 +182,6 @@ .. |open-ssf| image:: https://api.securityscorecards.dev/projects/github.com/nedbat/coveragepy/badge :target: https://deps.dev/pypi/coverage :alt: OpenSSF Scorecard +.. |snyk| image:: https://snyk.io/advisor/python/coverage/badge.svg + :target: https://snyk.io/advisor/python/coverage + :alt: Snyk package health diff -Nru python-coverage-6.5.0+dfsg1/.readthedocs.yml python-coverage-7.2.7+dfsg1/.readthedocs.yml --- python-coverage-6.5.0+dfsg1/.readthedocs.yml 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/.readthedocs.yml 2023-05-29 19:46:30.000000000 +0000 @@ -17,6 +17,7 @@ - pdf python: + # PYVERSIONS version: 3.7 install: - requirements: doc/requirements.pip diff -Nru python-coverage-6.5.0+dfsg1/requirements/dev.in python-coverage-7.2.7+dfsg1/requirements/dev.in --- python-coverage-6.5.0+dfsg1/requirements/dev.in 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/requirements/dev.in 2023-05-29 19:46:30.000000000 +0000 @@ -5,17 +5,17 @@ # "make upgrade" turns this into requirements/dev.pip. -c pins.pip --r pip.pip +-r pip.in # PyPI requirements for running tests. -tox --r pytest.pip +-r tox.in +-r pytest.in # for linting. +check-manifest cogapp greenlet pylint -check-manifest readme_renderer # for kitting. diff -Nru python-coverage-6.5.0+dfsg1/requirements/dev.pip python-coverage-7.2.7+dfsg1/requirements/dev.pip --- python-coverage-6.5.0+dfsg1/requirements/dev.pip 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/requirements/dev.pip 2023-05-29 19:46:30.000000000 +0000 @@ -1,565 +1,207 @@ # -# This file is autogenerated by pip-compile with python 3.7 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: # # make upgrade # -astroid==2.12.10 \ - --hash=sha256:81f870105d892e73bf535da77a8261aa5bde838fa4ed12bb2f435291a098c581 \ - --hash=sha256:997e0c735df60d4a4caff27080a3afc51f9bdd693d3572a4a0b7090b645c36c5 +astroid==2.15.5 # via pylint -atomicwrites==1.4.1 \ - --hash=sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11 - # via -r requirements/pytest.pip -attrs==22.1.0 \ - --hash=sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6 \ - --hash=sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c - # via - # -r requirements/pytest.pip - # hypothesis - # pytest -backports-functools-lru-cache==1.6.4 \ - --hash=sha256:d5ed2169378b67d3c545e5600d363a923b09c456dab1593914935a68ad478271 \ - --hash=sha256:dbead04b9daa817909ec64e8d2855fb78feafe0b901d4568758e3a60559d8978 - # via - # -r requirements/pytest.pip - # pycontracts -bleach==5.0.1 \ - --hash=sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a \ - --hash=sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c +attrs==23.1.0 + # via hypothesis +bleach==6.0.0 # via readme-renderer -build==0.8.0 \ - --hash=sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437 \ - --hash=sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0 +build==0.10.0 # via check-manifest -certifi==2022.5.18.1 \ - --hash=sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7 \ - --hash=sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a - # via - # -c requirements/pins.pip - # requests -charset-normalizer==2.1.1 \ - --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \ - --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f +cachetools==5.3.1 + # via tox +certifi==2023.5.7 # via requests -check-manifest==0.48 \ - --hash=sha256:3b575f1dade7beb3078ef4bf33a94519834457c7281dbc726b15c5466b55c657 \ - --hash=sha256:b1923685f98c1c2468601a1a7bed655db549a25d43c583caded3860ad8308f8c +chardet==5.1.0 + # via tox +charset-normalizer==3.1.0 + # via requests +check-manifest==0.49 # via -r requirements/dev.in -cogapp==3.3.0 \ - --hash=sha256:1be95183f70282422d594fa42426be6923070a4bd8335621f6347f3aeee81db0 \ - --hash=sha256:8b5b5f6063d8ee231961c05da010cb27c30876b2279e23ad0eae5f8f09460d50 +cogapp==3.3.0 # via -r requirements/dev.in -colorama==0.4.5 \ - --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \ - --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4 - # via -r requirements/pytest.pip -commonmark==0.9.1 \ - --hash=sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60 \ - --hash=sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9 - # via rich -decorator==5.1.1 \ - --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \ - --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186 - # via - # -r requirements/pytest.pip - # pycontracts -dill==0.3.5.1 \ - --hash=sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302 \ - --hash=sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86 - # via pylint -distlib==0.3.6 \ - --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \ - --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e +colorama==0.4.6 # via - # -r requirements/pip.pip - # virtualenv -docutils==0.19 \ - --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \ - --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc + # -r requirements/pytest.in + # -r requirements/tox.in + # tox +dill==0.3.6 + # via pylint +distlib==0.3.6 + # via virtualenv +docutils==0.20.1 # via readme-renderer -exceptiongroup==1.0.0rc9 \ - --hash=sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337 \ - --hash=sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96 +exceptiongroup==1.1.1 # via - # -r requirements/pytest.pip # hypothesis -execnet==1.9.0 \ - --hash=sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5 \ - --hash=sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142 - # via - # -r requirements/pytest.pip - # pytest-xdist -filelock==3.8.0 \ - --hash=sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc \ - --hash=sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4 + # pytest +execnet==1.9.0 + # via pytest-xdist +filelock==3.12.0 # via - # -r requirements/pip.pip # tox # virtualenv -flaky==3.7.0 \ - --hash=sha256:3ad100780721a1911f57a165809b7ea265a7863305acb66708220820caf8aa0d \ - --hash=sha256:d6eda73cab5ae7364504b7c44670f70abed9e75f77dd116352f662817592ec9c - # via -r requirements/pytest.pip -future==0.18.2 \ - --hash=sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d - # via - # -r requirements/pytest.pip - # pycontracts -greenlet==1.1.3 \ - --hash=sha256:0118817c9341ef2b0f75f5af79ac377e4da6ff637e5ee4ac91802c0e379dadb4 \ - --hash=sha256:048d2bed76c2aa6de7af500ae0ea51dd2267aec0e0f2a436981159053d0bc7cc \ - --hash=sha256:07c58e169bbe1e87b8bbf15a5c1b779a7616df9fd3e61cadc9d691740015b4f8 \ - --hash=sha256:095a980288fe05adf3d002fbb180c99bdcf0f930e220aa66fcd56e7914a38202 \ - --hash=sha256:0b181e9aa6cb2f5ec0cacc8cee6e5a3093416c841ba32c185c30c160487f0380 \ - --hash=sha256:1626185d938d7381631e48e6f7713e8d4b964be246073e1a1d15c2f061ac9f08 \ - --hash=sha256:184416e481295832350a4bf731ba619a92f5689bf5d0fa4341e98b98b1265bd7 \ - --hash=sha256:1dd51d2650e70c6c4af37f454737bf4a11e568945b27f74b471e8e2a9fd21268 \ - --hash=sha256:1ec2779774d8e42ed0440cf8bc55540175187e8e934f2be25199bf4ed948cd9e \ - --hash=sha256:2cf45e339cabea16c07586306a31cfcc5a3b5e1626d365714d283732afed6809 \ - --hash=sha256:2fb0aa7f6996879551fd67461d5d3ab0c3c0245da98be90c89fcb7a18d437403 \ - --hash=sha256:44b4817c34c9272c65550b788913620f1fdc80362b209bc9d7dd2f40d8793080 \ - --hash=sha256:466ce0928e33421ee84ae04c4ac6f253a3a3e6b8d600a79bd43fd4403e0a7a76 \ - --hash=sha256:4f166b4aca8d7d489e82d74627a7069ab34211ef5ebb57c300ec4b9337b60fc0 \ - --hash=sha256:510c3b15587afce9800198b4b142202b323bf4b4b5f9d6c79cb9a35e5e3c30d2 \ - --hash=sha256:5b756e6730ea59b2745072e28ad27f4c837084688e6a6b3633c8b1e509e6ae0e \ - --hash=sha256:5fbe1ab72b998ca77ceabbae63a9b2e2dc2d963f4299b9b278252ddba142d3f1 \ - --hash=sha256:6200a11f003ec26815f7e3d2ded01b43a3810be3528dd760d2f1fa777490c3cd \ - --hash=sha256:65ad1a7a463a2a6f863661329a944a5802c7129f7ad33583dcc11069c17e622c \ - --hash=sha256:694ffa7144fa5cc526c8f4512665003a39fa09ef00d19bbca5c8d3406db72fbe \ - --hash=sha256:6f5d4b2280ceea76c55c893827961ed0a6eadd5a584a7c4e6e6dd7bc10dfdd96 \ - --hash=sha256:7532a46505470be30cbf1dbadb20379fb481244f1ca54207d7df3bf0bbab6a20 \ - --hash=sha256:76a53bfa10b367ee734b95988bd82a9a5f0038a25030f9f23bbbc005010ca600 \ - --hash=sha256:77e41db75f9958f2083e03e9dd39da12247b3430c92267df3af77c83d8ff9eed \ - --hash=sha256:7a43bbfa9b6cfdfaeefbd91038dde65ea2c421dc387ed171613df340650874f2 \ - --hash=sha256:7b41d19c0cfe5c259fe6c539fd75051cd39a5d33d05482f885faf43f7f5e7d26 \ - --hash=sha256:7c5227963409551ae4a6938beb70d56bf1918c554a287d3da6853526212fbe0a \ - --hash=sha256:870a48007872d12e95a996fca3c03a64290d3ea2e61076aa35d3b253cf34cd32 \ - --hash=sha256:88b04e12c9b041a1e0bcb886fec709c488192638a9a7a3677513ac6ba81d8e79 \ - --hash=sha256:8c287ae7ac921dfde88b1c125bd9590b7ec3c900c2d3db5197f1286e144e712b \ - --hash=sha256:903fa5716b8fbb21019268b44f73f3748c41d1a30d71b4a49c84b642c2fed5fa \ - --hash=sha256:9537e4baf0db67f382eb29255a03154fcd4984638303ff9baaa738b10371fa57 \ - --hash=sha256:9951dcbd37850da32b2cb6e391f621c1ee456191c6ae5528af4a34afe357c30e \ - --hash=sha256:9b2f7d0408ddeb8ea1fd43d3db79a8cefaccadd2a812f021333b338ed6b10aba \ - --hash=sha256:9c88e134d51d5e82315a7c32b914a58751b7353eb5268dbd02eabf020b4c4700 \ - --hash=sha256:9fae214f6c43cd47f7bef98c56919b9222481e833be2915f6857a1e9e8a15318 \ - --hash=sha256:a3a669f11289a8995d24fbfc0e63f8289dd03c9aaa0cc8f1eab31d18ca61a382 \ - --hash=sha256:aa741c1a8a8cc25eb3a3a01a62bdb5095a773d8c6a86470bde7f607a447e7905 \ - --hash=sha256:b0877a9a2129a2c56a2eae2da016743db7d9d6a05d5e1c198f1b7808c602a30e \ - --hash=sha256:bcb6c6dd1d6be6d38d6db283747d07fda089ff8c559a835236560a4410340455 \ - --hash=sha256:caff52cb5cd7626872d9696aee5b794abe172804beb7db52eed1fd5824b63910 \ - --hash=sha256:cbc1eb55342cbac8f7ec159088d54e2cfdd5ddf61c87b8bbe682d113789331b2 \ - --hash=sha256:cd16a89efe3a003029c87ff19e9fba635864e064da646bc749fc1908a4af18f3 \ - --hash=sha256:ce5b64dfe8d0cca407d88b0ee619d80d4215a2612c1af8c98a92180e7109f4b5 \ - --hash=sha256:d58a5a71c4c37354f9e0c24c9c8321f0185f6945ef027460b809f4bb474bfe41 \ - --hash=sha256:db41f3845eb579b544c962864cce2c2a0257fe30f0f1e18e51b1e8cbb4e0ac6d \ - --hash=sha256:db5b25265010a1b3dca6a174a443a0ed4c4ab12d5e2883a11c97d6e6d59b12f9 \ - --hash=sha256:dd0404d154084a371e6d2bafc787201612a1359c2dee688ae334f9118aa0bf47 \ - --hash=sha256:de431765bd5fe62119e0bc6bc6e7b17ac53017ae1782acf88fcf6b7eae475a49 \ - --hash=sha256:df02fdec0c533301497acb0bc0f27f479a3a63dcdc3a099ae33a902857f07477 \ - --hash=sha256:e8533f5111704d75de3139bf0b8136d3a6c1642c55c067866fa0a51c2155ee33 \ - --hash=sha256:f2f908239b7098799b8845e5936c2ccb91d8c2323be02e82f8dcb4a80dcf4a25 \ - --hash=sha256:f8bfd36f368efe0ab2a6aa3db7f14598aac454b06849fb633b762ddbede1db90 \ - --hash=sha256:ffe73f9e7aea404722058405ff24041e59d31ca23d1da0895af48050a07b6932 - # via -r requirements/dev.in -hypothesis==6.54.6 \ - --hash=sha256:2d5e2d5ccd0efce4e0968a6164f4e4853f808e33f4d91490c975c98beec0c7c3 \ - --hash=sha256:e44833325f9a55f795596ceefd7ede7d626cfe45836025d2647cccaff7070e10 - # via -r requirements/pytest.pip -idna==3.4 \ - --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ - --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 +flaky==3.7.0 + # via -r requirements/pytest.in +greenlet==2.0.2 + # via -r requirements/dev.in +hypothesis==6.75.6 + # via -r requirements/pytest.in +idna==3.4 # via requests -importlib-metadata==4.12.0 \ - --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \ - --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23 +importlib-metadata==6.6.0 # via - # -r requirements/pip.pip - # -r requirements/pytest.pip + # attrs # build # keyring - # pep517 # pluggy # pytest # tox # twine # virtualenv -iniconfig==1.1.1 \ - --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ - --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 - # via - # -r requirements/pytest.pip - # pytest -isort==5.10.1 \ - --hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \ - --hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951 +importlib-resources==5.12.0 + # via keyring +iniconfig==2.0.0 + # via pytest +isort==5.11.5 # via pylint -jaraco-classes==3.2.3 \ - --hash=sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158 \ - --hash=sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a +jaraco-classes==3.2.3 # via keyring -jedi==0.18.1 \ - --hash=sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d \ - --hash=sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab +jedi==0.18.2 # via pudb -keyring==23.9.3 \ - --hash=sha256:69732a15cb1433bdfbc3b980a8a36a04878a6cfd7cb99f497b573f31618001c0 \ - --hash=sha256:69b01dd83c42f590250fe7a1f503fc229b14de83857314b1933a3ddbf595c4a5 +keyring==23.13.1 # via twine -lazy-object-proxy==1.7.1 \ - --hash=sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7 \ - --hash=sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a \ - --hash=sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c \ - --hash=sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc \ - --hash=sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f \ - --hash=sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09 \ - --hash=sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442 \ - --hash=sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e \ - --hash=sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029 \ - --hash=sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61 \ - --hash=sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb \ - --hash=sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0 \ - --hash=sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35 \ - --hash=sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42 \ - --hash=sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1 \ - --hash=sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad \ - --hash=sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443 \ - --hash=sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd \ - --hash=sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9 \ - --hash=sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148 \ - --hash=sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38 \ - --hash=sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55 \ - --hash=sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36 \ - --hash=sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a \ - --hash=sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b \ - --hash=sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44 \ - --hash=sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6 \ - --hash=sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69 \ - --hash=sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4 \ - --hash=sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84 \ - --hash=sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de \ - --hash=sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28 \ - --hash=sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c \ - --hash=sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1 \ - --hash=sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8 \ - --hash=sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b \ - --hash=sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb +lazy-object-proxy==1.9.0 # via astroid -libsass==0.21.0 \ - --hash=sha256:06c8776417fe930714bdc930a3d7e795ae3d72be6ac883ff72a1b8f7c49e5ffb \ - --hash=sha256:12f39712de38689a8b785b7db41d3ba2ea1d46f9379d81ea4595802d91fa6529 \ - --hash=sha256:1e25dd9047a9392d3c59a0b869e0404f2b325a03871ee45285ee33b3664f5613 \ - --hash=sha256:659ae41af8708681fa3ec73f47b9735a6725e71c3b66ff570bfce78952f2314e \ - --hash=sha256:6b984510ed94993708c0d697b4fef2d118929bbfffc3b90037be0f5ccadf55e7 \ - --hash=sha256:a005f298f64624f313a3ac618ab03f844c71d84ae4f4a4aec4b68d2a4ffe75eb \ - --hash=sha256:abc29357ee540849faf1383e1746d40d69ed5cb6d4c346df276b258f5aa8977a \ - --hash=sha256:c9ec490609752c1d81ff6290da33485aa7cb6d7365ac665b74464c1b7d97f7da \ - --hash=sha256:d5ba529d9ce668be9380563279f3ffe988f27bc5b299c5a28453df2e0b0fbaf2 \ - --hash=sha256:e2b1a7d093f2e76dc694c17c0c285e846d0b0deb0e8b21dc852ba1a3a4e2f1d6 +libsass==0.22.0 # via -r requirements/dev.in -mccabe==0.7.0 \ - --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \ - --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e +markdown-it-py==2.2.0 + # via rich +mccabe==0.7.0 # via pylint -more-itertools==8.14.0 \ - --hash=sha256:1bc4f91ee5b1b31ac7ceacc17c09befe6a40a503907baf9c839c229b5095cfd2 \ - --hash=sha256:c09443cd3d5438b8dafccd867a6bc1cb0894389e90cb53d227456b0b0bccb750 +mdurl==0.1.2 + # via markdown-it-py +more-itertools==9.1.0 # via jaraco-classes -packaging==21.3 \ - --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ - --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 +packaging==23.1 # via - # -r requirements/pytest.pip # build # pudb + # pyproject-api # pytest # tox -parso==0.8.3 \ - --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ - --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 +parso==0.8.3 # via jedi -pep517==0.13.0 \ - --hash=sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b \ - --hash=sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59 - # via build -pkginfo==1.8.3 \ - --hash=sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594 \ - --hash=sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c +pkginfo==1.9.6 # via twine -platformdirs==2.5.2 \ - --hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \ - --hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19 +platformdirs==3.5.1 # via - # -r requirements/pip.pip # pylint + # tox # virtualenv -pluggy==1.0.0 \ - --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \ - --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 +pluggy==1.0.0 # via - # -r requirements/pytest.pip # pytest # tox -pudb==2022.1.2 \ - --hash=sha256:6b83ab805bddb53710109690a2237e98bf83c0b3a00033c517cdf5f6a8fa470d +pudb==2022.1.3 # via -r requirements/dev.in -py==1.11.0 \ - --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ - --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 - # via - # -r requirements/pytest.pip - # pytest - # pytest-forked - # tox -pycontracts @ https://github.com/slorg1/contracts/archive/c5a6da27d4dc9985f68e574d20d86000880919c3.zip \ - --hash=sha256:2b889cbfb03b43dc811b5879248ac5c7e209ece78f03be9633de76a6b21a5a89 - # via -r requirements/pytest.pip -pygments==2.13.0 \ - --hash=sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1 \ - --hash=sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42 +pygments==2.15.1 # via # pudb # readme-renderer # rich -pylint==2.15.3 \ - --hash=sha256:5fdfd44af182866999e6123139d265334267339f29961f00c89783155eacc60b \ - --hash=sha256:7f6aad1d8d50807f7bc64f89ac75256a9baf8e6ed491cc9bc65592bc3f462cf1 +pylint==2.17.4 # via -r requirements/dev.in -pyparsing==3.0.9 \ - --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \ - --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc - # via - # -r requirements/pytest.pip - # packaging - # pycontracts -pytest==7.1.3 \ - --hash=sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7 \ - --hash=sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39 - # via - # -r requirements/pytest.pip - # pytest-forked - # pytest-xdist -pytest-forked==1.4.0 \ - --hash=sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e \ - --hash=sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8 +pyproject-api==1.5.1 + # via tox +pyproject-hooks==1.0.0 + # via build +pytest==7.3.1 # via - # -r requirements/pytest.pip + # -r requirements/pytest.in # pytest-xdist -pytest-xdist==2.5.0 \ - --hash=sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf \ - --hash=sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65 - # via -r requirements/pytest.pip -qualname==0.1.0 \ - --hash=sha256:277cf6aa4b2ad36beed1153cfa7bf521b210d54fbecb3d8eea0c5679cecc9ed8 - # via - # -r requirements/pytest.pip - # pycontracts -readme-renderer==37.2 \ - --hash=sha256:d3f06a69e8c40fca9ab3174eca48f96d9771eddb43517b17d96583418427b106 \ - --hash=sha256:e8ad25293c98f781dbc2c5a36a309929390009f902f99e1798c761aaf04a7923 +pytest-xdist==3.3.1 + # via -r requirements/pytest.in +readme-renderer==37.3 # via # -r requirements/dev.in # twine -requests==2.28.1 \ - --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \ - --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349 +requests==2.31.0 # via # -r requirements/dev.in # requests-toolbelt # twine -requests-toolbelt==0.9.1 \ - --hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f \ - --hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0 +requests-toolbelt==1.0.0 # via twine -rfc3986==2.0.0 \ - --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ - --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c +rfc3986==2.0.0 # via twine -rich==12.5.1 \ - --hash=sha256:2eb4e6894cde1e017976d2975ac210ef515d7548bc595ba20e195fb9628acdeb \ - --hash=sha256:63a5c5ce3673d3d5fbbf23cd87e11ab84b6b451436f1b7f19ec54b6bc36ed7ca +rich==13.3.5 # via twine -six==1.16.0 \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 - # via - # -r requirements/pytest.pip - # bleach - # libsass - # pycontracts - # tox -sortedcontainers==2.4.0 \ - --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ - --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 - # via - # -r requirements/pytest.pip - # hypothesis -tomli==2.0.1 \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f +six==1.16.0 + # via bleach +sortedcontainers==2.4.0 + # via hypothesis +tomli==2.0.1 # via - # -r requirements/pytest.pip # build # check-manifest - # pep517 # pylint + # pyproject-api + # pyproject-hooks # pytest # tox -tomlkit==0.11.4 \ - --hash=sha256:25d4e2e446c453be6360c67ddfb88838cfc42026322770ba13d1fbd403a93a5c \ - --hash=sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83 +tomlkit==0.11.8 # via pylint -tox==3.26.0 \ - --hash=sha256:44f3c347c68c2c68799d7d44f1808f9d396fc8a1a500cbc624253375c7ae107e \ - --hash=sha256:bf037662d7c740d15c9924ba23bb3e587df20598697bb985ac2b49bdc2d847f6 - # via -r requirements/dev.in -twine==4.0.1 \ - --hash=sha256:42026c18e394eac3e06693ee52010baa5313e4811d5a11050e7d48436cf41b9e \ - --hash=sha256:96b1cf12f7ae611a4a40b6ae8e9570215daff0611828f5fe1f37a16255ab24a0 +tox==4.5.2 + # via + # -r requirements/tox.in + # tox-gh +tox-gh==1.0.0 + # via -r requirements/tox.in +twine==4.0.2 # via -r requirements/dev.in -typed-ast==1.5.4 \ - --hash=sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2 \ - --hash=sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1 \ - --hash=sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6 \ - --hash=sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62 \ - --hash=sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac \ - --hash=sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d \ - --hash=sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc \ - --hash=sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2 \ - --hash=sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97 \ - --hash=sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35 \ - --hash=sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6 \ - --hash=sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1 \ - --hash=sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4 \ - --hash=sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c \ - --hash=sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e \ - --hash=sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec \ - --hash=sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f \ - --hash=sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72 \ - --hash=sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47 \ - --hash=sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72 \ - --hash=sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe \ - --hash=sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6 \ - --hash=sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3 \ - --hash=sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66 +typed-ast==1.5.4 # via astroid -typing-extensions==4.3.0 \ - --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \ - --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6 +typing-extensions==4.6.2 # via - # -r requirements/pip.pip - # -r requirements/pytest.pip # astroid # importlib-metadata + # markdown-it-py + # platformdirs # pylint # rich -urllib3==1.26.12 \ - --hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \ - --hash=sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997 + # tox +urllib3==2.0.2 # via # requests # twine -urwid==2.1.2 \ - --hash=sha256:588bee9c1cb208d0906a9f73c613d2bd32c3ed3702012f51efe318a3f2127eae +urwid==2.1.2 # via # pudb # urwid-readline -urwid-readline==0.13 \ - --hash=sha256:018020cbc864bb5ed87be17dc26b069eae2755cb29f3a9c569aac3bded1efaf4 +urwid-readline==0.13 # via pudb -virtualenv==20.16.5 \ - --hash=sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da \ - --hash=sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27 +virtualenv==20.23.0 # via - # -r requirements/pip.pip + # -r requirements/pip.in # tox -webencodings==0.5.1 \ - --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ - --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 +webencodings==0.5.1 # via bleach -wrapt==1.14.1 \ - --hash=sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3 \ - --hash=sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b \ - --hash=sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4 \ - --hash=sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2 \ - --hash=sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656 \ - --hash=sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3 \ - --hash=sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff \ - --hash=sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310 \ - --hash=sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a \ - --hash=sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57 \ - --hash=sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069 \ - --hash=sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383 \ - --hash=sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe \ - --hash=sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87 \ - --hash=sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d \ - --hash=sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b \ - --hash=sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907 \ - --hash=sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f \ - --hash=sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0 \ - --hash=sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28 \ - --hash=sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1 \ - --hash=sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853 \ - --hash=sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc \ - --hash=sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3 \ - --hash=sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3 \ - --hash=sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164 \ - --hash=sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1 \ - --hash=sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c \ - --hash=sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1 \ - --hash=sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7 \ - --hash=sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1 \ - --hash=sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320 \ - --hash=sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed \ - --hash=sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1 \ - --hash=sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248 \ - --hash=sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c \ - --hash=sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456 \ - --hash=sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77 \ - --hash=sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef \ - --hash=sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1 \ - --hash=sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7 \ - --hash=sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86 \ - --hash=sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4 \ - --hash=sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d \ - --hash=sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d \ - --hash=sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8 \ - --hash=sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5 \ - --hash=sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471 \ - --hash=sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00 \ - --hash=sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68 \ - --hash=sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3 \ - --hash=sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d \ - --hash=sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735 \ - --hash=sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d \ - --hash=sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569 \ - --hash=sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7 \ - --hash=sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59 \ - --hash=sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5 \ - --hash=sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb \ - --hash=sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b \ - --hash=sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f \ - --hash=sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462 \ - --hash=sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015 \ - --hash=sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af +wrapt==1.15.0 # via astroid -zipp==3.8.1 \ - --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \ - --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009 +zipp==3.15.0 # via - # -r requirements/pip.pip - # -r requirements/pytest.pip # importlib-metadata - # pep517 + # importlib-resources # The following packages are considered to be unsafe in a requirements file: -pip==22.0.4 \ - --hash=sha256:b3a9de2c6ef801e9247d1527a4b16f92f2cc141cd1489f3fffaf6a9e96729764 \ - --hash=sha256:c6aca0f2f081363f689f041d90dab2a07a9a07fb840284db2218117a52da800b - # via - # -c requirements/pins.pip - # -r requirements/pip.pip -setuptools==65.4.0 \ - --hash=sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9 \ - --hash=sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1 - # via check-manifest +pip==23.1.2 + # via -r requirements/pip.in +setuptools==67.8.0 + # via + # -r requirements/pip.in + # check-manifest diff -Nru python-coverage-6.5.0+dfsg1/requirements/kit.pip python-coverage-7.2.7+dfsg1/requirements/kit.pip --- python-coverage-6.5.0+dfsg1/requirements/kit.pip 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/requirements/kit.pip 2023-05-29 19:46:30.000000000 +0000 @@ -1,98 +1,54 @@ # -# This file is autogenerated by pip-compile with python 3.7 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: # # make upgrade # -auditwheel==5.1.2 \ - --hash=sha256:3ee5830014931ea84af5cd065c637b6614efa03d9b88bd8fbfc924e7ed01d6ba \ - --hash=sha256:4d06aea3ab59a2b8aa733798ac221556a3f5c021fddc42e5de5bcef20201c031 - # via -r requirements/kit.in -bashlex==0.16 \ - --hash=sha256:dc6f017e49ce2d0fe30ad9f5206da9cd13ded073d365688c9fda525354e8c373 \ - --hash=sha256:ff89fc743ccdef978792784d74d698a9236a862939bb4af471c0c3faf92c21bb +auditwheel==5.4.0 + # via -r requirements/kit.in +bashlex==0.18 # via cibuildwheel -bracex==2.3.post1 \ - --hash=sha256:351b7f20d56fb9ea91f9b9e9e7664db466eb234188c175fd943f8f755c807e73 \ - --hash=sha256:e7b23fc8b2cd06d3dec0692baabecb249dda94e06a617901ff03a6c56fd71693 +bracex==2.3.post1 # via cibuildwheel -build==0.8.0 \ - --hash=sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437 \ - --hash=sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0 - # via -r requirements/kit.in -certifi==2022.5.18.1 \ - --hash=sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7 \ - --hash=sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a - # via - # -c requirements/pins.pip - # cibuildwheel -cibuildwheel==2.10.2 \ - --hash=sha256:63ff185b4bc1ec62a87309411cc4bc297e6cfab051a3953ab1c7f0d6394b8da3 \ - --hash=sha256:d333005672c58f86b54c944983b495a91138f30cdbcaee47699ad9a29abc345d - # via -r requirements/kit.in -colorama==0.4.5 \ - --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \ - --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4 - # via -r requirements/kit.in -filelock==3.8.0 \ - --hash=sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc \ - --hash=sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4 +build==0.10.0 + # via -r requirements/kit.in +certifi==2023.5.7 + # via cibuildwheel +cibuildwheel==2.13.0 + # via -r requirements/kit.in +colorama==0.4.6 + # via -r requirements/kit.in +filelock==3.12.0 # via cibuildwheel -importlib-metadata==4.12.0 \ - --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \ - --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23 +importlib-metadata==6.6.0 # via # auditwheel # build - # pep517 -packaging==21.3 \ - --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ - --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 +packaging==23.1 # via # build # cibuildwheel -pep517==0.13.0 \ - --hash=sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b \ - --hash=sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59 - # via build -platformdirs==2.5.2 \ - --hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \ - --hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19 +platformdirs==3.5.1 # via cibuildwheel -pyelftools==0.29 \ - --hash=sha256:519f38cf412f073b2d7393aa4682b0190fa901f7c3fa0bff2b82d537690c7fc1 \ - --hash=sha256:ec761596aafa16e282a31de188737e5485552469ac63b60cfcccf22263fd24ff +pyelftools==0.29 # via auditwheel -pyparsing==3.0.9 \ - --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \ - --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc - # via packaging -tomli==2.0.1 \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f +pyproject-hooks==1.0.0 + # via build +tomli==2.0.1 # via # build # cibuildwheel - # pep517 -typing-extensions==4.3.0 \ - --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \ - --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6 + # pyproject-hooks +typing-extensions==4.6.2 # via # cibuildwheel # importlib-metadata -wheel==0.37.1 \ - --hash=sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a \ - --hash=sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4 - # via -r requirements/kit.in -zipp==3.8.1 \ - --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \ - --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009 - # via - # importlib-metadata - # pep517 + # platformdirs +wheel==0.40.0 + # via -r requirements/kit.in +zipp==3.15.0 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -setuptools==65.4.0 \ - --hash=sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9 \ - --hash=sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1 +setuptools==67.8.0 # via -r requirements/kit.in diff -Nru python-coverage-6.5.0+dfsg1/requirements/light-threads.pip python-coverage-7.2.7+dfsg1/requirements/light-threads.pip --- python-coverage-6.5.0+dfsg1/requirements/light-threads.pip 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/requirements/light-threads.pip 2023-05-29 19:46:30.000000000 +0000 @@ -1,245 +1,34 @@ # -# This file is autogenerated by pip-compile with python 3.7 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: # # make upgrade # -cffi==1.15.1 \ - --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ - --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \ - --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \ - --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \ - --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \ - --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \ - --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \ - --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \ - --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \ - --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \ - --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \ - --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \ - --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \ - --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \ - --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \ - --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \ - --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \ - --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \ - --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \ - --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \ - --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \ - --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \ - --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \ - --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \ - --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \ - --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \ - --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \ - --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \ - --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \ - --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \ - --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \ - --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \ - --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \ - --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \ - --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \ - --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \ - --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \ - --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \ - --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \ - --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \ - --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \ - --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \ - --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \ - --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \ - --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \ - --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \ - --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \ - --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \ - --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \ - --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \ - --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \ - --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \ - --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \ - --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \ - --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \ - --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \ - --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \ - --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \ - --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \ - --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \ - --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \ - --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \ - --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \ - --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0 +cffi==1.15.1 # via -r requirements/light-threads.in -dnspython==2.2.1 \ - --hash=sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e \ - --hash=sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f +dnspython==2.3.0 # via eventlet -eventlet==0.33.1 \ - --hash=sha256:a085922698e5029f820cf311a648ac324d73cec0e4792877609d978a4b5bbf31 \ - --hash=sha256:afbe17f06a58491e9aebd7a4a03e70b0b63fd4cf76d8307bae07f280479b1515 +eventlet==0.33.3 # via -r requirements/light-threads.in -gevent==21.12.0 \ - --hash=sha256:0082d8a5d23c35812ce0e716a91ede597f6dd2c5ff508a02a998f73598c59397 \ - --hash=sha256:01928770972181ad8866ee37ea3504f1824587b188fcab782ef1619ce7538766 \ - --hash=sha256:05c5e8a50cd6868dd36536c92fb4468d18090e801bd63611593c0717bab63692 \ - --hash=sha256:08b4c17064e28f4eb85604486abc89f442c7407d2aed249cf54544ce5c9baee6 \ - --hash=sha256:177f93a3a90f46a5009e0841fef561601e5c637ba4332ab8572edd96af650101 \ - --hash=sha256:22ce1f38fdfe2149ffe8ec2131ca45281791c1e464db34b3b4321ae9d8d2efbb \ - --hash=sha256:24d3550fbaeef5fddd794819c2853bca45a86c3d64a056a2c268d981518220d1 \ - --hash=sha256:2afa3f3ad528155433f6ac8bd64fa5cc303855b97004416ec719a6b1ca179481 \ - --hash=sha256:2bcec9f80196c751fdcf389ca9f7141e7b0db960d8465ed79be5e685bfcad682 \ - --hash=sha256:2cfff82f05f14b7f5d9ed53ccb7a609ae8604df522bb05c971bca78ec9d8b2b9 \ - --hash=sha256:3baeeccc4791ba3f8db27179dff11855a8f9210ddd754f6c9b48e0d2561c2aea \ - --hash=sha256:3c012c73e6c61f13c75e3a4869dbe6a2ffa025f103421a6de9c85e627e7477b1 \ - --hash=sha256:3dad62f55fad839d498c801e139481348991cee6e1c7706041b5fe096cb6a279 \ - --hash=sha256:542ae891e2aa217d2cf6d8446538fcd2f3263a40eec123b970b899bac391c47a \ - --hash=sha256:6a02a88723ed3f0fd92cbf1df3c4cd2fbd87d82b0a4bac3e36a8875923115214 \ - --hash=sha256:74fc1ef16b86616cfddcc74f7292642b0f72dde4dd95aebf4c45bb236744be54 \ - --hash=sha256:7909780f0cf18a1fc32aafd8c8e130cdd93c6e285b11263f7f2d1a0f3678bc50 \ - --hash=sha256:7ccffcf708094564e442ac6fde46f0ae9e40015cb69d995f4b39cc29a7643881 \ - --hash=sha256:8c21cb5c9f4e14d75b3fe0b143ec875d7dbd1495fad6d49704b00e57e781ee0f \ - --hash=sha256:973749bacb7bc4f4181a8fb2a7e0e2ff44038de56d08e856dd54a5ac1d7331b4 \ - --hash=sha256:9d86438ede1cbe0fde6ef4cc3f72bf2f1ecc9630d8b633ff344a3aeeca272cdd \ - --hash=sha256:9f9652d1e4062d4b5b5a0a49ff679fa890430b5f76969d35dccb2df114c55e0f \ - --hash=sha256:a5ad4ed8afa0a71e1927623589f06a9b5e8b5e77810be3125cb4d93050d3fd1f \ - --hash=sha256:b7709c64afa8bb3000c28bb91ec42c79594a7cb0f322e20427d57f9762366a5b \ - --hash=sha256:bb5cb8db753469c7a9a0b8a972d2660fe851aa06eee699a1ca42988afb0aaa02 \ - --hash=sha256:c43f081cbca41d27fd8fef9c6a32cf83cb979345b20abc07bf68df165cdadb24 \ - --hash=sha256:cc2fef0f98ee180704cf95ec84f2bc2d86c6c3711bb6b6740d74e0afe708b62c \ - --hash=sha256:da8d2d51a49b2a5beb02ad619ca9ddbef806ef4870ba04e5ac7b8b41a5b61db3 \ - --hash=sha256:e1899b921219fc8959ff9afb94dae36be82e0769ed13d330a393594d478a0b3a \ - --hash=sha256:eae3c46f9484eaacd67ffcdf4eaf6ca830f587edd543613b0f5c4eb3c11d052d \ - --hash=sha256:ec21f9eaaa6a7b1e62da786132d6788675b314f25f98d9541f1bf00584ed4749 \ - --hash=sha256:f289fae643a3f1c3b909d6b033e6921b05234a4907e9c9c8c3f1fe403e6ac452 \ - --hash=sha256:f48b64578c367b91fa793bf8eaaaf4995cb93c8bc45860e473bf868070ad094e +gevent==22.10.2 # via -r requirements/light-threads.in -greenlet==1.1.3 \ - --hash=sha256:0118817c9341ef2b0f75f5af79ac377e4da6ff637e5ee4ac91802c0e379dadb4 \ - --hash=sha256:048d2bed76c2aa6de7af500ae0ea51dd2267aec0e0f2a436981159053d0bc7cc \ - --hash=sha256:07c58e169bbe1e87b8bbf15a5c1b779a7616df9fd3e61cadc9d691740015b4f8 \ - --hash=sha256:095a980288fe05adf3d002fbb180c99bdcf0f930e220aa66fcd56e7914a38202 \ - --hash=sha256:0b181e9aa6cb2f5ec0cacc8cee6e5a3093416c841ba32c185c30c160487f0380 \ - --hash=sha256:1626185d938d7381631e48e6f7713e8d4b964be246073e1a1d15c2f061ac9f08 \ - --hash=sha256:184416e481295832350a4bf731ba619a92f5689bf5d0fa4341e98b98b1265bd7 \ - --hash=sha256:1dd51d2650e70c6c4af37f454737bf4a11e568945b27f74b471e8e2a9fd21268 \ - --hash=sha256:1ec2779774d8e42ed0440cf8bc55540175187e8e934f2be25199bf4ed948cd9e \ - --hash=sha256:2cf45e339cabea16c07586306a31cfcc5a3b5e1626d365714d283732afed6809 \ - --hash=sha256:2fb0aa7f6996879551fd67461d5d3ab0c3c0245da98be90c89fcb7a18d437403 \ - --hash=sha256:44b4817c34c9272c65550b788913620f1fdc80362b209bc9d7dd2f40d8793080 \ - --hash=sha256:466ce0928e33421ee84ae04c4ac6f253a3a3e6b8d600a79bd43fd4403e0a7a76 \ - --hash=sha256:4f166b4aca8d7d489e82d74627a7069ab34211ef5ebb57c300ec4b9337b60fc0 \ - --hash=sha256:510c3b15587afce9800198b4b142202b323bf4b4b5f9d6c79cb9a35e5e3c30d2 \ - --hash=sha256:5b756e6730ea59b2745072e28ad27f4c837084688e6a6b3633c8b1e509e6ae0e \ - --hash=sha256:5fbe1ab72b998ca77ceabbae63a9b2e2dc2d963f4299b9b278252ddba142d3f1 \ - --hash=sha256:6200a11f003ec26815f7e3d2ded01b43a3810be3528dd760d2f1fa777490c3cd \ - --hash=sha256:65ad1a7a463a2a6f863661329a944a5802c7129f7ad33583dcc11069c17e622c \ - --hash=sha256:694ffa7144fa5cc526c8f4512665003a39fa09ef00d19bbca5c8d3406db72fbe \ - --hash=sha256:6f5d4b2280ceea76c55c893827961ed0a6eadd5a584a7c4e6e6dd7bc10dfdd96 \ - --hash=sha256:7532a46505470be30cbf1dbadb20379fb481244f1ca54207d7df3bf0bbab6a20 \ - --hash=sha256:76a53bfa10b367ee734b95988bd82a9a5f0038a25030f9f23bbbc005010ca600 \ - --hash=sha256:77e41db75f9958f2083e03e9dd39da12247b3430c92267df3af77c83d8ff9eed \ - --hash=sha256:7a43bbfa9b6cfdfaeefbd91038dde65ea2c421dc387ed171613df340650874f2 \ - --hash=sha256:7b41d19c0cfe5c259fe6c539fd75051cd39a5d33d05482f885faf43f7f5e7d26 \ - --hash=sha256:7c5227963409551ae4a6938beb70d56bf1918c554a287d3da6853526212fbe0a \ - --hash=sha256:870a48007872d12e95a996fca3c03a64290d3ea2e61076aa35d3b253cf34cd32 \ - --hash=sha256:88b04e12c9b041a1e0bcb886fec709c488192638a9a7a3677513ac6ba81d8e79 \ - --hash=sha256:8c287ae7ac921dfde88b1c125bd9590b7ec3c900c2d3db5197f1286e144e712b \ - --hash=sha256:903fa5716b8fbb21019268b44f73f3748c41d1a30d71b4a49c84b642c2fed5fa \ - --hash=sha256:9537e4baf0db67f382eb29255a03154fcd4984638303ff9baaa738b10371fa57 \ - --hash=sha256:9951dcbd37850da32b2cb6e391f621c1ee456191c6ae5528af4a34afe357c30e \ - --hash=sha256:9b2f7d0408ddeb8ea1fd43d3db79a8cefaccadd2a812f021333b338ed6b10aba \ - --hash=sha256:9c88e134d51d5e82315a7c32b914a58751b7353eb5268dbd02eabf020b4c4700 \ - --hash=sha256:9fae214f6c43cd47f7bef98c56919b9222481e833be2915f6857a1e9e8a15318 \ - --hash=sha256:a3a669f11289a8995d24fbfc0e63f8289dd03c9aaa0cc8f1eab31d18ca61a382 \ - --hash=sha256:aa741c1a8a8cc25eb3a3a01a62bdb5095a773d8c6a86470bde7f607a447e7905 \ - --hash=sha256:b0877a9a2129a2c56a2eae2da016743db7d9d6a05d5e1c198f1b7808c602a30e \ - --hash=sha256:bcb6c6dd1d6be6d38d6db283747d07fda089ff8c559a835236560a4410340455 \ - --hash=sha256:caff52cb5cd7626872d9696aee5b794abe172804beb7db52eed1fd5824b63910 \ - --hash=sha256:cbc1eb55342cbac8f7ec159088d54e2cfdd5ddf61c87b8bbe682d113789331b2 \ - --hash=sha256:cd16a89efe3a003029c87ff19e9fba635864e064da646bc749fc1908a4af18f3 \ - --hash=sha256:ce5b64dfe8d0cca407d88b0ee619d80d4215a2612c1af8c98a92180e7109f4b5 \ - --hash=sha256:d58a5a71c4c37354f9e0c24c9c8321f0185f6945ef027460b809f4bb474bfe41 \ - --hash=sha256:db41f3845eb579b544c962864cce2c2a0257fe30f0f1e18e51b1e8cbb4e0ac6d \ - --hash=sha256:db5b25265010a1b3dca6a174a443a0ed4c4ab12d5e2883a11c97d6e6d59b12f9 \ - --hash=sha256:dd0404d154084a371e6d2bafc787201612a1359c2dee688ae334f9118aa0bf47 \ - --hash=sha256:de431765bd5fe62119e0bc6bc6e7b17ac53017ae1782acf88fcf6b7eae475a49 \ - --hash=sha256:df02fdec0c533301497acb0bc0f27f479a3a63dcdc3a099ae33a902857f07477 \ - --hash=sha256:e8533f5111704d75de3139bf0b8136d3a6c1642c55c067866fa0a51c2155ee33 \ - --hash=sha256:f2f908239b7098799b8845e5936c2ccb91d8c2323be02e82f8dcb4a80dcf4a25 \ - --hash=sha256:f8bfd36f368efe0ab2a6aa3db7f14598aac454b06849fb633b762ddbede1db90 \ - --hash=sha256:ffe73f9e7aea404722058405ff24041e59d31ca23d1da0895af48050a07b6932 +greenlet==2.0.2 # via # -r requirements/light-threads.in # eventlet # gevent -pycparser==2.21 \ - --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ - --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 +pycparser==2.21 # via cffi -six==1.16.0 \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 +six==1.16.0 # via eventlet -zope-event==4.5.0 \ - --hash=sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42 \ - --hash=sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330 +zope-event==4.6 # via gevent -zope-interface==5.4.0 \ - --hash=sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192 \ - --hash=sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702 \ - --hash=sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09 \ - --hash=sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4 \ - --hash=sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a \ - --hash=sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3 \ - --hash=sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf \ - --hash=sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c \ - --hash=sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d \ - --hash=sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78 \ - --hash=sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83 \ - --hash=sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531 \ - --hash=sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46 \ - --hash=sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021 \ - --hash=sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94 \ - --hash=sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc \ - --hash=sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63 \ - --hash=sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54 \ - --hash=sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117 \ - --hash=sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25 \ - --hash=sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05 \ - --hash=sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e \ - --hash=sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1 \ - --hash=sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004 \ - --hash=sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2 \ - --hash=sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e \ - --hash=sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f \ - --hash=sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f \ - --hash=sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120 \ - --hash=sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f \ - --hash=sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1 \ - --hash=sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9 \ - --hash=sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e \ - --hash=sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7 \ - --hash=sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8 \ - --hash=sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b \ - --hash=sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155 \ - --hash=sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7 \ - --hash=sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c \ - --hash=sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325 \ - --hash=sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d \ - --hash=sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb \ - --hash=sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e \ - --hash=sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959 \ - --hash=sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7 \ - --hash=sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920 \ - --hash=sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e \ - --hash=sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48 \ - --hash=sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8 \ - --hash=sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4 \ - --hash=sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263 +zope-interface==6.0 # via gevent # The following packages are considered to be unsafe in a requirements file: -setuptools==65.4.0 \ - --hash=sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9 \ - --hash=sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1 - # via gevent +setuptools==67.8.0 + # via + # gevent + # zope-event + # zope-interface diff -Nru python-coverage-6.5.0+dfsg1/requirements/lint.pip python-coverage-7.2.7+dfsg1/requirements/lint.pip --- python-coverage-6.5.0+dfsg1/requirements/lint.pip 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/requirements/lint.pip 2023-05-29 19:46:30.000000000 +0000 @@ -1,204 +1,84 @@ # -# This file is autogenerated by pip-compile with python 3.7 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: # # make upgrade # -alabaster==0.7.12 \ - --hash=sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359 \ - --hash=sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02 +alabaster==0.7.13 # via sphinx -astroid==2.12.10 \ - --hash=sha256:81f870105d892e73bf535da77a8261aa5bde838fa4ed12bb2f435291a098c581 \ - --hash=sha256:997e0c735df60d4a4caff27080a3afc51f9bdd693d3572a4a0b7090b645c36c5 +astroid==2.15.5 # via pylint -atomicwrites==1.4.1 \ - --hash=sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11 - # via -r requirements/pytest.pip -attrs==22.1.0 \ - --hash=sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6 \ - --hash=sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c +attrs==23.1.0 # via - # -r requirements/pytest.pip # hypothesis - # pytest -babel==2.10.3 \ - --hash=sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51 \ - --hash=sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb + # scriv +babel==2.12.1 # via sphinx -backports-functools-lru-cache==1.6.4 \ - --hash=sha256:d5ed2169378b67d3c545e5600d363a923b09c456dab1593914935a68ad478271 \ - --hash=sha256:dbead04b9daa817909ec64e8d2855fb78feafe0b901d4568758e3a60559d8978 - # via - # -r requirements/pytest.pip - # pycontracts -bleach==5.0.1 \ - --hash=sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a \ - --hash=sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c +bleach==6.0.0 # via readme-renderer -build==0.8.0 \ - --hash=sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437 \ - --hash=sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0 +build==0.10.0 # via check-manifest -certifi==2022.5.18.1 \ - --hash=sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7 \ - --hash=sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a - # via - # -c doc/../requirements/pins.pip - # -c requirements/pins.pip - # requests -charset-normalizer==2.1.1 \ - --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \ - --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f +cachetools==5.3.1 + # via tox +certifi==2023.5.7 # via requests -check-manifest==0.48 \ - --hash=sha256:3b575f1dade7beb3078ef4bf33a94519834457c7281dbc726b15c5466b55c657 \ - --hash=sha256:b1923685f98c1c2468601a1a7bed655db549a25d43c583caded3860ad8308f8c +chardet==5.1.0 + # via tox +charset-normalizer==3.1.0 + # via requests +check-manifest==0.49 # via -r requirements/dev.in -cogapp==3.3.0 \ - --hash=sha256:1be95183f70282422d594fa42426be6923070a4bd8335621f6347f3aeee81db0 \ - --hash=sha256:8b5b5f6063d8ee231961c05da010cb27c30876b2279e23ad0eae5f8f09460d50 +click==8.1.3 + # via + # click-log + # scriv +click-log==0.4.0 + # via scriv +cogapp==3.3.0 # via # -r doc/requirements.in # -r requirements/dev.in -colorama==0.4.5 \ - --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \ - --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4 +colorama==0.4.6 # via - # -r requirements/pytest.pip + # -r requirements/pytest.in + # -r requirements/tox.in # sphinx-autobuild -commonmark==0.9.1 \ - --hash=sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60 \ - --hash=sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9 - # via rich -decorator==5.1.1 \ - --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \ - --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186 - # via - # -r requirements/pytest.pip - # pycontracts -dill==0.3.5.1 \ - --hash=sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302 \ - --hash=sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86 + # tox +dill==0.3.6 # via pylint -distlib==0.3.6 \ - --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \ - --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e - # via - # -r requirements/pip.pip - # virtualenv -docutils==0.17.1 \ - --hash=sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125 \ - --hash=sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61 +distlib==0.3.6 + # via virtualenv +docutils==0.18.1 # via # readme-renderer # sphinx # sphinx-rtd-theme -exceptiongroup==1.0.0rc9 \ - --hash=sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337 \ - --hash=sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96 +exceptiongroup==1.1.1 # via - # -r requirements/pytest.pip # hypothesis -execnet==1.9.0 \ - --hash=sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5 \ - --hash=sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142 - # via - # -r requirements/pytest.pip - # pytest-xdist -filelock==3.8.0 \ - --hash=sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc \ - --hash=sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4 + # pytest +execnet==1.9.0 + # via pytest-xdist +filelock==3.12.0 # via - # -r requirements/pip.pip # tox # virtualenv -flaky==3.7.0 \ - --hash=sha256:3ad100780721a1911f57a165809b7ea265a7863305acb66708220820caf8aa0d \ - --hash=sha256:d6eda73cab5ae7364504b7c44670f70abed9e75f77dd116352f662817592ec9c - # via -r requirements/pytest.pip -future==0.18.2 \ - --hash=sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d - # via - # -r requirements/pytest.pip - # pycontracts -greenlet==1.1.3 \ - --hash=sha256:0118817c9341ef2b0f75f5af79ac377e4da6ff637e5ee4ac91802c0e379dadb4 \ - --hash=sha256:048d2bed76c2aa6de7af500ae0ea51dd2267aec0e0f2a436981159053d0bc7cc \ - --hash=sha256:07c58e169bbe1e87b8bbf15a5c1b779a7616df9fd3e61cadc9d691740015b4f8 \ - --hash=sha256:095a980288fe05adf3d002fbb180c99bdcf0f930e220aa66fcd56e7914a38202 \ - --hash=sha256:0b181e9aa6cb2f5ec0cacc8cee6e5a3093416c841ba32c185c30c160487f0380 \ - --hash=sha256:1626185d938d7381631e48e6f7713e8d4b964be246073e1a1d15c2f061ac9f08 \ - --hash=sha256:184416e481295832350a4bf731ba619a92f5689bf5d0fa4341e98b98b1265bd7 \ - --hash=sha256:1dd51d2650e70c6c4af37f454737bf4a11e568945b27f74b471e8e2a9fd21268 \ - --hash=sha256:1ec2779774d8e42ed0440cf8bc55540175187e8e934f2be25199bf4ed948cd9e \ - --hash=sha256:2cf45e339cabea16c07586306a31cfcc5a3b5e1626d365714d283732afed6809 \ - --hash=sha256:2fb0aa7f6996879551fd67461d5d3ab0c3c0245da98be90c89fcb7a18d437403 \ - --hash=sha256:44b4817c34c9272c65550b788913620f1fdc80362b209bc9d7dd2f40d8793080 \ - --hash=sha256:466ce0928e33421ee84ae04c4ac6f253a3a3e6b8d600a79bd43fd4403e0a7a76 \ - --hash=sha256:4f166b4aca8d7d489e82d74627a7069ab34211ef5ebb57c300ec4b9337b60fc0 \ - --hash=sha256:510c3b15587afce9800198b4b142202b323bf4b4b5f9d6c79cb9a35e5e3c30d2 \ - --hash=sha256:5b756e6730ea59b2745072e28ad27f4c837084688e6a6b3633c8b1e509e6ae0e \ - --hash=sha256:5fbe1ab72b998ca77ceabbae63a9b2e2dc2d963f4299b9b278252ddba142d3f1 \ - --hash=sha256:6200a11f003ec26815f7e3d2ded01b43a3810be3528dd760d2f1fa777490c3cd \ - --hash=sha256:65ad1a7a463a2a6f863661329a944a5802c7129f7ad33583dcc11069c17e622c \ - --hash=sha256:694ffa7144fa5cc526c8f4512665003a39fa09ef00d19bbca5c8d3406db72fbe \ - --hash=sha256:6f5d4b2280ceea76c55c893827961ed0a6eadd5a584a7c4e6e6dd7bc10dfdd96 \ - --hash=sha256:7532a46505470be30cbf1dbadb20379fb481244f1ca54207d7df3bf0bbab6a20 \ - --hash=sha256:76a53bfa10b367ee734b95988bd82a9a5f0038a25030f9f23bbbc005010ca600 \ - --hash=sha256:77e41db75f9958f2083e03e9dd39da12247b3430c92267df3af77c83d8ff9eed \ - --hash=sha256:7a43bbfa9b6cfdfaeefbd91038dde65ea2c421dc387ed171613df340650874f2 \ - --hash=sha256:7b41d19c0cfe5c259fe6c539fd75051cd39a5d33d05482f885faf43f7f5e7d26 \ - --hash=sha256:7c5227963409551ae4a6938beb70d56bf1918c554a287d3da6853526212fbe0a \ - --hash=sha256:870a48007872d12e95a996fca3c03a64290d3ea2e61076aa35d3b253cf34cd32 \ - --hash=sha256:88b04e12c9b041a1e0bcb886fec709c488192638a9a7a3677513ac6ba81d8e79 \ - --hash=sha256:8c287ae7ac921dfde88b1c125bd9590b7ec3c900c2d3db5197f1286e144e712b \ - --hash=sha256:903fa5716b8fbb21019268b44f73f3748c41d1a30d71b4a49c84b642c2fed5fa \ - --hash=sha256:9537e4baf0db67f382eb29255a03154fcd4984638303ff9baaa738b10371fa57 \ - --hash=sha256:9951dcbd37850da32b2cb6e391f621c1ee456191c6ae5528af4a34afe357c30e \ - --hash=sha256:9b2f7d0408ddeb8ea1fd43d3db79a8cefaccadd2a812f021333b338ed6b10aba \ - --hash=sha256:9c88e134d51d5e82315a7c32b914a58751b7353eb5268dbd02eabf020b4c4700 \ - --hash=sha256:9fae214f6c43cd47f7bef98c56919b9222481e833be2915f6857a1e9e8a15318 \ - --hash=sha256:a3a669f11289a8995d24fbfc0e63f8289dd03c9aaa0cc8f1eab31d18ca61a382 \ - --hash=sha256:aa741c1a8a8cc25eb3a3a01a62bdb5095a773d8c6a86470bde7f607a447e7905 \ - --hash=sha256:b0877a9a2129a2c56a2eae2da016743db7d9d6a05d5e1c198f1b7808c602a30e \ - --hash=sha256:bcb6c6dd1d6be6d38d6db283747d07fda089ff8c559a835236560a4410340455 \ - --hash=sha256:caff52cb5cd7626872d9696aee5b794abe172804beb7db52eed1fd5824b63910 \ - --hash=sha256:cbc1eb55342cbac8f7ec159088d54e2cfdd5ddf61c87b8bbe682d113789331b2 \ - --hash=sha256:cd16a89efe3a003029c87ff19e9fba635864e064da646bc749fc1908a4af18f3 \ - --hash=sha256:ce5b64dfe8d0cca407d88b0ee619d80d4215a2612c1af8c98a92180e7109f4b5 \ - --hash=sha256:d58a5a71c4c37354f9e0c24c9c8321f0185f6945ef027460b809f4bb474bfe41 \ - --hash=sha256:db41f3845eb579b544c962864cce2c2a0257fe30f0f1e18e51b1e8cbb4e0ac6d \ - --hash=sha256:db5b25265010a1b3dca6a174a443a0ed4c4ab12d5e2883a11c97d6e6d59b12f9 \ - --hash=sha256:dd0404d154084a371e6d2bafc787201612a1359c2dee688ae334f9118aa0bf47 \ - --hash=sha256:de431765bd5fe62119e0bc6bc6e7b17ac53017ae1782acf88fcf6b7eae475a49 \ - --hash=sha256:df02fdec0c533301497acb0bc0f27f479a3a63dcdc3a099ae33a902857f07477 \ - --hash=sha256:e8533f5111704d75de3139bf0b8136d3a6c1642c55c067866fa0a51c2155ee33 \ - --hash=sha256:f2f908239b7098799b8845e5936c2ccb91d8c2323be02e82f8dcb4a80dcf4a25 \ - --hash=sha256:f8bfd36f368efe0ab2a6aa3db7f14598aac454b06849fb633b762ddbede1db90 \ - --hash=sha256:ffe73f9e7aea404722058405ff24041e59d31ca23d1da0895af48050a07b6932 +flaky==3.7.0 + # via -r requirements/pytest.in +greenlet==2.0.2 # via -r requirements/dev.in -hypothesis==6.54.6 \ - --hash=sha256:2d5e2d5ccd0efce4e0968a6164f4e4853f808e33f4d91490c975c98beec0c7c3 \ - --hash=sha256:e44833325f9a55f795596ceefd7ede7d626cfe45836025d2647cccaff7070e10 - # via -r requirements/pytest.pip -idna==3.4 \ - --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ - --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 +hypothesis==6.75.6 + # via -r requirements/pytest.in +idna==3.4 # via requests -imagesize==1.4.1 \ - --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ - --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a +imagesize==1.4.1 # via sphinx -importlib-metadata==4.12.0 \ - --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \ - --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23 +importlib-metadata==6.6.0 # via - # -r requirements/pip.pip - # -r requirements/pytest.pip + # attrs # build + # click # keyring - # pep517 # pluggy # pytest # sphinx @@ -206,514 +86,201 @@ # tox # twine # virtualenv -iniconfig==1.1.1 \ - --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ - --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 - # via - # -r requirements/pytest.pip - # pytest -isort==5.10.1 \ - --hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \ - --hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951 +importlib-resources==5.12.0 + # via keyring +iniconfig==2.0.0 + # via pytest +isort==5.11.5 # via pylint -jaraco-classes==3.2.3 \ - --hash=sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158 \ - --hash=sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a +jaraco-classes==3.2.3 # via keyring -jedi==0.18.1 \ - --hash=sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d \ - --hash=sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab +jedi==0.18.2 # via pudb -jinja2==3.1.2 \ - --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ - --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 - # via sphinx -keyring==23.9.3 \ - --hash=sha256:69732a15cb1433bdfbc3b980a8a36a04878a6cfd7cb99f497b573f31618001c0 \ - --hash=sha256:69b01dd83c42f590250fe7a1f503fc229b14de83857314b1933a3ddbf595c4a5 +jinja2==3.1.2 + # via + # scriv + # sphinx +keyring==23.13.1 # via twine -lazy-object-proxy==1.7.1 \ - --hash=sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7 \ - --hash=sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a \ - --hash=sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c \ - --hash=sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc \ - --hash=sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f \ - --hash=sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09 \ - --hash=sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442 \ - --hash=sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e \ - --hash=sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029 \ - --hash=sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61 \ - --hash=sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb \ - --hash=sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0 \ - --hash=sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35 \ - --hash=sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42 \ - --hash=sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1 \ - --hash=sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad \ - --hash=sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443 \ - --hash=sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd \ - --hash=sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9 \ - --hash=sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148 \ - --hash=sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38 \ - --hash=sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55 \ - --hash=sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36 \ - --hash=sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a \ - --hash=sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b \ - --hash=sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44 \ - --hash=sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6 \ - --hash=sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69 \ - --hash=sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4 \ - --hash=sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84 \ - --hash=sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de \ - --hash=sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28 \ - --hash=sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c \ - --hash=sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1 \ - --hash=sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8 \ - --hash=sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b \ - --hash=sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb +lazy-object-proxy==1.9.0 # via astroid -libsass==0.21.0 \ - --hash=sha256:06c8776417fe930714bdc930a3d7e795ae3d72be6ac883ff72a1b8f7c49e5ffb \ - --hash=sha256:12f39712de38689a8b785b7db41d3ba2ea1d46f9379d81ea4595802d91fa6529 \ - --hash=sha256:1e25dd9047a9392d3c59a0b869e0404f2b325a03871ee45285ee33b3664f5613 \ - --hash=sha256:659ae41af8708681fa3ec73f47b9735a6725e71c3b66ff570bfce78952f2314e \ - --hash=sha256:6b984510ed94993708c0d697b4fef2d118929bbfffc3b90037be0f5ccadf55e7 \ - --hash=sha256:a005f298f64624f313a3ac618ab03f844c71d84ae4f4a4aec4b68d2a4ffe75eb \ - --hash=sha256:abc29357ee540849faf1383e1746d40d69ed5cb6d4c346df276b258f5aa8977a \ - --hash=sha256:c9ec490609752c1d81ff6290da33485aa7cb6d7365ac665b74464c1b7d97f7da \ - --hash=sha256:d5ba529d9ce668be9380563279f3ffe988f27bc5b299c5a28453df2e0b0fbaf2 \ - --hash=sha256:e2b1a7d093f2e76dc694c17c0c285e846d0b0deb0e8b21dc852ba1a3a4e2f1d6 +libsass==0.22.0 # via -r requirements/dev.in -livereload==2.6.3 \ - --hash=sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869 +livereload==2.6.3 # via sphinx-autobuild -markupsafe==2.1.1 \ - --hash=sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003 \ - --hash=sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88 \ - --hash=sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5 \ - --hash=sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7 \ - --hash=sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a \ - --hash=sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603 \ - --hash=sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1 \ - --hash=sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135 \ - --hash=sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247 \ - --hash=sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6 \ - --hash=sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601 \ - --hash=sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77 \ - --hash=sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02 \ - --hash=sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e \ - --hash=sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63 \ - --hash=sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f \ - --hash=sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980 \ - --hash=sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b \ - --hash=sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812 \ - --hash=sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff \ - --hash=sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96 \ - --hash=sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1 \ - --hash=sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925 \ - --hash=sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a \ - --hash=sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6 \ - --hash=sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e \ - --hash=sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f \ - --hash=sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4 \ - --hash=sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f \ - --hash=sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3 \ - --hash=sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c \ - --hash=sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a \ - --hash=sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417 \ - --hash=sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a \ - --hash=sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a \ - --hash=sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37 \ - --hash=sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452 \ - --hash=sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933 \ - --hash=sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a \ - --hash=sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7 +markdown-it-py==2.2.0 + # via rich +markupsafe==2.1.2 # via jinja2 -mccabe==0.7.0 \ - --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \ - --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e +mccabe==0.7.0 # via pylint -more-itertools==8.14.0 \ - --hash=sha256:1bc4f91ee5b1b31ac7ceacc17c09befe6a40a503907baf9c839c229b5095cfd2 \ - --hash=sha256:c09443cd3d5438b8dafccd867a6bc1cb0894389e90cb53d227456b0b0bccb750 +mdurl==0.1.2 + # via markdown-it-py +more-itertools==9.1.0 # via jaraco-classes -packaging==21.3 \ - --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ - --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 +packaging==23.1 # via - # -r requirements/pytest.pip # build # pudb + # pyproject-api # pytest # sphinx # tox -parso==0.8.3 \ - --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ - --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 +parso==0.8.3 # via jedi -pep517==0.13.0 \ - --hash=sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b \ - --hash=sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59 - # via build -pkginfo==1.8.3 \ - --hash=sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594 \ - --hash=sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c +pkginfo==1.9.6 # via twine -platformdirs==2.5.2 \ - --hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \ - --hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19 +platformdirs==3.5.1 # via - # -r requirements/pip.pip # pylint + # tox # virtualenv -pluggy==1.0.0 \ - --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \ - --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 +pluggy==1.0.0 # via - # -r requirements/pytest.pip # pytest # tox -pudb==2022.1.2 \ - --hash=sha256:6b83ab805bddb53710109690a2237e98bf83c0b3a00033c517cdf5f6a8fa470d +pudb==2022.1.3 # via -r requirements/dev.in -py==1.11.0 \ - --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ - --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 - # via - # -r requirements/pytest.pip - # pytest - # pytest-forked - # tox -pycontracts @ https://github.com/slorg1/contracts/archive/c5a6da27d4dc9985f68e574d20d86000880919c3.zip \ - --hash=sha256:2b889cbfb03b43dc811b5879248ac5c7e209ece78f03be9633de76a6b21a5a89 - # via -r requirements/pytest.pip -pyenchant==3.2.2 \ - --hash=sha256:1cf830c6614362a78aab78d50eaf7c6c93831369c52e1bb64ffae1df0341e637 \ - --hash=sha256:5a636832987eaf26efe971968f4d1b78e81f62bca2bde0a9da210c7de43c3bce \ - --hash=sha256:5facc821ece957208a81423af7d6ec7810dad29697cb0d77aae81e4e11c8e5a6 \ - --hash=sha256:6153f521852e23a5add923dbacfbf4bebbb8d70c4e4bad609a8e0f9faeb915d1 +pyenchant==3.2.2 # via # -r doc/requirements.in # sphinxcontrib-spelling -pygments==2.13.0 \ - --hash=sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1 \ - --hash=sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42 +pygments==2.15.1 # via # pudb # readme-renderer # rich # sphinx -pylint==2.15.3 \ - --hash=sha256:5fdfd44af182866999e6123139d265334267339f29961f00c89783155eacc60b \ - --hash=sha256:7f6aad1d8d50807f7bc64f89ac75256a9baf8e6ed491cc9bc65592bc3f462cf1 +pylint==2.17.4 # via -r requirements/dev.in -pyparsing==3.0.9 \ - --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \ - --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc - # via - # -r requirements/pytest.pip - # packaging - # pycontracts -pytest==7.1.3 \ - --hash=sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7 \ - --hash=sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39 - # via - # -r requirements/pytest.pip - # pytest-forked - # pytest-xdist -pytest-forked==1.4.0 \ - --hash=sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e \ - --hash=sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8 +pyproject-api==1.5.1 + # via tox +pyproject-hooks==1.0.0 + # via build +pytest==7.3.1 # via - # -r requirements/pytest.pip + # -r requirements/pytest.in # pytest-xdist -pytest-xdist==2.5.0 \ - --hash=sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf \ - --hash=sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65 - # via -r requirements/pytest.pip -pytz==2022.2.1 \ - --hash=sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197 \ - --hash=sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5 +pytest-xdist==3.3.1 + # via -r requirements/pytest.in +pytz==2023.3 # via babel -qualname==0.1.0 \ - --hash=sha256:277cf6aa4b2ad36beed1153cfa7bf521b210d54fbecb3d8eea0c5679cecc9ed8 - # via - # -r requirements/pytest.pip - # pycontracts -readme-renderer==37.2 \ - --hash=sha256:d3f06a69e8c40fca9ab3174eca48f96d9771eddb43517b17d96583418427b106 \ - --hash=sha256:e8ad25293c98f781dbc2c5a36a309929390009f902f99e1798c761aaf04a7923 +readme-renderer==37.3 # via # -r requirements/dev.in # twine -requests==2.28.1 \ - --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \ - --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349 +requests==2.31.0 # via # -r requirements/dev.in # requests-toolbelt + # scriv # sphinx # twine -requests-toolbelt==0.9.1 \ - --hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f \ - --hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0 +requests-toolbelt==1.0.0 # via twine -rfc3986==2.0.0 \ - --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ - --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c +rfc3986==2.0.0 # via twine -rich==12.5.1 \ - --hash=sha256:2eb4e6894cde1e017976d2975ac210ef515d7548bc595ba20e195fb9628acdeb \ - --hash=sha256:63a5c5ce3673d3d5fbbf23cd87e11ab84b6b451436f1b7f19ec54b6bc36ed7ca +rich==13.3.5 # via twine -six==1.16.0 \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 +scriv==1.3.1 + # via -r doc/requirements.in +six==1.16.0 # via - # -r requirements/pytest.pip # bleach - # libsass # livereload - # pycontracts - # tox -snowballstemmer==2.2.0 \ - --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ - --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a +snowballstemmer==2.2.0 # via sphinx -sortedcontainers==2.4.0 \ - --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ - --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 - # via - # -r requirements/pytest.pip - # hypothesis -sphinx==5.2.1 \ - --hash=sha256:3dcf00fcf82cf91118db9b7177edea4fc01998976f893928d0ab0c58c54be2ca \ - --hash=sha256:c009bb2e9ac5db487bcf53f015504005a330ff7c631bb6ab2604e0d65bae8b54 +sortedcontainers==2.4.0 + # via hypothesis +sphinx==5.3.0 # via # -r doc/requirements.in # sphinx-autobuild # sphinx-rtd-theme + # sphinxcontrib-jquery # sphinxcontrib-restbuilder # sphinxcontrib-spelling -sphinx-autobuild==2021.3.14 \ - --hash=sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac \ - --hash=sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05 +sphinx-autobuild==2021.3.14 # via -r doc/requirements.in -sphinx-rtd-theme==1.0.0 \ - --hash=sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8 \ - --hash=sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c +sphinx-rtd-theme==1.2.1 # via -r doc/requirements.in -sphinxcontrib-applehelp==1.0.2 \ - --hash=sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a \ - --hash=sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58 +sphinxcontrib-applehelp==1.0.2 # via sphinx -sphinxcontrib-devhelp==1.0.2 \ - --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \ - --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4 +sphinxcontrib-devhelp==1.0.2 # via sphinx -sphinxcontrib-htmlhelp==2.0.0 \ - --hash=sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07 \ - --hash=sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2 +sphinxcontrib-htmlhelp==2.0.0 # via sphinx -sphinxcontrib-jsmath==1.0.1 \ - --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ - --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 +sphinxcontrib-jquery==4.1 + # via sphinx-rtd-theme +sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.3 \ - --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \ - --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6 +sphinxcontrib-qthelp==1.0.3 # via sphinx -sphinxcontrib-restbuilder==0.3 \ - --hash=sha256:6b3ee9394b5ec5e73e6afb34d223530d0b9098cb7562f9c5e364e6d6b41410ce \ - --hash=sha256:6ba2ddc7a87d845c075c1b2e00d541bd1c8400488e50e32c9b4169ccdd9f30cb +sphinxcontrib-restbuilder==0.3 # via -r doc/requirements.in -sphinxcontrib-serializinghtml==1.1.5 \ - --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \ - --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 +sphinxcontrib-serializinghtml==1.1.5 # via sphinx -sphinxcontrib-spelling==7.6.0 \ - --hash=sha256:292cd7e1f73a763451693b4d48c9bded151084f6a91e5337733e9fa8715d20ec \ - --hash=sha256:6c1313618412511109f7b76029fbd60df5aa4acf67a2dc9cd1b1016d15e882ff +sphinxcontrib-spelling==8.0.0 # via -r doc/requirements.in -tomli==2.0.1 \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f +tomli==2.0.1 # via - # -r requirements/pytest.pip # build # check-manifest - # pep517 # pylint + # pyproject-api + # pyproject-hooks # pytest # tox -tomlkit==0.11.4 \ - --hash=sha256:25d4e2e446c453be6360c67ddfb88838cfc42026322770ba13d1fbd403a93a5c \ - --hash=sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83 +tomlkit==0.11.8 # via pylint -tornado==6.2 \ - --hash=sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca \ - --hash=sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72 \ - --hash=sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23 \ - --hash=sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8 \ - --hash=sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b \ - --hash=sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9 \ - --hash=sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13 \ - --hash=sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75 \ - --hash=sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac \ - --hash=sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e \ - --hash=sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b +tornado==6.2 # via livereload -tox==3.26.0 \ - --hash=sha256:44f3c347c68c2c68799d7d44f1808f9d396fc8a1a500cbc624253375c7ae107e \ - --hash=sha256:bf037662d7c740d15c9924ba23bb3e587df20598697bb985ac2b49bdc2d847f6 - # via -r requirements/dev.in -twine==4.0.1 \ - --hash=sha256:42026c18e394eac3e06693ee52010baa5313e4811d5a11050e7d48436cf41b9e \ - --hash=sha256:96b1cf12f7ae611a4a40b6ae8e9570215daff0611828f5fe1f37a16255ab24a0 +tox==4.5.2 + # via + # -r requirements/tox.in + # tox-gh +tox-gh==1.0.0 + # via -r requirements/tox.in +twine==4.0.2 # via -r requirements/dev.in -typed-ast==1.5.4 \ - --hash=sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2 \ - --hash=sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1 \ - --hash=sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6 \ - --hash=sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62 \ - --hash=sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac \ - --hash=sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d \ - --hash=sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc \ - --hash=sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2 \ - --hash=sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97 \ - --hash=sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35 \ - --hash=sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6 \ - --hash=sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1 \ - --hash=sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4 \ - --hash=sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c \ - --hash=sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e \ - --hash=sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec \ - --hash=sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f \ - --hash=sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72 \ - --hash=sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47 \ - --hash=sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72 \ - --hash=sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe \ - --hash=sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6 \ - --hash=sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3 \ - --hash=sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66 +typed-ast==1.5.4 # via astroid -typing-extensions==4.3.0 \ - --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \ - --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6 +typing-extensions==4.6.2 # via - # -r requirements/pip.pip - # -r requirements/pytest.pip # astroid # importlib-metadata + # markdown-it-py + # platformdirs # pylint # rich -urllib3==1.26.12 \ - --hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \ - --hash=sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997 + # tox +urllib3==2.0.2 # via # requests # twine -urwid==2.1.2 \ - --hash=sha256:588bee9c1cb208d0906a9f73c613d2bd32c3ed3702012f51efe318a3f2127eae +urwid==2.1.2 # via # pudb # urwid-readline -urwid-readline==0.13 \ - --hash=sha256:018020cbc864bb5ed87be17dc26b069eae2755cb29f3a9c569aac3bded1efaf4 +urwid-readline==0.13 # via pudb -virtualenv==20.16.5 \ - --hash=sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da \ - --hash=sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27 +virtualenv==20.23.0 # via - # -r requirements/pip.pip + # -r requirements/pip.in # tox -webencodings==0.5.1 \ - --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ - --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 +webencodings==0.5.1 # via bleach -wrapt==1.14.1 \ - --hash=sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3 \ - --hash=sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b \ - --hash=sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4 \ - --hash=sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2 \ - --hash=sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656 \ - --hash=sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3 \ - --hash=sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff \ - --hash=sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310 \ - --hash=sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a \ - --hash=sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57 \ - --hash=sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069 \ - --hash=sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383 \ - --hash=sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe \ - --hash=sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87 \ - --hash=sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d \ - --hash=sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b \ - --hash=sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907 \ - --hash=sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f \ - --hash=sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0 \ - --hash=sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28 \ - --hash=sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1 \ - --hash=sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853 \ - --hash=sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc \ - --hash=sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3 \ - --hash=sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3 \ - --hash=sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164 \ - --hash=sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1 \ - --hash=sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c \ - --hash=sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1 \ - --hash=sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7 \ - --hash=sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1 \ - --hash=sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320 \ - --hash=sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed \ - --hash=sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1 \ - --hash=sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248 \ - --hash=sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c \ - --hash=sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456 \ - --hash=sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77 \ - --hash=sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef \ - --hash=sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1 \ - --hash=sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7 \ - --hash=sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86 \ - --hash=sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4 \ - --hash=sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d \ - --hash=sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d \ - --hash=sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8 \ - --hash=sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5 \ - --hash=sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471 \ - --hash=sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00 \ - --hash=sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68 \ - --hash=sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3 \ - --hash=sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d \ - --hash=sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735 \ - --hash=sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d \ - --hash=sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569 \ - --hash=sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7 \ - --hash=sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59 \ - --hash=sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5 \ - --hash=sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb \ - --hash=sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b \ - --hash=sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f \ - --hash=sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462 \ - --hash=sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015 \ - --hash=sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af +wrapt==1.15.0 # via astroid -zipp==3.8.1 \ - --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \ - --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009 +zipp==3.15.0 # via - # -r requirements/pip.pip - # -r requirements/pytest.pip # importlib-metadata - # pep517 + # importlib-resources # The following packages are considered to be unsafe in a requirements file: -pip==22.0.4 \ - --hash=sha256:b3a9de2c6ef801e9247d1527a4b16f92f2cc141cd1489f3fffaf6a9e96729764 \ - --hash=sha256:c6aca0f2f081363f689f041d90dab2a07a9a07fb840284db2218117a52da800b - # via - # -c doc/../requirements/pins.pip - # -c requirements/pins.pip - # -r requirements/pip.pip -setuptools==65.4.0 \ - --hash=sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9 \ - --hash=sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1 - # via check-manifest +pip==23.1.2 + # via -r requirements/pip.in +setuptools==67.8.0 + # via + # -r requirements/pip.in + # check-manifest diff -Nru python-coverage-6.5.0+dfsg1/requirements/mypy.in python-coverage-7.2.7+dfsg1/requirements/mypy.in --- python-coverage-6.5.0+dfsg1/requirements/mypy.in 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/requirements/mypy.in 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,9 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +-c pins.pip + +# So that we have pytest types. +-r pytest.in + +mypy diff -Nru python-coverage-6.5.0+dfsg1/requirements/mypy.pip python-coverage-7.2.7+dfsg1/requirements/mypy.pip --- python-coverage-6.5.0+dfsg1/requirements/mypy.pip 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/requirements/mypy.pip 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,55 @@ +# +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: +# +# make upgrade +# +attrs==23.1.0 + # via hypothesis +colorama==0.4.6 + # via -r requirements/pytest.in +exceptiongroup==1.1.1 + # via + # hypothesis + # pytest +execnet==1.9.0 + # via pytest-xdist +flaky==3.7.0 + # via -r requirements/pytest.in +hypothesis==6.75.6 + # via -r requirements/pytest.in +importlib-metadata==6.6.0 + # via + # attrs + # pluggy + # pytest +iniconfig==2.0.0 + # via pytest +mypy==1.3.0 + # via -r requirements/mypy.in +mypy-extensions==1.0.0 + # via mypy +packaging==23.1 + # via pytest +pluggy==1.0.0 + # via pytest +pytest==7.3.1 + # via + # -r requirements/pytest.in + # pytest-xdist +pytest-xdist==3.3.1 + # via -r requirements/pytest.in +sortedcontainers==2.4.0 + # via hypothesis +tomli==2.0.1 + # via + # mypy + # pytest +typed-ast==1.5.4 + # via mypy +typing-extensions==4.6.2 + # via + # importlib-metadata + # mypy +zipp==3.15.0 + # via importlib-metadata diff -Nru python-coverage-6.5.0+dfsg1/requirements/pins.pip python-coverage-7.2.7+dfsg1/requirements/pins.pip --- python-coverage-6.5.0+dfsg1/requirements/pins.pip 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/requirements/pins.pip 2023-05-29 19:46:30.000000000 +0000 @@ -7,10 +7,8 @@ # but have different pins. This seems to satisfy them all: #docutils>=0.17,<0.18 -# https://github.com/jazzband/pip-tools/issues/1617 -pip<22.1 - -# requests gets different versions in dev.pip and doc/requirements.pip, not -# sure why, and they then ask for different versions of certifi, and we can't -# install, so pin certifi. -certifi==2022.5.18.1 +# Setuptools became stricter about version number syntax. But it shouldn't be +# checking the Python version like that, should it? +# https://github.com/pypa/packaging/issues/678 +# https://github.com/nedbat/coveragepy/issues/1556 +#setuptools<66.0.0 diff -Nru python-coverage-6.5.0+dfsg1/requirements/pip.in python-coverage-7.2.7+dfsg1/requirements/pip.in --- python-coverage-6.5.0+dfsg1/requirements/pip.in 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/requirements/pip.in 2023-05-29 19:46:30.000000000 +0000 @@ -6,4 +6,5 @@ # "make upgrade" turns this into requirements/pip.pip. pip +setuptools virtualenv diff -Nru python-coverage-6.5.0+dfsg1/requirements/pip.pip python-coverage-7.2.7+dfsg1/requirements/pip.pip --- python-coverage-6.5.0+dfsg1/requirements/pip.pip 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/requirements/pip.pip 2023-05-29 19:46:30.000000000 +0000 @@ -1,42 +1,28 @@ # -# This file is autogenerated by pip-compile with python 3.7 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: # # make upgrade # -distlib==0.3.6 \ - --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \ - --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e +distlib==0.3.6 # via virtualenv -filelock==3.8.0 \ - --hash=sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc \ - --hash=sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4 +filelock==3.12.0 # via virtualenv -importlib-metadata==4.12.0 \ - --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \ - --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23 +importlib-metadata==6.6.0 # via virtualenv -platformdirs==2.5.2 \ - --hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \ - --hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19 +platformdirs==3.5.1 # via virtualenv -typing-extensions==4.3.0 \ - --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \ - --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6 - # via importlib-metadata -virtualenv==20.16.5 \ - --hash=sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da \ - --hash=sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27 +typing-extensions==4.6.2 + # via + # importlib-metadata + # platformdirs +virtualenv==20.23.0 # via -r requirements/pip.in -zipp==3.8.1 \ - --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \ - --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009 +zipp==3.15.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -pip==22.0.4 \ - --hash=sha256:b3a9de2c6ef801e9247d1527a4b16f92f2cc141cd1489f3fffaf6a9e96729764 \ - --hash=sha256:c6aca0f2f081363f689f041d90dab2a07a9a07fb840284db2218117a52da800b - # via - # -c requirements/pins.pip - # -r requirements/pip.in +pip==23.1.2 + # via -r requirements/pip.in +setuptools==67.8.0 + # via -r requirements/pip.in diff -Nru python-coverage-6.5.0+dfsg1/requirements/pip-tools.pip python-coverage-7.2.7+dfsg1/requirements/pip-tools.pip --- python-coverage-6.5.0+dfsg1/requirements/pip-tools.pip 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/requirements/pip-tools.pip 2023-05-29 19:46:30.000000000 +0000 @@ -1,69 +1,36 @@ # -# This file is autogenerated by pip-compile with python 3.7 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: # # make upgrade # -build==0.8.0 \ - --hash=sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437 \ - --hash=sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0 +build==0.10.0 # via pip-tools -click==8.1.3 \ - --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \ - --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48 +click==8.1.3 # via pip-tools -importlib-metadata==4.12.0 \ - --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \ - --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23 +importlib-metadata==6.6.0 # via # build # click - # pep517 -packaging==21.3 \ - --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ - --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 +packaging==23.1 # via build -pep517==0.13.0 \ - --hash=sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b \ - --hash=sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59 - # via build -pip-tools==6.8.0 \ - --hash=sha256:39e8aee465446e02278d80dbebd4325d1dd8633248f43213c73a25f58e7d8a55 \ - --hash=sha256:3e5cd4acbf383d19bdfdeab04738b6313ebf4ad22ce49bf529c729061eabfab8 +pip-tools==6.13.0 # via -r requirements/pip-tools.in -pyparsing==3.0.9 \ - --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \ - --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc - # via packaging -tomli==2.0.1 \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f +pyproject-hooks==1.0.0 + # via build +tomli==2.0.1 # via # build - # pep517 -typing-extensions==4.3.0 \ - --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \ - --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6 + # pyproject-hooks +typing-extensions==4.6.2 # via importlib-metadata -wheel==0.37.1 \ - --hash=sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a \ - --hash=sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4 +wheel==0.40.0 # via pip-tools -zipp==3.8.1 \ - --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \ - --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009 - # via - # importlib-metadata - # pep517 +zipp==3.15.0 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -pip==22.0.4 \ - --hash=sha256:b3a9de2c6ef801e9247d1527a4b16f92f2cc141cd1489f3fffaf6a9e96729764 \ - --hash=sha256:c6aca0f2f081363f689f041d90dab2a07a9a07fb840284db2218117a52da800b - # via - # -c requirements/pins.pip - # pip-tools -setuptools==65.4.0 \ - --hash=sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9 \ - --hash=sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1 +pip==23.1.2 + # via pip-tools +setuptools==67.8.0 # via pip-tools diff -Nru python-coverage-6.5.0+dfsg1/requirements/pytest.in python-coverage-7.2.7+dfsg1/requirements/pytest.in --- python-coverage-6.5.0+dfsg1/requirements/pytest.in 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/requirements/pytest.in 2023-05-29 19:46:30.000000000 +0000 @@ -10,19 +10,9 @@ hypothesis pytest pytest-xdist -# Use a fork of PyContracts that supports Python 3.9 -#PyContracts==1.8.12 -# git+https://github.com/slorg1/contracts@collections_and_validator -https://github.com/slorg1/contracts/archive/c5a6da27d4dc9985f68e574d20d86000880919c3.zip # Pytest has a windows-only dependency on colorama: # https://github.com/pytest-dev/pytest/blob/main/setup.cfg#L49 # colorama;sys_platform=="win32" # We copy it here so it can get pinned. colorama - -# Pytest has a windows-only dependency on atomicwrites: -# https://github.com/pytest-dev/pytest/blob/7.1.2/setup.cfg#L50 -# atomicwrites>=1.0;sys_platform=="win32" -# though it's been removed on main. -atomicwrites>=1.0 diff -Nru python-coverage-6.5.0+dfsg1/requirements/pytest.pip python-coverage-7.2.7+dfsg1/requirements/pytest.pip --- python-coverage-6.5.0+dfsg1/requirements/pytest.pip 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/requirements/pytest.pip 2023-05-29 19:46:30.000000000 +0000 @@ -1,117 +1,45 @@ # -# This file is autogenerated by pip-compile with python 3.7 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: # # make upgrade # -atomicwrites==1.4.1 \ - --hash=sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11 +attrs==23.1.0 + # via hypothesis +colorama==0.4.6 # via -r requirements/pytest.in -attrs==22.1.0 \ - --hash=sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6 \ - --hash=sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c +exceptiongroup==1.1.1 # via # hypothesis # pytest -backports-functools-lru-cache==1.6.4 \ - --hash=sha256:d5ed2169378b67d3c545e5600d363a923b09c456dab1593914935a68ad478271 \ - --hash=sha256:dbead04b9daa817909ec64e8d2855fb78feafe0b901d4568758e3a60559d8978 - # via pycontracts -colorama==0.4.5 \ - --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \ - --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4 - # via -r requirements/pytest.in -decorator==5.1.1 \ - --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \ - --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186 - # via pycontracts -exceptiongroup==1.0.0rc9 \ - --hash=sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337 \ - --hash=sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96 - # via hypothesis -execnet==1.9.0 \ - --hash=sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5 \ - --hash=sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142 +execnet==1.9.0 # via pytest-xdist -flaky==3.7.0 \ - --hash=sha256:3ad100780721a1911f57a165809b7ea265a7863305acb66708220820caf8aa0d \ - --hash=sha256:d6eda73cab5ae7364504b7c44670f70abed9e75f77dd116352f662817592ec9c +flaky==3.7.0 # via -r requirements/pytest.in -future==0.18.2 \ - --hash=sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d - # via pycontracts -hypothesis==6.54.6 \ - --hash=sha256:2d5e2d5ccd0efce4e0968a6164f4e4853f808e33f4d91490c975c98beec0c7c3 \ - --hash=sha256:e44833325f9a55f795596ceefd7ede7d626cfe45836025d2647cccaff7070e10 +hypothesis==6.75.6 # via -r requirements/pytest.in -importlib-metadata==4.12.0 \ - --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \ - --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23 +importlib-metadata==6.6.0 # via + # attrs # pluggy # pytest -iniconfig==1.1.1 \ - --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ - --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 +iniconfig==2.0.0 # via pytest -packaging==21.3 \ - --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ - --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 +packaging==23.1 # via pytest -pluggy==1.0.0 \ - --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \ - --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 +pluggy==1.0.0 # via pytest -py==1.11.0 \ - --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ - --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 - # via - # pytest - # pytest-forked -pycontracts @ https://github.com/slorg1/contracts/archive/c5a6da27d4dc9985f68e574d20d86000880919c3.zip \ - --hash=sha256:2b889cbfb03b43dc811b5879248ac5c7e209ece78f03be9633de76a6b21a5a89 - # via -r requirements/pytest.in -pyparsing==3.0.9 \ - --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \ - --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc - # via - # packaging - # pycontracts -pytest==7.1.3 \ - --hash=sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7 \ - --hash=sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39 +pytest==7.3.1 # via # -r requirements/pytest.in - # pytest-forked # pytest-xdist -pytest-forked==1.4.0 \ - --hash=sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e \ - --hash=sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8 - # via pytest-xdist -pytest-xdist==2.5.0 \ - --hash=sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf \ - --hash=sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65 +pytest-xdist==3.3.1 # via -r requirements/pytest.in -qualname==0.1.0 \ - --hash=sha256:277cf6aa4b2ad36beed1153cfa7bf521b210d54fbecb3d8eea0c5679cecc9ed8 - # via pycontracts -six==1.16.0 \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 - # via pycontracts -sortedcontainers==2.4.0 \ - --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ - --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 +sortedcontainers==2.4.0 # via hypothesis -tomli==2.0.1 \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f +tomli==2.0.1 # via pytest -typing-extensions==4.3.0 \ - --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \ - --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6 +typing-extensions==4.6.2 # via importlib-metadata -zipp==3.8.1 \ - --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \ - --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009 +zipp==3.15.0 # via importlib-metadata diff -Nru python-coverage-6.5.0+dfsg1/requirements/tox.in python-coverage-7.2.7+dfsg1/requirements/tox.in --- python-coverage-6.5.0+dfsg1/requirements/tox.in 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/requirements/tox.in 2023-05-29 19:46:30.000000000 +0000 @@ -7,7 +7,7 @@ # "make upgrade" turns this into requirements/tox.pip. tox -tox-gh-actions +tox-gh # Tox has a windows-only dependency on colorama: # https://github.com/tox-dev/tox/blob/master/setup.cfg#L44 diff -Nru python-coverage-6.5.0+dfsg1/requirements/tox.pip python-coverage-7.2.7+dfsg1/requirements/tox.pip --- python-coverage-6.5.0+dfsg1/requirements/tox.pip 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/requirements/tox.pip 2023-05-29 19:46:30.000000000 +0000 @@ -1,83 +1,56 @@ # -# This file is autogenerated by pip-compile with python 3.7 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: # # make upgrade # -colorama==0.4.5 \ - --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \ - --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4 - # via -r requirements/tox.in -distlib==0.3.6 \ - --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \ - --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e +cachetools==5.3.1 + # via tox +chardet==5.1.0 + # via tox +colorama==0.4.6 + # via + # -r requirements/tox.in + # tox +distlib==0.3.6 # via virtualenv -filelock==3.8.0 \ - --hash=sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc \ - --hash=sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4 +filelock==3.12.0 # via # tox # virtualenv -importlib-metadata==4.12.0 \ - --hash=sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670 \ - --hash=sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23 +importlib-metadata==6.6.0 # via # pluggy # tox # virtualenv -importlib-resources==5.9.0 \ - --hash=sha256:5481e97fb45af8dcf2f798952625591c58fe599d0735d86b10f54de086a61681 \ - --hash=sha256:f78a8df21a79bcc30cfd400bdc38f314333de7c0fb619763f6b9dabab8268bb7 - # via tox-gh-actions -packaging==21.3 \ - --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \ - --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 - # via tox -platformdirs==2.5.2 \ - --hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \ - --hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19 - # via virtualenv -pluggy==1.0.0 \ - --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \ - --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 - # via tox -py==1.11.0 \ - --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ - --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 - # via tox -pyparsing==3.0.9 \ - --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \ - --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc - # via packaging -six==1.16.0 \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 +packaging==23.1 + # via + # pyproject-api + # tox +platformdirs==3.5.1 + # via + # tox + # virtualenv +pluggy==1.0.0 # via tox -tomli==2.0.1 \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f +pyproject-api==1.5.1 # via tox -tox==3.26.0 \ - --hash=sha256:44f3c347c68c2c68799d7d44f1808f9d396fc8a1a500cbc624253375c7ae107e \ - --hash=sha256:bf037662d7c740d15c9924ba23bb3e587df20598697bb985ac2b49bdc2d847f6 +tomli==2.0.1 + # via + # pyproject-api + # tox +tox==4.5.2 # via # -r requirements/tox.in - # tox-gh-actions -tox-gh-actions==2.10.0 \ - --hash=sha256:4a21e70d799736016cf75c3415f5991008cf81fa6ee1c047d1be24900cec31bb \ - --hash=sha256:acb64641f09022581040d92c28d6c06d261a829c008575892fd458e23e638971 + # tox-gh +tox-gh==1.0.0 # via -r requirements/tox.in -typing-extensions==4.3.0 \ - --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \ - --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6 - # via importlib-metadata -virtualenv==20.16.5 \ - --hash=sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da \ - --hash=sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27 - # via tox -zipp==3.8.1 \ - --hash=sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2 \ - --hash=sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009 +typing-extensions==4.6.2 # via # importlib-metadata - # importlib-resources + # platformdirs + # tox +virtualenv==20.23.0 + # via tox +zipp==3.15.0 + # via importlib-metadata diff -Nru python-coverage-6.5.0+dfsg1/setup.cfg python-coverage-7.2.7+dfsg1/setup.cfg --- python-coverage-6.5.0+dfsg1/setup.cfg 2022-09-29 16:36:48.202740000 +0000 +++ python-coverage-7.2.7+dfsg1/setup.cfg 2023-05-29 19:46:41.742986000 +0000 @@ -1,25 +1,3 @@ -[tool:pytest] -addopts = -q -n auto --strict-markers --no-flaky-report -rfEX --failed-first -python_classes = *Test -markers = - expensive: too slow to run during "make smoke" -filterwarnings = - ignore:the imp module is deprecated in favour of importlib:DeprecationWarning - ignore:distutils Version classes are deprecated:DeprecationWarning - ignore:The distutils package is deprecated and slated for removal in Python 3.12:DeprecationWarning -xfail_strict = true -balanced_clumps = - VirtualenvTest - CompareTest - GetZipBytesTest - -[pep8] -ignore = E265,E266,E123,E133,E226,E241,E242,E301,E401 -max-line-length = 100 - -[metadata] -license_files = LICENSE.txt - [egg_info] tag_build = tag_date = 0 diff -Nru python-coverage-6.5.0+dfsg1/setup.py python-coverage-7.2.7+dfsg1/setup.py --- python-coverage-6.5.0+dfsg1/setup.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/setup.py 2023-05-29 19:46:30.000000000 +0000 @@ -14,20 +14,6 @@ from distutils.core import Extension # pylint: disable=wrong-import-order from setuptools.command.build_ext import build_ext # pylint: disable=wrong-import-order from distutils import errors # pylint: disable=wrong-import-order -import distutils.log # pylint: disable=wrong-import-order - -# $set_env.py: COVERAGE_QUIETER - Set to remove some noise from test output. -if bool(int(os.getenv("COVERAGE_QUIETER", "0"))): - # Distutils has its own mini-logging code, and it sets the level too high. - # When I ask for --quiet when running tests, I don't want to see warnings. - old_set_verbosity = distutils.log.set_verbosity - def better_set_verbosity(v): - """--quiet means no warnings!""" - if v <= 0: - distutils.log.set_threshold(distutils.log.ERROR) - else: - old_set_verbosity(v) - distutils.log.set_verbosity = better_set_verbosity # Get or massage our metadata. We exec coverage/version.py so we can avoid # importing the product code into setup.py. @@ -45,6 +31,7 @@ Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 +Programming Language :: Python :: 3.12 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Quality Assurance @@ -58,10 +45,19 @@ # Keep pylint happy. __version__ = __url__ = version_info = "" # Execute the code in version.py. - exec(compile(version_file.read(), cov_ver_py, 'exec')) + exec(compile(version_file.read(), cov_ver_py, 'exec', dont_inherit=True)) with open("README.rst") as readme: - long_description = readme.read().replace("https://coverage.readthedocs.io", __url__) + readme_text = readme.read() + +temp_url = __url__.replace("readthedocs", "@@") +assert "@@" not in readme_text +long_description = ( + readme_text + .replace("https://coverage.readthedocs.io/en/latest", temp_url) + .replace("https://coverage.readthedocs.io", temp_url) + .replace("@@", "readthedocs") +) with open("CONTRIBUTORS.txt", "rb") as contributors: paras = contributors.read().split(b"\n\n") @@ -93,6 +89,7 @@ 'coverage': [ 'htmlfiles/*.*', 'fullcoverage/*.*', + 'py.typed', ] }, @@ -120,7 +117,8 @@ long_description=long_description, long_description_content_type='text/x-rst', keywords='code coverage testing', - license='Apache 2.0', + license='Apache-2.0', + license_files=["LICENSE.txt"], classifiers=classifier_list, url="https://github.com/nedbat/coveragepy", project_urls={ @@ -130,7 +128,8 @@ '?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=pypi' ), 'Issues': 'https://github.com/nedbat/coveragepy/issues', - 'Twitter': 'https://twitter.com/coveragepy', + 'Mastodon': 'https://hachyderm.io/@coveragepy', + 'Mastodon (nedbat)': 'https://hachyderm.io/@nedbat', }, python_requires=">=3.7", # minimum of PYVERSIONS ) @@ -185,10 +184,6 @@ compile_extension = True -if sys.platform.startswith('java'): - # Jython can't compile C extensions - compile_extension = False - if '__pypy__' in sys.builtin_module_names: # Pypy can't compile C extensions compile_extension = False diff -Nru python-coverage-6.5.0+dfsg1/tests/balance_xdist_plugin.py python-coverage-7.2.7+dfsg1/tests/balance_xdist_plugin.py --- python-coverage-6.5.0+dfsg1/tests/balance_xdist_plugin.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/balance_xdist_plugin.py 2023-05-29 19:46:30.000000000 +0000 @@ -29,6 +29,7 @@ import os import shutil import time + from pathlib import Path import pytest @@ -64,7 +65,7 @@ if not self.running_all: return - tests_csv_dir = Path(session.startdir).resolve() / "tmp/tests_csv" + tests_csv_dir = session.startpath.resolve() / "tmp/tests_csv" self.tests_csv = tests_csv_dir / f"{self.worker}.csv" if self.worker == "none": @@ -104,7 +105,7 @@ yield self.write_duration_row(item, "teardown", time.time() - start) - @pytest.mark.trylast + @pytest.hookimpl(trylast=True) def pytest_xdist_make_scheduler(self, config, log): """Create our BalancedScheduler using time data from the last run.""" # Assign tests to chunks diff -Nru python-coverage-6.5.0+dfsg1/tests/conftest.py python-coverage-7.2.7+dfsg1/tests/conftest.py --- python-coverage-6.5.0+dfsg1/tests/conftest.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/conftest.py 2023-05-29 19:46:30.000000000 +0000 @@ -7,16 +7,19 @@ This module is run automatically by pytest, to define and enable fixtures. """ +from __future__ import annotations + import os import sys import sysconfig import warnings + from pathlib import Path +from typing import Iterator, Optional import pytest from coverage import env -from coverage.exceptions import _StopEverything from coverage.files import set_relative_directory # Pytest will rewrite assertions in test modules, but not elsewhere. @@ -31,13 +34,13 @@ @pytest.fixture(autouse=True) -def set_warnings(): +def set_warnings() -> None: """Configure warnings to show while running tests.""" warnings.simplefilter("default") warnings.simplefilter("once", DeprecationWarning) # Warnings to suppress: - # How come these warnings are successfully suppressed here, but not in setup.cfg?? + # How come these warnings are successfully suppressed here, but not in pyproject.toml?? warnings.filterwarnings( "ignore", @@ -62,7 +65,7 @@ @pytest.fixture(autouse=True) -def reset_sys_path(): +def reset_sys_path() -> Iterator[None]: """Clean up sys.path changes around every test.""" sys_path = list(sys.path) yield @@ -70,7 +73,7 @@ @pytest.fixture(autouse=True) -def reset_environment(): +def reset_environment() -> Iterator[None]: """Make sure a test setting an envvar doesn't leak into another test.""" old_environ = os.environ.copy() yield @@ -79,14 +82,14 @@ @pytest.fixture(autouse=True) -def reset_filesdotpy_globals(): +def reset_filesdotpy_globals() -> Iterator[None]: """coverage/files.py has some unfortunate globals. Reset them every test.""" set_relative_directory() yield WORKER = os.environ.get("PYTEST_XDIST_WORKER", "none") -def pytest_sessionstart(): +def pytest_sessionstart() -> None: """Run once at the start of the test session.""" # Only in the main process... if WORKER == "none": @@ -97,7 +100,7 @@ # subcover.pth is deleted by pytest_sessionfinish below. -def pytest_sessionfinish(): +def pytest_sessionfinish() -> None: """Hook the end of a test session, to clean up.""" # This is called by each of the workers and by the main process. if WORKER == "none": @@ -106,16 +109,8 @@ if pth_file.exists(): pth_file.unlink() -@pytest.hookimpl(hookwrapper=True) -def pytest_runtest_call(item): - """Run once for each test.""" - # Convert _StopEverything into skipped tests. - outcome = yield - if outcome.excinfo and issubclass(outcome.excinfo[0], _StopEverything): # pragma: only jython - pytest.skip(f"Skipping {item.nodeid} for _StopEverything: {outcome.excinfo[1]}") - -def possible_pth_dirs(): +def possible_pth_dirs() -> Iterator[Path]: """Produce a sequence of directories for trying to write .pth files.""" # First look through sys.path, and if we find a .pth file, then it's a good # place to put ours. @@ -129,7 +124,7 @@ yield Path(sysconfig.get_path("purelib")) # pragma: cant happen -def find_writable_pth_directory(): +def find_writable_pth_directory() -> Optional[Path]: """Find a place to write a .pth file.""" for pth_dir in possible_pth_dirs(): # pragma: part covered try_it = pth_dir / f"touch_{WORKER}.it" diff -Nru python-coverage-6.5.0+dfsg1/tests/coveragetest.py python-coverage-7.2.7+dfsg1/tests/coveragetest.py --- python-coverage-6.5.0+dfsg1/tests/coveragetest.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/coveragetest.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Base test case class for coverage.py testing.""" +from __future__ import annotations + import contextlib import datetime import difflib @@ -15,12 +17,18 @@ import shlex import sys -import pytest +from types import ModuleType +from typing import ( + Any, Collection, Dict, Iterable, Iterator, List, Mapping, Optional, + Sequence, Tuple, Union, +) import coverage -from coverage import env +from coverage import Coverage from coverage.cmdline import CoverageScript +from coverage.data import CoverageData from coverage.misc import import_local_file +from coverage.types import TArc, TLineNo from tests.helpers import arcs_to_arcz_repr, arcz_to_arcs, assert_count_equal from tests.helpers import nice_file, run_command @@ -56,15 +64,20 @@ # Let stderr go to stderr, pytest will capture it for us. show_stderr = True - def setUp(self): + def setUp(self) -> None: super().setUp() # Attributes for getting info about what happened. - self.last_command_status = None - self.last_command_output = None - self.last_module_name = None - - def start_import_stop(self, cov, modname, modfile=None): + self.last_command_status: Optional[int] = None + self.last_command_output: Optional[str] = None + self.last_module_name: Optional[str] = None + + def start_import_stop( + self, + cov: Coverage, + modname: str, + modfile: Optional[str] = None + ) -> ModuleType: """Start coverage, import a file, then stop coverage. `cov` is started and stopped, with an `import_local_file` of @@ -83,22 +96,28 @@ cov.stop() return mod - def get_report(self, cov, squeeze=True, **kwargs): + def get_report(self, cov: Coverage, squeeze: bool = True, **kwargs: Any) -> str: """Get the report from `cov`, and canonicalize it.""" repout = io.StringIO() kwargs.setdefault("show_missing", False) cov.report(file=repout, **kwargs) report = repout.getvalue().replace('\\', '/') + print(report) # When tests fail, it's helpful to see the output if squeeze: report = re.sub(r" +", " ", report) return report - def get_module_name(self): + def get_module_name(self) -> str: """Return a random module name to use for this test run.""" self.last_module_name = 'coverage_test_' + str(random.random())[2:] return self.last_module_name - def _check_arcs(self, a1, a2, arc_type): + def _check_arcs( + self, + a1: Optional[Iterable[TArc]], + a2: Optional[Iterable[TArc]], + arc_type: str, + ) -> str: """Check that the arc lists `a1` and `a2` are equal. If they are equal, return empty string. If they are unequal, return @@ -116,11 +135,20 @@ return "" def check_coverage( - self, text, lines=None, missing="", report="", - excludes=None, partials="", - arcz=None, arcz_missing=None, arcz_unpredicted=None, - arcs=None, arcs_missing=None, arcs_unpredicted=None, - ): + self, + text: str, + lines: Optional[Union[Sequence[TLineNo], Sequence[List[TLineNo]]]] = None, + missing: Union[str, Sequence[str]] = "", + report: str = "", + excludes: Optional[Iterable[str]] = None, + partials: Iterable[str] = (), + arcz: Optional[str] = None, + arcz_missing: Optional[str] = None, + arcz_unpredicted: Optional[str] = None, + arcs: Optional[Iterable[TArc]] = None, + arcs_missing: Optional[Iterable[TArc]] = None, + arcs_unpredicted: Optional[Iterable[TArc]] = None, + ) -> Coverage: """Check the coverage measurement of `text`. The source `text` is run and measured. `lines` are the line numbers @@ -174,7 +202,7 @@ if isinstance(lines[0], int): # lines is just a list of numbers, it must match the statements # found in the code. - assert statements == lines, f"{statements!r} != {lines!r}" + assert statements == lines, f"lines: {statements!r} != {lines!r}" else: # lines is a list of possible line number lists, one of them # must match. @@ -186,7 +214,7 @@ missing_formatted = analysis.missing_formatted() if isinstance(missing, str): - msg = f"{missing_formatted!r} != {missing!r}" + msg = f"missing: {missing_formatted!r} != {missing!r}" assert missing_formatted == missing, msg else: for missing_list in missing: @@ -218,18 +246,33 @@ return cov - def make_data_file(self, basename=None, suffix=None, lines=None, file_tracers=None): + def make_data_file( + self, + basename: Optional[str] = None, + suffix: Optional[str] = None, + lines: Optional[Mapping[str, Collection[TLineNo]]] = None, + arcs: Optional[Mapping[str, Collection[TArc]]] = None, + file_tracers: Optional[Mapping[str, str]] = None, + ) -> CoverageData: """Write some data into a coverage data file.""" data = coverage.CoverageData(basename=basename, suffix=suffix) + assert lines is None or arcs is None if lines: data.add_lines(lines) + if arcs: + data.add_arcs(arcs) if file_tracers: data.add_file_tracers(file_tracers) data.write() return data @contextlib.contextmanager - def assert_warnings(self, cov, warnings, not_warnings=()): + def assert_warnings( + self, + cov: Coverage, + warnings: Iterable[str], + not_warnings: Iterable[str] = (), + ) -> Iterator[None]: """A context manager to check that particular warnings happened in `cov`. `cov` is a Coverage instance. `warnings` is a list of regexes. Every @@ -247,7 +290,11 @@ """ __tracebackhide__ = True saved_warnings = [] - def capture_warning(msg, slug=None, once=False): # pylint: disable=unused-argument + def capture_warning( + msg: str, + slug: Optional[str] = None, + once: bool = False, # pylint: disable=unused-argument + ) -> None: """A fake implementation of Coverage._warn, to capture warnings.""" # NOTE: we don't implement `once`. if slug: @@ -255,7 +302,7 @@ saved_warnings.append(msg) original_warn = cov._warn - cov._warn = capture_warning + cov._warn = capture_warning # type: ignore[method-assign] try: yield @@ -280,36 +327,41 @@ if saved_warnings: assert False, f"Unexpected warnings: {saved_warnings!r}" finally: - cov._warn = original_warn + cov._warn = original_warn # type: ignore[method-assign] - def assert_same_files(self, flist1, flist2): + def assert_same_files(self, flist1: Iterable[str], flist2: Iterable[str]) -> None: """Assert that `flist1` and `flist2` are the same set of file names.""" flist1_nice = [nice_file(f) for f in flist1] flist2_nice = [nice_file(f) for f in flist2] assert_count_equal(flist1_nice, flist2_nice) - def assert_exists(self, fname): + def assert_exists(self, fname: str) -> None: """Assert that `fname` is a file that exists.""" assert os.path.exists(fname), f"File {fname!r} should exist" - def assert_doesnt_exist(self, fname): + def assert_doesnt_exist(self, fname: str) -> None: """Assert that `fname` is a file that doesn't exist.""" assert not os.path.exists(fname), f"File {fname!r} shouldn't exist" - def assert_file_count(self, pattern, count): + def assert_file_count(self, pattern: str, count: int) -> None: """Assert that there are `count` files matching `pattern`.""" files = sorted(glob.glob(pattern)) msg = "There should be {} files matching {!r}, but there are these: {}" msg = msg.format(count, pattern, files) assert len(files) == count, msg - def assert_recent_datetime(self, dt, seconds=10, msg=None): + def assert_recent_datetime( + self, + dt: datetime.datetime, + seconds: int = 10, + msg: Optional[str] = None, + ) -> None: """Assert that `dt` marks a time at most `seconds` seconds ago.""" age = datetime.datetime.now() - dt assert age.total_seconds() >= 0, msg assert age.total_seconds() <= seconds, msg - def command_line(self, args, ret=OK): + def command_line(self, args: str, ret: int = OK) -> None: """Run `args` through the command line. Use this when you want to run the full coverage machinery, but in the @@ -330,7 +382,7 @@ # https://salsa.debian.org/debian/pkg-python-coverage/-/blob/master/debian/patches/02.rename-public-programs.patch coverage_command = "coverage" - def run_command(self, cmd): + def run_command(self, cmd: str) -> str: """Run the command-line `cmd` in a sub-process. `cmd` is the command line to invoke in a sub-process. Returns the @@ -347,7 +399,7 @@ _, output = self.run_command_status(cmd) return output - def run_command_status(self, cmd): + def run_command_status(self, cmd: str) -> Tuple[int, str]: """Run the command-line `cmd` in a sub-process, and print its output. Use this when you need to test the process behavior of coverage. @@ -382,18 +434,9 @@ command_words = [os.path.basename(sys.executable)] elif command_name == "coverage": - if env.JYTHON: # pragma: only jython - # Jython can't do reporting, so let's skip the test now. - if command_args and command_args[0] in ('report', 'html', 'xml', 'annotate'): - pytest.skip("Can't run reporting commands in Jython") - # Jython can't run "coverage" as a command because the shebang - # refers to another shebang'd Python script. So run them as - # modules. - command_words = "jython -m coverage".split() - else: - # The invocation requests the coverage.py program. Substitute the - # actual coverage.py main command name. - command_words = [self.coverage_command] + # The invocation requests the coverage.py program. Substitute the + # actual coverage.py main command name. + command_words = [self.coverage_command] else: command_words = [command_name] @@ -403,8 +446,6 @@ # Add our test modules directory to PYTHONPATH. I'm sure there's too # much path munging here, but... pythonpath_name = "PYTHONPATH" - if env.JYTHON: - pythonpath_name = "JYTHONPATH" # pragma: only jython testmods = nice_file(self.working_root(), "tests/modules") zipfile = nice_file(self.working_root(), "tests/zipmods.zip") @@ -418,36 +459,36 @@ print(self.last_command_output) return self.last_command_status, self.last_command_output - def working_root(self): + def working_root(self) -> str: """Where is the root of the coverage.py working tree?""" return os.path.dirname(nice_file(__file__, "..")) - def report_from_command(self, cmd): + def report_from_command(self, cmd: str) -> str: """Return the report from the `cmd`, with some convenience added.""" report = self.run_command(cmd).replace('\\', '/') assert "error" not in report.lower() return report - def report_lines(self, report): + def report_lines(self, report: str) -> List[str]: """Return the lines of the report, as a list.""" lines = report.split('\n') assert lines[-1] == "" return lines[:-1] - def line_count(self, report): + def line_count(self, report: str) -> int: """How many lines are in `report`?""" return len(self.report_lines(report)) - def squeezed_lines(self, report): + def squeezed_lines(self, report: str) -> List[str]: """Return a list of the lines in report, with the spaces squeezed.""" lines = self.report_lines(report) return [re.sub(r"\s+", " ", l.strip()) for l in lines] - def last_line_squeezed(self, report): + def last_line_squeezed(self, report: str) -> str: """Return the last line of `report` with the spaces squeezed down.""" return self.squeezed_lines(report)[-1] - def get_measured_filenames(self, coverage_data): + def get_measured_filenames(self, coverage_data: CoverageData) -> Dict[str, str]: """Get paths to measured files. Returns a dict of {filename: absolute path to file} @@ -456,9 +497,10 @@ return {os.path.basename(filename): filename for filename in coverage_data.measured_files()} - def get_missing_arc_description(self, cov, start, end): + def get_missing_arc_description(self, cov: Coverage, start: TLineNo, end: TLineNo) -> str: """Get the missing-arc description for a line arc in a coverage run.""" # ugh, unexposed methods?? + assert self.last_module_name is not None filename = self.last_module_name + ".py" fr = cov._get_file_reporter(filename) arcs_executed = cov._analyze(filename).arcs_executed() @@ -468,8 +510,8 @@ class UsingModulesMixin: """A mixin for importing modules from tests/modules and tests/moremodules.""" - def setUp(self): - super().setUp() + def setUp(self) -> None: + super().setUp() # type: ignore[misc] # Parent class saves and restores sys.path, we can just modify it. sys.path.append(nice_file(TESTS_DIR, "modules")) @@ -477,7 +519,7 @@ sys.path.append(nice_file(TESTS_DIR, "zipmods.zip")) -def command_line(args): +def command_line(args: str) -> int: """Run `args` through the CoverageScript command line. Returns the return code from CoverageScript.command_line. diff -Nru python-coverage-6.5.0+dfsg1/tests/gold/html/contexts/index.html python-coverage-7.2.7+dfsg1/tests/gold/html/contexts/index.html --- python-coverage-6.5.0+dfsg1/tests/gold/html/contexts/index.html 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/gold/html/contexts/index.html 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,102 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 94% +

+ +
+ +
+

+ coverage.py v7.2.3a0.dev1, + created at 2023-03-21 08:44 -0400 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
two_tests.py171094%
Total171094%
+

+ No items found using the specified filter. +

+
+ + + diff -Nru python-coverage-6.5.0+dfsg1/tests/gold/html/contexts/two_tests_py.html python-coverage-7.2.7+dfsg1/tests/gold/html/contexts/two_tests_py.html --- python-coverage-6.5.0+dfsg1/tests/gold/html/contexts/two_tests_py.html 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/gold/html/contexts/two_tests_py.html 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,126 @@ + + + + + Coverage for two_tests.py: 94% + + + + + + +
+ +
+
+

1def helper(lineno): 

+

2 x = 2 1acb

+

3 

+

4def test_one(): 

+

5 a = 5 1c

+

6 helper(6) 1c

+

7 

+

8def test_two(): 

+

9 a = 9 1b

+

10 b = 10 1b

+

11 if a > 11: 1b

+

12 b = 12 

+

13 assert a == (13-4) 1b

+

14 assert b == (14-4) 1b

+

15 helper( 1b

+

16 16 

+

17 ) 

+

18 

+

19test_one() 

+

20x = 20 

+

21helper(21) 

+

22test_two() 

+
+ + + diff -Nru python-coverage-6.5.0+dfsg1/tests/gold/html/Makefile python-coverage-7.2.7+dfsg1/tests/gold/html/Makefile --- python-coverage-6.5.0+dfsg1/tests/gold/html/Makefile 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/gold/html/Makefile 2023-05-29 19:46:30.000000000 +0000 @@ -17,7 +17,7 @@ clean: ## Remove the effects of this Makefile. @git clean -fq . -update-gold: ## Copy output files from latest tests to gold files. +update-gold: ## Copy actual output files from latest tests to gold files. @for sub in ../../actual/html/*; do \ rsync --verbose --existing --recursive $$sub/ $$(basename $$sub) ; \ done ; \ diff -Nru python-coverage-6.5.0+dfsg1/tests/gold/html/styled/style.css python-coverage-7.2.7+dfsg1/tests/gold/html/styled/style.css --- python-coverage-6.5.0+dfsg1/tests/gold/html/styled/style.css 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/gold/html/styled/style.css 2023-05-29 19:46:30.000000000 +0000 @@ -258,12 +258,10 @@ @media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } -#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; } +#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; text-align: right; } @media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } -#source p .ctxs span { display: block; text-align: right; } - #index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } #index table.index { margin-left: -.5em; } diff -Nru python-coverage-6.5.0+dfsg1/tests/gold/html/support/coverage_html.js python-coverage-7.2.7+dfsg1/tests/gold/html/support/coverage_html.js --- python-coverage-6.5.0+dfsg1/tests/gold/html/support/coverage_html.js 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/gold/html/support/coverage_html.js 2023-05-29 19:46:30.000000000 +0000 @@ -166,7 +166,7 @@ // Trigger change event on setup, to force filter on page refresh // (filter value may still be present). - document.getElementById("filter").dispatchEvent(new Event("change")); + document.getElementById("filter").dispatchEvent(new Event("input")); }; coverage.INDEX_SORT_STORAGE = "COVERAGE_INDEX_SORT_2"; @@ -214,7 +214,7 @@ coverage.pyfile_ready = function () { // If we're directed to a particular line number, highlight the line. var frag = location.hash; - if (frag.length > 2 && frag[1] === 't') { + if (frag.length > 2 && frag[1] === "t") { document.querySelector(frag).closest(".n").classList.add("highlight"); coverage.set_sel(parseInt(frag.substr(2), 10)); } else { @@ -257,6 +257,10 @@ coverage.init_scroll_markers(); coverage.wire_up_sticky_header(); + document.querySelectorAll("[id^=ctxs]").forEach( + cbox => cbox.addEventListener("click", coverage.expand_contexts) + ); + // Rebuild scroll markers when the window height changes. window.addEventListener("resize", coverage.build_scroll_markers); }; @@ -528,14 +532,14 @@ coverage.init_scroll_markers = function () { // Init some variables - coverage.lines_len = document.querySelectorAll('#source > p').length; + coverage.lines_len = document.querySelectorAll("#source > p").length; // Build html coverage.build_scroll_markers(); }; coverage.build_scroll_markers = function () { - const temp_scroll_marker = document.getElementById('scroll_marker') + const temp_scroll_marker = document.getElementById("scroll_marker") if (temp_scroll_marker) temp_scroll_marker.remove(); // Don't build markers if the window has no scroll bar. if (document.body.scrollHeight <= window.innerHeight) { @@ -549,11 +553,11 @@ const scroll_marker = document.createElement("div"); scroll_marker.id = "scroll_marker"; - document.getElementById('source').querySelectorAll( - 'p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par' + document.getElementById("source").querySelectorAll( + "p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par" ).forEach(element => { const line_top = Math.floor(element.offsetTop * marker_scale); - const line_number = parseInt(element.id.substr(1)); + const line_number = parseInt(element.querySelector(".n a").id.substr(1)); if (line_number === previous_line + 1) { // If this solid missed block just make previous mark higher. @@ -577,24 +581,40 @@ }; coverage.wire_up_sticky_header = function () { - const header = document.querySelector('header'); + const header = document.querySelector("header"); const header_bottom = ( - header.querySelector('.content h2').getBoundingClientRect().top - + header.querySelector(".content h2").getBoundingClientRect().top - header.getBoundingClientRect().top ); function updateHeader() { if (window.scrollY > header_bottom) { - header.classList.add('sticky'); + header.classList.add("sticky"); } else { - header.classList.remove('sticky'); + header.classList.remove("sticky"); } } - window.addEventListener('scroll', updateHeader); + window.addEventListener("scroll", updateHeader); updateHeader(); }; +coverage.expand_contexts = function (e) { + var ctxs = e.target.parentNode.querySelector(".ctxs"); + + if (!ctxs.classList.contains("expanded")) { + var ctxs_text = ctxs.textContent; + var width = Number(ctxs_text[0]); + ctxs.textContent = ""; + for (var i = 1; i < ctxs_text.length; i += width) { + key = ctxs_text.substring(i, i + width).trim(); + ctxs.appendChild(document.createTextNode(contexts[key])); + ctxs.appendChild(document.createElement("br")); + } + ctxs.classList.add("expanded"); + } +}; + document.addEventListener("DOMContentLoaded", () => { if (document.body.classList.contains("indexfile")) { coverage.index_ready(); diff -Nru python-coverage-6.5.0+dfsg1/tests/gold/html/support/style.css python-coverage-7.2.7+dfsg1/tests/gold/html/support/style.css --- python-coverage-6.5.0+dfsg1/tests/gold/html/support/style.css 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/gold/html/support/style.css 2023-05-29 19:46:30.000000000 +0000 @@ -258,12 +258,10 @@ @media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } -#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; } +#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; text-align: right; } @media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } -#source p .ctxs span { display: block; text-align: right; } - #index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } #index table.index { margin-left: -.5em; } diff -Nru python-coverage-6.5.0+dfsg1/tests/gold/README.rst python-coverage-7.2.7+dfsg1/tests/gold/README.rst --- python-coverage-6.5.0+dfsg1/tests/gold/README.rst 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/gold/README.rst 2023-05-29 19:46:30.000000000 +0000 @@ -9,16 +9,40 @@ If gold tests are failing, you may need to update the gold files by copying the current output of the tests into the gold files. When a test fails, the actual -output is in the tests/actual directory. Do not commit those files to git. +output is in the tests/actual directory. Those files are ignored by git. -You can run just the failed tests again with:: +There's a Makefile in the html directory for working with gold files and their +associated support files. + +To view the tests/actual files, you need to tentatively copy them to the gold +directories, and then add the supporting files so they can be viewed as +complete output. For example:: + + cp tests/actual/html/contexts/* tests/gold/html/contexts + cd tests/actual/html + make complete + +If the new actual output is correct, you can use "make update-gold" to copy the +actual output as the new gold files. + +If you have changed some of the supporting files (.css or .js), then "make +update-support" will copy the updated files to the tests/gold/html/support +directory for checking test output. + +If you have added a gold test, you'll need to manually copy the tests/actual +files to tests/gold. + +Once you've copied the actual results to the gold files, or to check your work +again, you can run just the failed tests again with:: tox -e py39 -- -n 0 --lf The saved HTML files in the html directories can't be viewed properly without the supporting CSS and Javascript files. But we don't want to save copies of -those files in every subdirectory. There's a Makefile in the html directory -for working with the saved copies of the support files. +those files in every subdirectory. The make target "make complete" in +tests/gold/html will copy the support files so you can open the HTML files to +see how they look. When you are done checking the output, you can use "make +clean" to remove the support files from the gold directories. If the output files are correct, you can update the gold files with "make update-gold". If there are version-specific gold files (for example, diff -Nru python-coverage-6.5.0+dfsg1/tests/goldtest.py python-coverage-7.2.7+dfsg1/tests/goldtest.py --- python-coverage-6.5.0+dfsg1/tests/goldtest.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/goldtest.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """A test base class for tests based on gold file comparison.""" +from __future__ import annotations + import difflib import filecmp import fnmatch @@ -11,19 +13,24 @@ import re import xml.etree.ElementTree +from typing import Iterable, List, Optional, Tuple + from tests.coveragetest import TESTS_DIR from tests.helpers import os_sep -def gold_path(path): +def gold_path(path: str) -> str: """Get a path to a gold file for comparison.""" return os.path.join(TESTS_DIR, "gold", path) def compare( - expected_dir, actual_dir, file_pattern=None, - actual_extra=False, scrubs=None, - ): + expected_dir: str, + actual_dir: str, + file_pattern: Optional[str] = None, + actual_extra: bool = False, + scrubs: Optional[List[Tuple[str, str]]] = None, +) -> None: """Compare files matching `file_pattern` in `expected_dir` and `actual_dir`. `actual_extra` true means `actual_dir` can have extra files in it @@ -39,19 +46,23 @@ """ __tracebackhide__ = True # pytest, please don't show me this function. assert os_sep("/gold/") in expected_dir + assert os.path.exists(actual_dir) + os.makedirs(expected_dir, exist_ok=True) dc = filecmp.dircmp(expected_dir, actual_dir) - diff_files = fnmatch_list(dc.diff_files, file_pattern) - expected_only = fnmatch_list(dc.left_only, file_pattern) - actual_only = fnmatch_list(dc.right_only, file_pattern) + diff_files = _fnmatch_list(dc.diff_files, file_pattern) + expected_only = _fnmatch_list(dc.left_only, file_pattern) + actual_only = _fnmatch_list(dc.right_only, file_pattern) - def save_mismatch(f): + def save_mismatch(f: str) -> None: """Save a mismatched result to tests/actual.""" save_path = expected_dir.replace(os_sep("/gold/"), os_sep("/actual/")) os.makedirs(save_path, exist_ok=True) - with open(os.path.join(save_path, f), "w") as savef: + save_file = os.path.join(save_path, f) + with open(save_file, "w") as savef: with open(os.path.join(actual_dir, f)) as readf: savef.write(readf.read()) + print(os_sep(f"Saved actual output to '{save_file}': see tests/gold/README.rst")) # filecmp only compares in binary mode, but we want text mode. So # look through the list of different files, and compare them @@ -75,10 +86,10 @@ actual = scrub(actual, scrubs) if expected != actual: text_diff.append(f'{expected_file} != {actual_file}') - expected = expected.splitlines() - actual = actual.splitlines() + expected_lines = expected.splitlines() + actual_lines = actual.splitlines() print(f":::: diff '{expected_file}' and '{actual_file}'") - print("\n".join(difflib.Differ().compare(expected, actual))) + print("\n".join(difflib.Differ().compare(expected_lines, actual_lines))) print(f":::: end diff '{expected_file}' and '{actual_file}'") save_mismatch(f) @@ -93,7 +104,7 @@ assert not actual_only, f"Files in {actual_dir} only: {actual_only}" -def contains(filename, *strlist): +def contains(filename: str, *strlist: str) -> None: """Check that the file contains all of a list of strings. An assert will be raised if one of the arguments in `strlist` is @@ -107,7 +118,7 @@ assert s in text, f"Missing content in {filename}: {s!r}" -def contains_rx(filename, *rxlist): +def contains_rx(filename: str, *rxlist: str) -> None: """Check that the file has lines that re.search all of the regexes. An assert will be raised if one of the regexes in `rxlist` doesn't match @@ -123,7 +134,7 @@ ) -def contains_any(filename, *strlist): +def contains_any(filename: str, *strlist: str) -> None: """Check that the file contains at least one of a list of strings. An assert will be raised if none of the arguments in `strlist` is in @@ -140,7 +151,7 @@ assert False, f"Missing content in {filename}: {strlist[0]!r} [1 of {len(strlist)}]" -def doesnt_contain(filename, *strlist): +def doesnt_contain(filename: str, *strlist: str) -> None: """Check that the file contains none of a list of strings. An assert will be raised if any of the strings in `strlist` appears in @@ -156,16 +167,15 @@ # Helpers -def canonicalize_xml(xtext): +def canonicalize_xml(xtext: str) -> str: """Canonicalize some XML text.""" root = xml.etree.ElementTree.fromstring(xtext) for node in root.iter(): node.attrib = dict(sorted(node.items())) - xtext = xml.etree.ElementTree.tostring(root) - return xtext.decode("utf-8") + return xml.etree.ElementTree.tostring(root).decode("utf-8") -def fnmatch_list(files, file_pattern): +def _fnmatch_list(files: List[str], file_pattern: Optional[str]) -> List[str]: """Filter the list of `files` to only those that match `file_pattern`. If `file_pattern` is None, then return the entire list of files. Returns a list of the filtered files. @@ -175,7 +185,7 @@ return files -def scrub(strdata, scrubs): +def scrub(strdata: str, scrubs: Iterable[Tuple[str, str]]) -> str: """Scrub uninteresting data from the payload in `strdata`. `scrubs` is a list of (find, replace) pairs of regexes that are used on `strdata`. A string is returned. diff -Nru python-coverage-6.5.0+dfsg1/tests/helpers.py python-coverage-7.2.7+dfsg1/tests/helpers.py --- python-coverage-6.5.0+dfsg1/tests/helpers.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/helpers.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Helpers for coverage.py tests.""" +from __future__ import annotations + import collections import contextlib import os @@ -13,16 +15,21 @@ import textwrap import warnings -from unittest import mock +from pathlib import Path +from typing import ( + Any, Callable, Iterable, Iterator, List, Optional, Set, Tuple, Type, + TypeVar, Union, cast, +) import pytest from coverage import env from coverage.exceptions import CoverageWarning from coverage.misc import output_encoding +from coverage.types import TArc, TLineNo -def run_command(cmd): +def run_command(cmd: str) -> Tuple[int, str]: """Run a command in a sub-process. Returns the exit status code and the combined stdout and stderr. @@ -30,8 +37,8 @@ """ # Subprocesses are expensive, but convenient, and so may be over-used in # the test suite. Use these lines to get a list of the tests using them: - if 0: # pragma: debugging - with open("/tmp/processes.txt", "a") as proctxt: + if 0: # pragma: debugging + with open("/tmp/processes.txt", "a") as proctxt: # type: ignore[unreachable] print(os.environ.get("PYTEST_CURRENT_TEST", "unknown"), file=proctxt, flush=True) # In some strange cases (PyPy3 in a virtualenv!?) the stdout encoding of @@ -46,24 +53,29 @@ env=sub_env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT + stderr=subprocess.STDOUT, ) output, _ = proc.communicate() status = proc.returncode # Get the output, and canonicalize it to strings with newlines. - output = output.decode(output_encoding()).replace("\r", "") - return status, output + output_str = output.decode(output_encoding()).replace("\r", "") + return status, output_str -def make_file(filename, text="", bytes=b"", newline=None): +def make_file( + filename: str, + text: str = "", + bytes: bytes = b"", + newline: Optional[str] = None, +) -> str: """Create a file for testing. `filename` is the relative path to the file, including directories if desired, which will be created if need be. - `text` is the content to create in the file, a native string (bytes in - Python 2, unicode in Python 3), or `bytes` are the bytes to write. + `text` is the text content to create in the file, or `bytes` are the + bytes to write. If `newline` is provided, it is a string that will be used as the line endings in the created file, otherwise the line endings are as provided @@ -91,8 +103,8 @@ f.write(data) # For debugging, enable this to show the contents of files created. - if 0: # pragma: debugging - print(f" ───┬──┤ {filename} ├───────────────────────") + if 0: # pragma: debugging + print(f" ───┬──┤ {filename} ├───────────────────────") # type: ignore[unreachable] for lineno, line in enumerate(data.splitlines(), start=1): print(f"{lineno:6}│ {line.rstrip().decode()}") print() @@ -100,25 +112,26 @@ return filename -def nice_file(*fparts): +def nice_file(*fparts: str) -> str: """Canonicalize the file name composed of the parts in `fparts`.""" fname = os.path.join(*fparts) return os.path.normcase(os.path.abspath(os.path.realpath(fname))) -def os_sep(s): +def os_sep(s: str) -> str: """Replace slashes in `s` with the correct separator for the OS.""" return s.replace("/", os.sep) class CheckUniqueFilenames: """Asserts the uniqueness of file names passed to a function.""" - def __init__(self, wrapped): - self.filenames = set() + + def __init__(self, wrapped: Callable[..., Any]) -> None: + self.filenames: Set[str] = set() self.wrapped = wrapped @classmethod - def hook(cls, obj, method_name): + def hook(cls, obj: Any, method_name: str) -> CheckUniqueFilenames: """Replace a method with our checking wrapper. The method must take a string as a first argument. That argument @@ -133,17 +146,16 @@ setattr(obj, method_name, hook.wrapper) return hook - def wrapper(self, filename, *args, **kwargs): + def wrapper(self, filename: str, *args: Any, **kwargs: Any) -> Any: """The replacement method. Check that we don't have dupes.""" assert filename not in self.filenames, ( f"File name {filename!r} passed to {self.wrapped!r} twice" ) self.filenames.add(filename) - ret = self.wrapped(filename, *args, **kwargs) - return ret + return self.wrapped(filename, *args, **kwargs) -def re_lines(pat, text, match=True): +def re_lines(pat: str, text: str, match: bool = True) -> List[str]: """Return a list of lines selected by `pat` in the string `text`. If `match` is false, the selection is inverted: only the non-matching @@ -156,12 +168,12 @@ return [l for l in text.splitlines() if bool(re.search(pat, l)) == match] -def re_lines_text(pat, text, match=True): +def re_lines_text(pat: str, text: str, match: bool = True) -> str: """Return the multi-line text of lines selected by `pat`.""" return "".join(l + "\n" for l in re_lines(pat, text, match=match)) -def re_line(pat, text): +def re_line(pat: str, text: str) -> str: """Return the one line in `text` that matches regex `pat`. Raises an AssertionError if more than one, or less than one, line matches. @@ -172,7 +184,7 @@ return lines[0] -def remove_tree(dirname): +def remove_tree(dirname: str) -> None: """Remove a directory tree. It's fine for the directory to not exist in the first place. @@ -186,7 +198,8 @@ _arcz_map.update({c: ord(c) - ord('0') for c in '123456789'}) _arcz_map.update({c: 10 + ord(c) - ord('A') for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'}) -def arcz_to_arcs(arcz): + +def arcz_to_arcs(arcz: str) -> List[TArc]: """Convert a compact textual representation of arcs to a list of pairs. The text has space-separated pairs of letters. Period is -1, 1-9 are @@ -200,19 +213,23 @@ "-11, 12, 2-5" --> [(-1,1), (1,2), (2,-5)] """ + # The `type: ignore[misc]` here are to suppress "Unpacking a string is + # disallowed". + a: str + b: str arcs = [] for pair in arcz.split(): asgn = bsgn = 1 if len(pair) == 2: - a, b = pair + a, b = pair # type: ignore[misc] else: assert len(pair) == 3 - if pair[0] == '-': - _, a, b = pair + if pair[0] == "-": + _, a, b = pair # type: ignore[misc] asgn = -1 else: - assert pair[1] == '-' - a, _, b = pair + assert pair[1] == "-" + a, _, b = pair # type: ignore[misc] bsgn = -1 arcs.append((asgn * _arcz_map[a], bsgn * _arcz_map[b])) return sorted(arcs) @@ -220,7 +237,8 @@ _arcz_unmap = {val: ch for ch, val in _arcz_map.items()} -def _arcs_to_arcz_repr_one(num): + +def _arcs_to_arcz_repr_one(num: TLineNo) -> str: """Return an arcz form of the number `num`, or "?" if there is none.""" if num == -1: return "." @@ -232,7 +250,7 @@ return z -def arcs_to_arcz_repr(arcs): +def arcs_to_arcz_repr(arcs: Optional[Iterable[TArc]]) -> str: """Convert a list of arcs to a readable multi-line form for asserting. Each pair is on its own line, with a comment showing the arcz form, @@ -250,7 +268,7 @@ @contextlib.contextmanager -def change_dir(new_dir): +def change_dir(new_dir: Union[str, Path]) -> Iterator[None]: """Change directory, and then change back. Use as a context manager, it will return to the original @@ -264,43 +282,37 @@ finally: os.chdir(old_dir) +T = TypeVar("T") -def without_module(using_module, missing_module_name): - """ - Hide a module for testing. - - Use this in a test function to make an optional module unavailable during - the test:: - - with without_module(product.something, 'tomli'): - use_toml_somehow() - - Arguments: - using_module: a module in which to hide `missing_module_name`. - missing_module_name (str): the name of the module to hide. - - """ - return mock.patch.object(using_module, missing_module_name, None) - - -def assert_count_equal(a, b): +def assert_count_equal( + a: Optional[Iterable[T]], + b: Optional[Iterable[T]], +) -> None: """ A pytest-friendly implementation of assertCountEqual. Assert that `a` and `b` have the same elements, but maybe in different order. This only works for hashable elements. """ + assert a is not None + assert b is not None assert collections.Counter(list(a)) == collections.Counter(list(b)) -def assert_coverage_warnings(warns, *msgs): +def assert_coverage_warnings( + warns: Iterable[warnings.WarningMessage], + *msgs: Union[str, re.Pattern[str]], +) -> None: """ Assert that the CoverageWarning's in `warns` have `msgs` as messages. + + Each msg can be a string compared for equality, or a compiled regex used to + search the text. """ assert msgs # don't call this without some messages. warns = [w for w in warns if issubclass(w.category, CoverageWarning)] assert len(warns) == len(msgs) - for actual, expected in zip((w.message.args[0] for w in warns), msgs): + for actual, expected in zip((cast(Warning, w.message).args[0] for w in warns), msgs): if hasattr(expected, "search"): assert expected.search(actual), f"{actual!r} didn't match {expected!r}" else: @@ -308,7 +320,10 @@ @contextlib.contextmanager -def swallow_warnings(message=r".", category=CoverageWarning): +def swallow_warnings( + message: str = r".", + category: Type[Warning] = CoverageWarning, +) -> Iterator[None]: """Swallow particular warnings. It's OK if they happen, or if they don't happen. Just ignore them. @@ -318,7 +333,7 @@ yield -xfail_pypy_3749 = pytest.mark.xfail( - env.PYVERSION[:2] == (3, 8) and env.PYPY and env.PYPYVERSION >= (7, 3, 10), - reason="Avoid a PyPy bug: https://foss.heptapod.net/pypy/pypy/-/issues/3749", +xfail_pypy38 = pytest.mark.xfail( + env.PYPY and env.PYVERSION[:2] == (3, 8) and env.PYPYVERSION < (7, 3, 11), + reason="These tests fail on older PyPy 3.8", ) diff -Nru python-coverage-6.5.0+dfsg1/tests/mixins.py python-coverage-7.2.7+dfsg1/tests/mixins.py --- python-coverage-6.5.0+dfsg1/tests/mixins.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/mixins.py 2023-05-29 19:46:30.000000000 +0000 @@ -7,11 +7,15 @@ Some of these are transitional while working toward pure-pytest style. """ +from __future__ import annotations + import importlib import os import os.path import sys +from typing import Any, Callable, Iterable, Iterator, Optional, Tuple, cast + import pytest from coverage.misc import SysModuleSaver @@ -22,26 +26,30 @@ """A base class to connect to pytest in a test class hierarchy.""" @pytest.fixture(autouse=True) - def connect_to_pytest(self, request, monkeypatch): + def connect_to_pytest( + self, + request: pytest.FixtureRequest, + monkeypatch: pytest.MonkeyPatch, + ) -> None: """Captures pytest facilities for use by other test helpers.""" # pylint: disable=attribute-defined-outside-init self._pytest_request = request self._monkeypatch = monkeypatch self.setUp() - def setUp(self): + def setUp(self) -> None: """Per-test initialization. Override this as you wish.""" pass - def addCleanup(self, fn, *args): + def addCleanup(self, fn: Callable[..., None], *args: Any) -> None: """Like unittest's addCleanup: code to call when the test is done.""" self._pytest_request.addfinalizer(lambda: fn(*args)) - def set_environ(self, name, value): + def set_environ(self, name: str, value: str) -> None: """Set an environment variable `name` to be `value`.""" self._monkeypatch.setenv(name, value) - def del_environ(self, name): + def del_environ(self, name: str) -> None: """Delete an environment variable, unless we set it.""" self._monkeypatch.delenv(name, raising=False) @@ -55,10 +63,10 @@ run_in_temp_dir = True @pytest.fixture(autouse=True) - def _temp_dir(self, tmpdir_factory): + def _temp_dir(self, tmp_path_factory: pytest.TempPathFactory) -> Iterator[None]: """Create a temp dir for the tests, if they want it.""" if self.run_in_temp_dir: - tmpdir = tmpdir_factory.mktemp("t") + tmpdir = tmp_path_factory.mktemp("t") self.temp_dir = str(tmpdir) with change_dir(self.temp_dir): # Modules should be importable from this temp directory. We don't @@ -70,7 +78,13 @@ else: yield - def make_file(self, filename, text="", bytes=b"", newline=None): + def make_file( + self, + filename: str, + text: str = "", + bytes: bytes = b"", + newline: Optional[str] = None, + ) -> str: """Make a file. See `tests.helpers.make_file`""" # pylint: disable=redefined-builtin # bytes assert self.run_in_temp_dir, "Only use make_file when running in a temp dir" @@ -81,7 +95,7 @@ """Auto-restore the imported modules at the end of each test.""" @pytest.fixture(autouse=True) - def _module_saving(self): + def _module_saving(self) -> Iterable[None]: """Remove modules we imported during the test.""" self._sys_module_saver = SysModuleSaver() try: @@ -89,7 +103,7 @@ finally: self._sys_module_saver.restore() - def clean_local_file_imports(self): + def clean_local_file_imports(self) -> None: """Clean up the results of calls to `import_local_file`. Use this if you need to `import_local_file` the same file twice in @@ -99,7 +113,7 @@ # So that we can re-import files, clean them out first. self._sys_module_saver.restore() - # Also have to clean out the .pyc files, since the timestamp + # Also have to clean out the .pyc files, since the time stamp # resolution is only one second, a changed file might not be # picked up. remove_tree("__pycache__") @@ -118,18 +132,18 @@ """ @pytest.fixture(autouse=True) - def _capcapsys(self, capsys): + def _capcapsys(self, capsys: pytest.CaptureFixture[str]) -> None: """Grab the fixture so our methods can use it.""" self.capsys = capsys - def stdouterr(self): + def stdouterr(self) -> Tuple[str, str]: """Returns (out, err), two strings for stdout and stderr.""" - return self.capsys.readouterr() + return cast(Tuple[str, str], self.capsys.readouterr()) - def stdout(self): + def stdout(self) -> str: """Returns a string, the captured stdout.""" return self.capsys.readouterr().out - def stderr(self): + def stderr(self) -> str: """Returns a string, the captured stderr.""" return self.capsys.readouterr().err diff -Nru python-coverage-6.5.0+dfsg1/tests/modules/plugins/another.py python-coverage-7.2.7+dfsg1/tests/modules/plugins/another.py --- python-coverage-6.5.0+dfsg1/tests/modules/plugins/another.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/modules/plugins/another.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,12 +3,19 @@ """A plugin for tests to reference.""" -from coverage import CoveragePlugin +from __future__ import annotations + +from typing import Any +from coverage import CoveragePlugin +from coverage.plugin_support import Plugins class Plugin(CoveragePlugin): pass -def coverage_init(reg, options): +def coverage_init( + reg: Plugins, + options: Any, # pylint: disable=unused-argument +) -> None: reg.add_file_tracer(Plugin()) diff -Nru python-coverage-6.5.0+dfsg1/tests/modules/plugins/a_plugin.py python-coverage-7.2.7+dfsg1/tests/modules/plugins/a_plugin.py --- python-coverage-6.5.0+dfsg1/tests/modules/plugins/a_plugin.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/modules/plugins/a_plugin.py 2023-05-29 19:46:30.000000000 +0000 @@ -1,11 +1,19 @@ """A plugin for tests to reference.""" +from __future__ import annotations + +from typing import Any + from coverage import CoveragePlugin +from coverage.plugin_support import Plugins class Plugin(CoveragePlugin): pass -def coverage_init(reg, options): +def coverage_init( + reg: Plugins, + options: Any, # pylint: disable=unused-argument +) -> None: reg.add_file_tracer(Plugin()) diff -Nru python-coverage-6.5.0+dfsg1/tests/modules/process_test/try_execfile.py python-coverage-7.2.7+dfsg1/tests/modules/process_test/try_execfile.py --- python-coverage-6.5.0+dfsg1/tests/modules/process_test/try_execfile.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/modules/process_test/try_execfile.py 2023-05-29 19:46:30.000000000 +0000 @@ -20,17 +20,21 @@ """ +from __future__ import annotations + import itertools import json import os import sys +from typing import Any, List + # sys.path varies by execution environments. Coverage.py uses setuptools to # make console scripts, which means pkg_resources is imported. pkg_resources # removes duplicate entries from sys.path. So we do that too, since the extra # entries don't affect the running of the program. -def same_file(p1, p2): +def same_file(p1: str, p2: str) -> bool: """Determine if `p1` and `p2` refer to the same existing file.""" if not p1: return not p2 @@ -45,9 +49,9 @@ norm2 = os.path.normcase(os.path.normpath(p2)) return norm1 == norm2 -def without_same_files(filenames): +def without_same_files(filenames: List[str]) -> List[str]: """Return the list `filenames` with duplicates (by same_file) removed.""" - reduced = [] + reduced: List[str] = [] for filename in filenames: if not any(same_file(filename, other) for other in reduced): reduced.append(filename) @@ -59,7 +63,7 @@ import __main__ -def my_function(a): +def my_function(a: Any) -> str: """A function to force execution of module-level values.""" return f"my_fn({a!r})" @@ -71,7 +75,7 @@ # A more compact ad-hoc grouped-by-first-letter list of builtins. CLUMPS = "ABC,DEF,GHI,JKLMN,OPQR,ST,U,VWXYZ_,ab,cd,efg,hij,lmno,pqr,stuvwxyz".split(",") -def word_group(w): +def word_group(w: str) -> int: """Figure out which CLUMP the first letter of w is in.""" for i, clump in enumerate(CLUMPS): if w[0] in clump: diff -Nru python-coverage-6.5.0+dfsg1/tests/osinfo.py python-coverage-7.2.7+dfsg1/tests/osinfo.py --- python-coverage-6.5.0+dfsg1/tests/osinfo.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/osinfo.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,12 +3,14 @@ """OS information for testing.""" -from coverage import env +from __future__ import annotations +import sys -if env.WINDOWS: + +if sys.platform == "win32": # Windows implementation - def process_ram(): + def process_ram() -> int: """How much RAM is this process using? (Windows)""" import ctypes # From: http://lists.ubuntu.com/archives/bazaar-commits/2009-February/011990.html @@ -38,35 +40,35 @@ return 0 # pragma: cant happen return mem_struct.PrivateUsage -elif env.LINUX: +elif sys.platform.startswith("linux"): # Linux implementation import os _scale = {'kb': 1024, 'mb': 1024*1024} - def _VmB(key): + def _VmB(key: str) -> int: """Read the /proc/PID/status file to find memory use.""" try: # Get pseudo file /proc//status - with open('/proc/%d/status' % os.getpid()) as t: + with open(f"/proc/{os.getpid()}/status") as t: v = t.read() except OSError: # pragma: cant happen return 0 # non-Linux? # Get VmKey line e.g. 'VmRSS: 9999 kB\n ...' i = v.index(key) - v = v[i:].split(None, 3) - if len(v) < 3: # pragma: part covered + vp = v[i:].split(None, 3) + if len(vp) < 3: # pragma: part covered return 0 # pragma: cant happen # Convert Vm value to bytes. - return int(float(v[1]) * _scale[v[2].lower()]) + return int(float(vp[1]) * _scale[vp[2].lower()]) - def process_ram(): + def process_ram() -> int: """How much RAM is this process using? (Linux implementation)""" return _VmB('VmRSS') else: # Generic implementation. - def process_ram(): + def process_ram() -> int: """How much RAM is this process using? (stdlib implementation)""" import resource return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss diff -Nru python-coverage-6.5.0+dfsg1/tests/plugin1.py python-coverage-7.2.7+dfsg1/tests/plugin1.py --- python-coverage-6.5.0+dfsg1/tests/plugin1.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/plugin1.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,28 +3,34 @@ """A file tracer plugin for test_plugins.py to import.""" +from __future__ import annotations + import os.path -import coverage +from types import FrameType +from typing import Any, Optional, Set, Tuple, Union +from coverage import CoveragePlugin, FileReporter, FileTracer +from coverage.plugin_support import Plugins +from coverage.types import TLineNo -class Plugin(coverage.CoveragePlugin): +class Plugin(CoveragePlugin): """A file tracer plugin to import, so that it isn't in the test's current directory.""" - def file_tracer(self, filename): + def file_tracer(self, filename: str) -> Optional[FileTracer]: """Trace only files named xyz.py""" if "xyz.py" in filename: - return FileTracer(filename) + return MyFileTracer(filename) return None - def file_reporter(self, filename): - return FileReporter(filename) + def file_reporter(self, filename: str) -> Union[FileReporter, str]: + return MyFileReporter(filename) -class FileTracer(coverage.FileTracer): +class MyFileTracer(FileTracer): """A FileTracer emulating a simple static plugin.""" - def __init__(self, filename): + def __init__(self, filename: str) -> None: """Claim that */*xyz.py was actually sourced from /src/*ABC.zz""" self._filename = filename self._source_filename = os.path.join( @@ -32,21 +38,24 @@ os.path.basename(filename.replace("xyz.py", "ABC.zz")) ) - def source_filename(self): + def source_filename(self) -> str: return self._source_filename - def line_number_range(self, frame): + def line_number_range(self, frame: FrameType) -> Tuple[TLineNo, TLineNo]: """Map the line number X to X05,X06,X07.""" lineno = frame.f_lineno return lineno*100+5, lineno*100+7 -class FileReporter(coverage.FileReporter): +class MyFileReporter(FileReporter): """Dead-simple FileReporter.""" - def lines(self): + def lines(self) -> Set[TLineNo]: return {105, 106, 107, 205, 206, 207} -def coverage_init(reg, options): # pylint: disable=unused-argument +def coverage_init( + reg: Plugins, + options: Any, # pylint: disable=unused-argument +) -> None: """Called by coverage to initialize the plugins here.""" reg.add_file_tracer(Plugin()) diff -Nru python-coverage-6.5.0+dfsg1/tests/plugin2.py python-coverage-7.2.7+dfsg1/tests/plugin2.py --- python-coverage-6.5.0+dfsg1/tests/plugin2.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/plugin2.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,9 +3,16 @@ """A file tracer plugin for test_plugins.py to import.""" +from __future__ import annotations + import os.path -import coverage +from types import FrameType +from typing import Any, Optional, Set, Tuple + +from coverage import CoveragePlugin, FileReporter, FileTracer +from coverage.plugin_support import Plugins +from coverage.types import TLineNo try: import third.render # pylint: disable=unused-import @@ -16,43 +23,50 @@ pass -class Plugin(coverage.CoveragePlugin): +class Plugin(CoveragePlugin): """A file tracer plugin for testing.""" - def file_tracer(self, filename): + def file_tracer(self, filename: str) -> Optional[FileTracer]: if "render.py" in filename: return RenderFileTracer() return None - def file_reporter(self, filename): - return FileReporter(filename) + def file_reporter(self, filename: str) -> FileReporter: + return MyFileReporter(filename) -class RenderFileTracer(coverage.FileTracer): +class RenderFileTracer(FileTracer): """A FileTracer using information from the caller.""" - def has_dynamic_source_filename(self): + def has_dynamic_source_filename(self) -> bool: return True - def dynamic_source_filename(self, filename, frame): + def dynamic_source_filename( + self, + filename: str, + frame: FrameType, + ) -> Optional[str]: if frame.f_code.co_name != "render": return None - source_filename = os.path.abspath(frame.f_locals['filename']) + source_filename: str = os.path.abspath(frame.f_locals['filename']) return source_filename - def line_number_range(self, frame): + def line_number_range(self, frame: FrameType) -> Tuple[TLineNo, TLineNo]: lineno = frame.f_locals['linenum'] return lineno, lineno+1 -class FileReporter(coverage.FileReporter): +class MyFileReporter(FileReporter): """A goofy file reporter.""" - def lines(self): + def lines(self) -> Set[TLineNo]: # Goofy test arrangement: claim that the file has as many lines as the # number in its name. num = os.path.basename(self.filename).split(".")[0].split("_")[1] return set(range(1, int(num)+1)) -def coverage_init(reg, options): # pylint: disable=unused-argument +def coverage_init( + reg: Plugins, + options: Any, # pylint: disable=unused-argument +) -> None: """Called by coverage to initialize the plugins here.""" reg.add_file_tracer(Plugin()) diff -Nru python-coverage-6.5.0+dfsg1/tests/plugin_config.py python-coverage-7.2.7+dfsg1/tests/plugin_config.py --- python-coverage-6.5.0+dfsg1/tests/plugin_config.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/plugin_config.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,20 +3,29 @@ """A configuring plugin for test_plugins.py to import.""" +from __future__ import annotations + +from typing import Any, List, cast + import coverage +from coverage.plugin_support import Plugins +from coverage.types import TConfigurable class Plugin(coverage.CoveragePlugin): """A configuring plugin for testing.""" - def configure(self, config): + def configure(self, config: TConfigurable) -> None: """Configure all the things!""" opt_name = "report:exclude_lines" - exclude_lines = config.get_option(opt_name) + exclude_lines = cast(List[str], config.get_option(opt_name)) exclude_lines.append(r"pragma: custom") exclude_lines.append(r"pragma: or whatever") config.set_option(opt_name, exclude_lines) -def coverage_init(reg, options): # pylint: disable=unused-argument +def coverage_init( + reg: Plugins, + options: Any, # pylint: disable=unused-argument +) -> None: """Called by coverage to initialize the plugins here.""" reg.add_configurer(Plugin()) diff -Nru python-coverage-6.5.0+dfsg1/tests/stress_phystoken_dos.tok python-coverage-7.2.7+dfsg1/tests/stress_phystoken_dos.tok --- python-coverage-6.5.0+dfsg1/tests/stress_phystoken_dos.tok 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/stress_phystoken_dos.tok 2023-05-29 19:46:30.000000000 +0000 @@ -3,7 +3,7 @@ # Here's some random Python so that test_tokenize_myself will have some # stressful stuff to try. This file is .tok instead of .py so pylint won't -# complain about it, check_eol won't look at it, etc. +# complain about it, editors won't mess with it, etc. first_back = """\ hey there! diff -Nru python-coverage-6.5.0+dfsg1/tests/stress_phystoken.tok python-coverage-7.2.7+dfsg1/tests/stress_phystoken.tok --- python-coverage-6.5.0+dfsg1/tests/stress_phystoken.tok 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/stress_phystoken.tok 2023-05-29 19:46:30.000000000 +0000 @@ -3,7 +3,7 @@ # Here's some random Python so that test_tokenize_myself will have some # stressful stuff to try. This file is .tok instead of .py so pylint won't -# complain about it, check_eol won't look at it, etc. +# complain about it, editors won't mess with it, etc. # Some lines are here to reproduce fixed bugs in ast_dump also. first_back = """\ diff -Nru python-coverage-6.5.0+dfsg1/tests/test_annotate.py python-coverage-7.2.7+dfsg1/tests/test_annotate.py --- python-coverage-6.5.0+dfsg1/tests/test_annotate.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_annotate.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Tests for annotation from coverage.py.""" +from __future__ import annotations + import coverage from tests.coveragetest import CoverageTest @@ -12,7 +14,7 @@ class AnnotationGoldTest(CoverageTest): """Test the annotate feature with gold files.""" - def make_multi(self): + def make_multi(self) -> None: """Make a few source files we need for the tests.""" self.make_file("multi.py", """\ import a.a @@ -36,7 +38,7 @@ print(msg) """) - def test_multi(self): + def test_multi(self) -> None: self.make_multi() cov = coverage.Coverage() self.start_import_stop(cov, "multi") @@ -44,7 +46,7 @@ compare(gold_path("annotate/multi"), ".", "*,cover") - def test_annotate_dir(self): + def test_annotate_dir(self) -> None: self.make_multi() cov = coverage.Coverage(source=["."]) self.start_import_stop(cov, "multi") @@ -52,7 +54,7 @@ compare(gold_path("annotate/anno_dir"), "out_anno_dir", "*,cover") - def test_encoding(self): + def test_encoding(self) -> None: self.make_file("utf8.py", """\ # -*- coding: utf-8 -*- # This comment has an accent: é @@ -64,7 +66,7 @@ cov.annotate() compare(gold_path("annotate/encodings"), ".", "*,cover") - def test_white(self): + def test_white(self) -> None: self.make_file("white.py", """\ # A test case sent to me by Steve White @@ -106,7 +108,7 @@ cov.annotate() compare(gold_path("annotate/white"), ".", "*,cover") - def test_missing_after_else(self): + def test_missing_after_else(self) -> None: self.make_file("mae.py", """\ def f(x): if x == 1: diff -Nru python-coverage-6.5.0+dfsg1/tests/test_api.py python-coverage-7.2.7+dfsg1/tests/test_api.py --- python-coverage-6.5.0+dfsg1/tests/test_api.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_api.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Tests for coverage.py's API.""" +from __future__ import annotations + import fnmatch import glob import io @@ -13,14 +15,17 @@ import sys import textwrap +from typing import cast, Callable, Dict, Iterable, List, Optional, Set + import pytest import coverage -from coverage import env -from coverage.data import line_counts +from coverage import Coverage, env +from coverage.data import line_counts, sorted_lines from coverage.exceptions import CoverageException, DataError, NoDataError, NoSource from coverage.files import abs_file, relative_filename from coverage.misc import import_local_file +from coverage.types import FilePathClasses, FilePathType, Protocol, TCovKwargs from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin from tests.helpers import assert_count_equal, assert_coverage_warnings @@ -31,7 +36,7 @@ class ApiTest(CoverageTest): """Api-oriented tests for coverage.py.""" - def clean_files(self, files, pats): + def clean_files(self, files: List[str], pats: List[str]) -> List[str]: """Remove names matching `pats` from `files`, a list of file names.""" good = [] for f in files: @@ -42,13 +47,13 @@ good.append(f) return good - def assertFiles(self, files): + def assertFiles(self, files: List[str]) -> None: """Assert that the files here are `files`, ignoring the usual junk.""" here = os.listdir(".") here = self.clean_files(here, ["*.pyc", "__pycache__", "*$py.class"]) assert_count_equal(here, files) - def test_unexecuted_file(self): + def test_unexecuted_file(self) -> None: cov = coverage.Coverage() self.make_file("mycode.py", """\ @@ -70,8 +75,7 @@ assert statements == [1] assert missing == [1] - def test_filenames(self): - + def test_filenames(self) -> None: self.make_file("mymain.py", """\ import mymod a = 1 @@ -110,7 +114,7 @@ filename, _, _, _ = cov.analysis(sys.modules["mymod"]) assert os.path.basename(filename) == "mymod.py" - def test_ignore_stdlib(self): + def test_ignore_stdlib(self) -> None: self.make_file("mymain.py", """\ import colorsys a = 1 @@ -140,7 +144,7 @@ _, statements, missing, _ = cov2.analysis("colorsys.py") assert statements != missing - def test_include_can_measure_stdlib(self): + def test_include_can_measure_stdlib(self) -> None: self.make_file("mymain.py", """\ import colorsys, random a = 1 @@ -159,7 +163,7 @@ _, statements, missing, _ = cov1.analysis("random.py") assert statements == missing - def test_exclude_list(self): + def test_exclude_list(self) -> None: cov = coverage.Coverage() cov.clear_exclude() assert cov.get_exclude_list() == [] @@ -171,7 +175,7 @@ cov.clear_exclude() assert cov.get_exclude_list() == [] - def test_exclude_partial_list(self): + def test_exclude_partial_list(self) -> None: cov = coverage.Coverage() cov.clear_exclude(which='partial') assert cov.get_exclude_list(which='partial') == [] @@ -183,7 +187,7 @@ cov.clear_exclude(which='partial') assert cov.get_exclude_list(which='partial') == [] - def test_exclude_and_partial_are_separate_lists(self): + def test_exclude_and_partial_are_separate_lists(self) -> None: cov = coverage.Coverage() cov.clear_exclude(which='partial') cov.clear_exclude(which='exclude') @@ -204,7 +208,7 @@ assert cov.get_exclude_list(which='partial') == [] assert cov.get_exclude_list(which='exclude') == [] - def test_datafile_default(self): + def test_datafile_default(self) -> None: # Default data file behavior: it's .coverage self.make_file("datatest1.py", """\ fooey = 17 @@ -216,31 +220,33 @@ cov.save() self.assertFiles(["datatest1.py", ".coverage"]) - def test_datafile_specified(self): + @pytest.mark.parametrize("file_class", FilePathClasses) + def test_datafile_specified(self, file_class: FilePathType) -> None: # You can specify the data file name. self.make_file("datatest2.py", """\ fooey = 17 """) self.assertFiles(["datatest2.py"]) - cov = coverage.Coverage(data_file="cov.data") + cov = coverage.Coverage(data_file=file_class("cov.data")) self.start_import_stop(cov, "datatest2") cov.save() self.assertFiles(["datatest2.py", "cov.data"]) - def test_datafile_and_suffix_specified(self): + @pytest.mark.parametrize("file_class", FilePathClasses) + def test_datafile_and_suffix_specified(self, file_class: FilePathType) -> None: # You can specify the data file name and suffix. self.make_file("datatest3.py", """\ fooey = 17 """) self.assertFiles(["datatest3.py"]) - cov = coverage.Coverage(data_file="cov.data", data_suffix="14") + cov = coverage.Coverage(data_file=file_class("cov.data"), data_suffix="14") self.start_import_stop(cov, "datatest3") cov.save() self.assertFiles(["datatest3.py", "cov.data.14"]) - def test_datafile_from_rcfile(self): + def test_datafile_from_rcfile(self) -> None: # You can specify the data file name in the .coveragerc file self.make_file("datatest4.py", """\ fooey = 17 @@ -256,7 +262,7 @@ cov.save() self.assertFiles(["datatest4.py", ".coveragerc", "mydata.dat"]) - def test_deep_datafile(self): + def test_deep_datafile(self) -> None: self.make_file("datatest5.py", "fooey = 17") self.assertFiles(["datatest5.py"]) cov = coverage.Coverage(data_file="deep/sub/cov.data") @@ -265,16 +271,16 @@ self.assertFiles(["datatest5.py", "deep"]) self.assert_exists("deep/sub/cov.data") - def test_datafile_none(self): + def test_datafile_none(self) -> None: cov = coverage.Coverage(data_file=None) - def f1(): # pragma: nested - a = 1 # pylint: disable=unused-variable + def f1() -> None: # pragma: nested + a = 1 # pylint: disable=unused-variable one_line_number = f1.__code__.co_firstlineno + 1 lines = [] - def run_one_function(f): + def run_one_function(f: Callable[[], None]) -> None: cov.erase() cov.start() f() @@ -290,14 +296,14 @@ self.assert_doesnt_exist(".coverage") assert os.listdir(".") == [] - def test_empty_reporting(self): + def test_empty_reporting(self) -> None: # empty summary reports raise exception, just like the xml report cov = coverage.Coverage() cov.erase() with pytest.raises(NoDataError, match="No data to report."): cov.report() - def test_completely_zero_reporting(self): + def test_completely_zero_reporting(self) -> None: # https://github.com/nedbat/coveragepy/issues/884 # If nothing was measured, the file-touching didn't happen properly. self.make_file("foo/bar.py", "print('Never run')") @@ -316,7 +322,7 @@ last = self.last_line_squeezed(self.stdout()) assert "TOTAL 1 1 0%" == last - def test_cov4_data_file(self): + def test_cov4_data_file(self) -> None: cov4_data = ( "!coverage.py: This is a private format, don't read it directly!" + '{"lines":{"/private/tmp/foo.py":[1,5,2,3]}}' @@ -327,7 +333,7 @@ cov.load() cov.erase() - def make_code1_code2(self): + def make_code1_code2(self) -> None: """Create the code1.py and code2.py files.""" self.make_file("code1.py", """\ code1 = 1 @@ -337,7 +343,7 @@ code2 = 2 """) - def check_code1_code2(self, cov): + def check_code1_code2(self, cov: Coverage) -> None: """Check the analysis is correct for code1.py and code2.py.""" _, statements, missing, _ = cov.analysis("code1.py") assert statements == [1] @@ -346,7 +352,7 @@ assert statements == [1, 2] assert missing == [] - def test_start_stop_start_stop(self): + def test_start_stop_start_stop(self) -> None: self.make_code1_code2() cov = coverage.Coverage() self.start_import_stop(cov, "code1") @@ -354,7 +360,7 @@ self.start_import_stop(cov, "code2") self.check_code1_code2(cov) - def test_start_save_stop(self): + def test_start_save_stop(self) -> None: self.make_code1_code2() cov = coverage.Coverage() cov.start() @@ -364,7 +370,7 @@ cov.stop() # pragma: nested self.check_code1_code2(cov) - def test_start_save_nostop(self): + def test_start_save_nostop(self) -> None: self.make_code1_code2() cov = coverage.Coverage() cov.start() @@ -375,7 +381,7 @@ # Then stop it, or the test suite gets out of whack. cov.stop() # pragma: nested - def test_two_getdata_only_warn_once(self): + def test_two_getdata_only_warn_once(self) -> None: self.make_code1_code2() cov = coverage.Coverage(source=["."], omit=["code1.py"]) cov.start() @@ -389,7 +395,7 @@ with self.assert_warnings(cov, []): cov.get_data() - def test_two_getdata_warn_twice(self): + def test_two_getdata_warn_twice(self) -> None: self.make_code1_code2() cov = coverage.Coverage(source=["."], omit=["code1.py", "code2.py"]) cov.start() @@ -404,7 +410,7 @@ # Then stop it, or the test suite gets out of whack. cov.stop() # pragma: nested - def make_good_data_files(self): + def make_good_data_files(self) -> None: """Make some good data files.""" self.make_code1_code2() cov = coverage.Coverage(data_suffix=True) @@ -416,7 +422,7 @@ cov.save() self.assert_file_count(".coverage.*", 2) - def test_combining_corrupt_data(self): + def test_combining_corrupt_data(self) -> None: # If you combine a corrupt data file, then you will get a warning, # and the file will remain. self.make_good_data_files() @@ -435,7 +441,7 @@ self.assert_exists(".coverage.foo") self.assert_file_count(".coverage.*", 1) - def test_combining_twice(self): + def test_combining_twice(self) -> None: self.make_good_data_files() cov1 = coverage.Coverage() cov1.combine() @@ -460,7 +466,7 @@ assert statements == [1, 2] assert missing == [1, 2] - def test_combining_with_a_used_coverage(self): + def test_combining_with_a_used_coverage(self) -> None: # Can you use a coverage object to run one shard of a parallel suite, # and then also combine the data? self.make_code1_code2() @@ -476,10 +482,13 @@ assert self.stdout() == "" self.check_code1_code2(cov) - def test_ordered_combine(self): + def test_ordered_combine(self) -> None: # https://github.com/nedbat/coveragepy/issues/649 - # The order of the [paths] setting matters - def make_data_file(): + # The order of the [paths] setting used to matter. Now the + # resulting path must exist, so the order doesn't matter. + def make_files() -> None: + self.make_file("plugins/p1.py", "") + self.make_file("girder/g1.py", "") self.make_data_file( basename=".coverage.1", lines={ @@ -488,7 +497,7 @@ }, ) - def get_combined_filenames(): + def get_combined_filenames() -> Set[str]: cov = coverage.Coverage() cov.combine() assert self.stdout() == "" @@ -498,7 +507,7 @@ return filenames # Case 1: get the order right. - make_data_file() + make_files() self.make_file(".coveragerc", """\ [paths] plugins = @@ -510,8 +519,8 @@ """) assert get_combined_filenames() == {'girder/g1.py', 'plugins/p1.py'} - # Case 2: get the order wrong. - make_data_file() + # Case 2: get the order "wrong". + make_files() self.make_file(".coveragerc", """\ [paths] girder = @@ -521,9 +530,9 @@ plugins/ ci/girder/plugins/ """) - assert get_combined_filenames() == {'girder/g1.py', 'girder/plugins/p1.py'} + assert get_combined_filenames() == {'girder/g1.py', 'plugins/p1.py'} - def test_warnings(self): + def test_warnings(self) -> None: self.make_file("hello.py", """\ import sys, os print("Hello") @@ -542,7 +551,7 @@ "No data was collected. (no-data-collected)", ) - def test_warnings_suppressed(self): + def test_warnings_suppressed(self) -> None: self.make_file("hello.py", """\ import sys, os print("Hello") @@ -561,7 +570,7 @@ # No "module-not-imported" in warns # No "no-data-collected" in warns - def test_warn_once(self): + def test_warn_once(self) -> None: with pytest.warns(Warning) as warns: cov = coverage.Coverage() cov.load() @@ -571,7 +580,7 @@ assert_coverage_warnings(warns, "Warning, warning 1! (bot)") # No "Warning, warning 2!" in warns - def test_source_and_include_dont_conflict(self): + def test_source_and_include_dont_conflict(self) -> None: # A bad fix made this case fail: https://github.com/nedbat/coveragepy/issues/541 self.make_file("a.py", "import b\na = 1") self.make_file("b.py", "b = 1") @@ -600,7 +609,7 @@ """) assert expected == self.stdout() - def make_test_files(self): + def make_test_files(self) -> None: """Create a simple file representing a method with two tests. Returns absolute path to the file. @@ -616,7 +625,7 @@ assert timestwo(6) == 12 """) - def test_switch_context_testrunner(self): + def test_switch_context_testrunner(self) -> None: # This test simulates a coverage-aware test runner, # measuring labeled coverage via public API self.make_test_files() @@ -649,11 +658,11 @@ suite_filename = filenames['testsuite.py'] data.set_query_context("multiply_six") - assert [2, 8] == sorted(data.lines(suite_filename)) + assert [2, 8] == sorted_lines(data, suite_filename) data.set_query_context("multiply_zero") - assert [2, 5] == sorted(data.lines(suite_filename)) + assert [2, 5] == sorted_lines(data, suite_filename) - def test_switch_context_with_static(self): + def test_switch_context_with_static(self) -> None: # This test simulates a coverage-aware test runner, # measuring labeled coverage via public API, # with static label prefix. @@ -688,11 +697,11 @@ suite_filename = filenames['testsuite.py'] data.set_query_context("mysuite|multiply_six") - assert [2, 8] == sorted(data.lines(suite_filename)) + assert [2, 8] == sorted_lines(data, suite_filename) data.set_query_context("mysuite|multiply_zero") - assert [2, 5] == sorted(data.lines(suite_filename)) + assert [2, 5] == sorted_lines(data, suite_filename) - def test_dynamic_context_conflict(self): + def test_dynamic_context_conflict(self) -> None: cov = coverage.Coverage(source=["."]) cov.set_option("run:dynamic_context", "test_function") cov.start() @@ -703,7 +712,13 @@ cov.stop() # pragma: nested assert_coverage_warnings(warns, "Conflicting dynamic contexts (dynamic-conflict)") - def test_switch_context_unstarted(self): + def test_unknown_dynamic_context(self) -> None: + cov = coverage.Coverage() + cov.set_option("run:dynamic_context", "no-idea") + with pytest.raises(Exception, match="Don't understand dynamic_context setting: 'no-idea'"): + cov.start() + + def test_switch_context_unstarted(self) -> None: # Coverage must be started to switch context msg = "Cannot switch context, coverage is not started" cov = coverage.Coverage() @@ -717,7 +732,7 @@ with pytest.raises(CoverageException, match=msg): cov.switch_context("test3") - def test_config_crash(self): + def test_config_crash(self) -> None: # The internal '[run] _crash' setting can be used to artificially raise # exceptions from inside Coverage. cov = coverage.Coverage() @@ -725,20 +740,20 @@ with pytest.raises(Exception, match="Crashing because called by test_config_crash"): cov.start() - def test_config_crash_no_crash(self): + def test_config_crash_no_crash(self) -> None: # '[run] _crash' really checks the call stack. cov = coverage.Coverage() cov.set_option("run:_crash", "not_my_caller") cov.start() cov.stop() - def test_run_debug_sys(self): + def test_run_debug_sys(self) -> None: # https://github.com/nedbat/coveragepy/issues/907 cov = coverage.Coverage() cov.start() d = dict(cov.sys_info()) # pragma: nested cov.stop() # pragma: nested - assert d['data_file'].endswith(".coverage") + assert cast(str, d['data_file']).endswith(".coverage") class CurrentInstanceTest(CoverageTest): @@ -746,7 +761,7 @@ run_in_temp_dir = False - def assert_current_is_none(self, current): + def assert_current_is_none(self, current: Optional[Coverage]) -> None: """Assert that a current we expect to be None is correct.""" # During meta-coverage, the None answers will be wrong because the # overall coverage measurement will still be on the current-stack. @@ -755,7 +770,7 @@ if not env.METACOV: assert current is None - def test_current(self): + def test_current(self) -> None: cur0 = coverage.Coverage.current() self.assert_current_is_none(cur0) # Making an instance doesn't make it current. @@ -779,7 +794,7 @@ class NamespaceModuleTest(UsingModulesMixin, CoverageTest): """Test PEP-420 namespace modules.""" - def test_explicit_namespace_module(self): + def test_explicit_namespace_module(self) -> None: self.make_file("main.py", "import namespace_420\n") cov = coverage.Coverage() @@ -788,7 +803,7 @@ with pytest.raises(CoverageException, match=r"Module .* has no file"): cov.analysis(sys.modules['namespace_420']) - def test_bug_572(self): + def test_bug_572(self) -> None: self.make_file("main.py", "import namespace_420\n") # Use source=namespace_420 to trigger the check that used to fail, @@ -799,57 +814,67 @@ cov.report() -class IncludeOmitTestsMixin(UsingModulesMixin, CoverageTest): +class CoverageUsePkgs(Protocol): + """A number of test classes have the same helper method.""" + def coverage_usepkgs( + self, # pylint: disable=unused-argument + **kwargs: TCovKwargs, + ) -> Iterable[str]: + """Run coverage on usepkgs, return a line summary. kwargs are for Coverage(**kwargs).""" + return "" + + +class IncludeOmitTestsMixin(CoverageUsePkgs, UsingModulesMixin, CoverageTest): """Test methods for coverage methods taking include and omit.""" - def filenames_in(self, summary, filenames): - """Assert the `filenames` are in the keys of `summary`.""" + def filenames_in(self, summary: Iterable[str], filenames: str) -> None: + """Assert the `filenames` are in the `summary`.""" for filename in filenames.split(): assert filename in summary - def filenames_not_in(self, summary, filenames): - """Assert the `filenames` are not in the keys of `summary`.""" + def filenames_not_in(self, summary: Iterable[str], filenames: str) -> None: + """Assert the `filenames` are not in the `summary`.""" for filename in filenames.split(): assert filename not in summary - def test_nothing_specified(self): + def test_nothing_specified(self) -> None: result = self.coverage_usepkgs() self.filenames_in(result, "p1a p1b p2a p2b othera otherb osa osb") self.filenames_not_in(result, "p1c") # Because there was no source= specified, we don't search for - # unexecuted files. + # un-executed files. - def test_include(self): + def test_include(self) -> None: result = self.coverage_usepkgs(include=["*/p1a.py"]) self.filenames_in(result, "p1a") self.filenames_not_in(result, "p1b p1c p2a p2b othera otherb osa osb") - def test_include_2(self): + def test_include_2(self) -> None: result = self.coverage_usepkgs(include=["*a.py"]) self.filenames_in(result, "p1a p2a othera osa") self.filenames_not_in(result, "p1b p1c p2b otherb osb") - def test_include_as_string(self): + def test_include_as_string(self) -> None: result = self.coverage_usepkgs(include="*a.py") self.filenames_in(result, "p1a p2a othera osa") self.filenames_not_in(result, "p1b p1c p2b otherb osb") - def test_omit(self): + def test_omit(self) -> None: result = self.coverage_usepkgs(omit=["*/p1a.py"]) self.filenames_in(result, "p1b p2a p2b") self.filenames_not_in(result, "p1a p1c") - def test_omit_2(self): + def test_omit_2(self) -> None: result = self.coverage_usepkgs(omit=["*a.py"]) self.filenames_in(result, "p1b p2b otherb osb") self.filenames_not_in(result, "p1a p1c p2a othera osa") - def test_omit_as_string(self): + def test_omit_as_string(self) -> None: result = self.coverage_usepkgs(omit="*a.py") self.filenames_in(result, "p1b p2b otherb osb") self.filenames_not_in(result, "p1a p1c p2a othera osa") - def test_omit_and_include(self): + def test_omit_and_include(self) -> None: result = self.coverage_usepkgs(include=["*/p1*"], omit=["*/p1a.py"]) self.filenames_in(result, "p1b") self.filenames_not_in(result, "p1a p1c p2a p2b") @@ -858,7 +883,7 @@ class SourceIncludeOmitTest(IncludeOmitTestsMixin, CoverageTest): """Test using `source`, `include`, and `omit` when measuring code.""" - def setUp(self): + def setUp(self) -> None: super().setUp() # These tests use the TESTS_DIR/modules files, but they cd into it. To @@ -873,8 +898,8 @@ ) sys.path.insert(0, abs_file("tests_dir_modules")) - def coverage_usepkgs(self, **kwargs): - """Run coverage on usepkgs and return the line summary. + def coverage_usepkgs_counts(self, **kwargs: TCovKwargs) -> Dict[str, int]: + """Run coverage on usepkgs and return a line summary. Arguments are passed to the `coverage.Coverage` constructor. @@ -891,82 +916,86 @@ summary[k[:-3]] = v return summary - def test_source_include_exclusive(self): + def coverage_usepkgs(self, **kwargs: TCovKwargs) -> Iterable[str]: + summary = self.coverage_usepkgs_counts(**kwargs) + return list(summary) + + def test_source_include_exclusive(self) -> None: cov = coverage.Coverage(source=["pkg1"], include=["pkg2"]) with self.assert_warnings(cov, ["--include is ignored because --source is set"]): cov.start() cov.stop() # pragma: nested - def test_source_package_as_package(self): + def test_source_package_as_package(self) -> None: assert not os.path.isdir("pkg1") - lines = self.coverage_usepkgs(source=["pkg1"]) - self.filenames_in(lines, "p1a p1b") - self.filenames_not_in(lines, "p2a p2b othera otherb osa osb") - # Because source= was specified, we do search for unexecuted files. + lines = self.coverage_usepkgs_counts(source=["pkg1"]) + self.filenames_in(list(lines), "p1a p1b") + self.filenames_not_in(list(lines), "p2a p2b othera otherb osa osb") + # Because source= was specified, we do search for un-executed files. assert lines['p1c'] == 0 - def test_source_package_as_dir(self): + def test_source_package_as_dir(self) -> None: os.chdir("tests_dir_modules") assert os.path.isdir("pkg1") - lines = self.coverage_usepkgs(source=["pkg1"]) - self.filenames_in(lines, "p1a p1b") - self.filenames_not_in(lines, "p2a p2b othera otherb osa osb") - # Because source= was specified, we do search for unexecuted files. + lines = self.coverage_usepkgs_counts(source=["pkg1"]) + self.filenames_in(list(lines), "p1a p1b") + self.filenames_not_in(list(lines), "p2a p2b othera otherb osa osb") + # Because source= was specified, we do search for un-executed files. assert lines['p1c'] == 0 - def test_source_package_dotted_sub(self): - lines = self.coverage_usepkgs(source=["pkg1.sub"]) - self.filenames_not_in(lines, "p2a p2b othera otherb osa osb") - # Because source= was specified, we do search for unexecuted files. + def test_source_package_dotted_sub(self) -> None: + lines = self.coverage_usepkgs_counts(source=["pkg1.sub"]) + self.filenames_not_in(list(lines), "p2a p2b othera otherb osa osb") + # Because source= was specified, we do search for un-executed files. assert lines['runmod3'] == 0 - def test_source_package_dotted_p1b(self): - lines = self.coverage_usepkgs(source=["pkg1.p1b"]) - self.filenames_in(lines, "p1b") - self.filenames_not_in(lines, "p1a p1c p2a p2b othera otherb osa osb") + def test_source_package_dotted_p1b(self) -> None: + lines = self.coverage_usepkgs_counts(source=["pkg1.p1b"]) + self.filenames_in(list(lines), "p1b") + self.filenames_not_in(list(lines), "p1a p1c p2a p2b othera otherb osa osb") - def test_source_package_part_omitted(self): + def test_source_package_part_omitted(self) -> None: # https://github.com/nedbat/coveragepy/issues/218 # Used to be if you omitted something executed and inside the source, # then after it was executed but not recorded, it would be found in - # the search for unexecuted files, and given a score of 0%. + # the search for un-executed files, and given a score of 0%. # The omit arg is by path, so need to be in the modules directory. os.chdir("tests_dir_modules") - lines = self.coverage_usepkgs(source=["pkg1"], omit=["pkg1/p1b.py"]) - self.filenames_in(lines, "p1a") - self.filenames_not_in(lines, "p1b") + lines = self.coverage_usepkgs_counts(source=["pkg1"], omit=["pkg1/p1b.py"]) + self.filenames_in(list(lines), "p1a") + self.filenames_not_in(list(lines), "p1b") assert lines['p1c'] == 0 - def test_source_package_as_package_part_omitted(self): + def test_source_package_as_package_part_omitted(self) -> None: # https://github.com/nedbat/coveragepy/issues/638 - lines = self.coverage_usepkgs(source=["pkg1"], omit=["*/p1b.py"]) - self.filenames_in(lines, "p1a") - self.filenames_not_in(lines, "p1b") + lines = self.coverage_usepkgs_counts(source=["pkg1"], omit=["*/p1b.py"]) + self.filenames_in(list(lines), "p1a") + self.filenames_not_in(list(lines), "p1b") assert lines['p1c'] == 0 - def test_ambiguous_source_package_as_dir(self): + def test_ambiguous_source_package_as_dir(self) -> None: # pkg1 is a directory and a pkg, since we cd into tests_dir_modules/ambiguous os.chdir("tests_dir_modules/ambiguous") # pkg1 defaults to directory because tests_dir_modules/ambiguous/pkg1 exists - lines = self.coverage_usepkgs(source=["pkg1"]) - self.filenames_in(lines, "ambiguous") - self.filenames_not_in(lines, "p1a p1b p1c") + lines = self.coverage_usepkgs_counts(source=["pkg1"]) + self.filenames_in(list(lines), "ambiguous") + self.filenames_not_in(list(lines), "p1a p1b p1c") - def test_ambiguous_source_package_as_package(self): + def test_ambiguous_source_package_as_package(self) -> None: # pkg1 is a directory and a pkg, since we cd into tests_dir_modules/ambiguous os.chdir("tests_dir_modules/ambiguous") - lines = self.coverage_usepkgs(source_pkgs=["pkg1"]) - self.filenames_in(lines, "p1a p1b") - self.filenames_not_in(lines, "p2a p2b othera otherb osa osb ambiguous") - # Because source= was specified, we do search for unexecuted files. + lines = self.coverage_usepkgs_counts(source_pkgs=["pkg1"]) + self.filenames_in(list(lines), "p1a p1b") + self.filenames_not_in(list(lines), "p2a p2b othera otherb osa osb ambiguous") + # Because source= was specified, we do search for un-executed files. assert lines['p1c'] == 0 class ReportIncludeOmitTest(IncludeOmitTestsMixin, CoverageTest): """Tests of the report include/omit functionality.""" - def coverage_usepkgs(self, **kwargs): + def coverage_usepkgs(self, **kwargs: TCovKwargs) -> Iterable[str]: """Try coverage.report().""" cov = coverage.Coverage() cov.start() @@ -985,7 +1014,7 @@ """ - def coverage_usepkgs(self, **kwargs): + def coverage_usepkgs(self, **kwargs: TCovKwargs) -> Iterable[str]: """Try coverage.xml_report().""" cov = coverage.Coverage() cov.start() @@ -997,7 +1026,7 @@ class AnalysisTest(CoverageTest): """Test the numerical analysis of results.""" - def test_many_missing_branches(self): + def test_many_missing_branches(self) -> None: cov = coverage.Coverage(branch=True) self.make_file("missing.py", """\ @@ -1034,7 +1063,7 @@ way they do. """ - def pretend_to_be_nose_with_cover(self, erase=False, cd=False): + def pretend_to_be_nose_with_cover(self, erase: bool = False, cd: bool = False) -> None: """This is what the nose --with-cover plugin does.""" self.make_file("no_biggie.py", """\ a = 1 @@ -1065,17 +1094,17 @@ if cd: os.chdir("..") - def test_nose_plugin(self): + def test_nose_plugin(self) -> None: self.pretend_to_be_nose_with_cover() - def test_nose_plugin_with_erase(self): + def test_nose_plugin_with_erase(self) -> None: self.pretend_to_be_nose_with_cover(erase=True) - def test_nose_plugin_with_cd(self): + def test_nose_plugin_with_cd(self) -> None: # https://github.com/nedbat/coveragepy/issues/916 self.pretend_to_be_nose_with_cover(cd=True) - def pretend_to_be_pytestcov(self, append): + def pretend_to_be_pytestcov(self, append: bool) -> None: """Act like pytest-cov.""" self.make_file("prog.py", """\ a = 1 @@ -1110,16 +1139,17 @@ self.assert_file_count(".coverage", 0) self.assert_file_count(".coverage.*", 1) - def test_pytestcov_parallel(self): + def test_pytestcov_parallel(self) -> None: self.pretend_to_be_pytestcov(append=False) - def test_pytestcov_parallel_append(self): + def test_pytestcov_parallel_append(self) -> None: self.pretend_to_be_pytestcov(append=True) class ImmutableConfigTest(CoverageTest): """Check that reporting methods don't permanently change the configuration.""" - def test_config_doesnt_change(self): + + def test_config_doesnt_change(self) -> None: self.make_file("simple.py", "a = 1") cov = coverage.Coverage() self.start_import_stop(cov, "simple") @@ -1130,7 +1160,8 @@ class RelativePathTest(CoverageTest): """Tests of the relative_files setting.""" - def test_moving_stuff(self): + + def test_moving_stuff(self) -> None: # When using absolute file names, moving the source around results in # "No source for code" errors while reporting. self.make_file("foo.py", "a = 1") @@ -1149,7 +1180,7 @@ with pytest.raises(NoSource, match=expected): cov.report() - def test_moving_stuff_with_relative(self): + def test_moving_stuff_with_relative(self) -> None: # When using relative file names, moving the source around is fine. self.make_file("foo.py", "a = 1") self.make_file(".coveragerc", """\ @@ -1171,7 +1202,7 @@ res = cov.report() assert res == 100 - def test_combine_relative(self): + def test_combine_relative(self) -> None: self.make_file("foo.py", """\ import mod a = 1 @@ -1197,6 +1228,10 @@ cov.save() shutil.move(glob.glob(".coverage.*")[0], "..") + self.make_file("foo.py", "a = 1") + self.make_file("bar.py", "a = 1") + self.make_file("modsrc/__init__.py", "x = 1") + self.make_file(".coveragerc", """\ [run] relative_files = true @@ -1209,10 +1244,6 @@ cov.combine() cov.save() - self.make_file("foo.py", "a = 1") - self.make_file("bar.py", "a = 1") - self.make_file("modsrc/__init__.py", "x = 1") - cov = coverage.Coverage() cov.load() files = cov.get_data().measured_files() @@ -1220,7 +1251,7 @@ res = cov.report() assert res == 100 - def test_combine_no_suffix_multiprocessing(self): + def test_combine_no_suffix_multiprocessing(self) -> None: self.make_file(".coveragerc", """\ [run] branch = True @@ -1240,6 +1271,38 @@ self.assert_file_count(".coverage.*", 0) self.assert_exists(".coverage") + def test_files_up_one_level(self) -> None: + # https://github.com/nedbat/coveragepy/issues/1280 + self.make_file("src/mycode.py", """\ + def foo(): + return 17 + """) + self.make_file("test/test_it.py", """\ + from src.mycode import foo + assert foo() == 17 + """) + self.make_file("test/.coveragerc", """\ + [run] + parallel = True + relative_files = True + + [paths] + source = + ../src/ + */src + """) + os.chdir("test") + sys.path.insert(0, "..") + cov1 = coverage.Coverage() + self.start_import_stop(cov1, "test_it") + cov1.save() + cov2 = coverage.Coverage() + cov2.combine() + cov3 = coverage.Coverage() + cov3.load() + report = self.get_report(cov3) + assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" + class CombiningTest(CoverageTest): """More tests of combining data.""" @@ -1247,7 +1310,7 @@ B_LINES = {"b_or_c.py": [1, 2, 3, 4, 8, 9]} C_LINES = {"b_or_c.py": [1, 2, 3, 6, 7, 8, 9]} - def make_b_or_c_py(self): + def make_b_or_c_py(self) -> None: """Create b_or_c.py, used in a few of these tests.""" # "b_or_c.py b" will run 6 lines. # "b_or_c.py c" will run 7 lines. @@ -1264,7 +1327,7 @@ print('done') """) - def test_combine_parallel_data(self): + def test_combine_parallel_data(self) -> None: self.make_b_or_c_py() self.make_data_file(".coverage.b", lines=self.B_LINES) self.make_data_file(".coverage.c", lines=self.C_LINES) @@ -1294,7 +1357,7 @@ data.read() assert line_counts(data)['b_or_c.py'] == 8 - def test_combine_parallel_data_with_a_corrupt_file(self): + def test_combine_parallel_data_with_a_corrupt_file(self) -> None: self.make_b_or_c_py() self.make_data_file(".coverage.b", lines=self.B_LINES) self.make_data_file(".coverage.c", lines=self.C_LINES) @@ -1324,14 +1387,14 @@ data.read() assert line_counts(data)['b_or_c.py'] == 8 - def test_combine_no_usable_files(self): + def test_combine_no_usable_files(self) -> None: # https://github.com/nedbat/coveragepy/issues/629 self.make_b_or_c_py() self.make_data_file(".coverage", lines=self.B_LINES) # Make bogus data files. self.make_file(".coverage.bad1", "This isn't a coverage data file.") - self.make_file(".coverage.bad2", "This isn't a coverage data file.") + self.make_file(".coverage.bad2", "This isn't a coverage data file either.") # Combine the parallel coverage data files into .coverage, but nothing is readable. cov = coverage.Coverage() @@ -1356,7 +1419,7 @@ data.read() assert line_counts(data)['b_or_c.py'] == 6 - def test_combine_parallel_data_in_two_steps(self): + def test_combine_parallel_data_in_two_steps(self) -> None: self.make_b_or_c_py() self.make_data_file(".coverage.b", lines=self.B_LINES) @@ -1386,7 +1449,7 @@ data.read() assert line_counts(data)['b_or_c.py'] == 8 - def test_combine_parallel_data_no_append(self): + def test_combine_parallel_data_no_append(self) -> None: self.make_b_or_c_py() self.make_data_file(".coverage.b", lines=self.B_LINES) @@ -1413,7 +1476,7 @@ data.read() assert line_counts(data)['b_or_c.py'] == 7 - def test_combine_parallel_data_keep(self): + def test_combine_parallel_data_keep(self) -> None: self.make_b_or_c_py() self.make_data_file(".coverage.b", lines=self.B_LINES) self.make_data_file(".coverage.c", lines=self.C_LINES) diff -Nru python-coverage-6.5.0+dfsg1/tests/test_arcs.py python-coverage-7.2.7+dfsg1/tests/test_arcs.py --- python-coverage-6.5.0+dfsg1/tests/test_arcs.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_arcs.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,27 +3,31 @@ """Tests for coverage.py's arc measurement.""" +from __future__ import annotations + import pytest from tests.coveragetest import CoverageTest -from tests.helpers import assert_count_equal, xfail_pypy_3749 +from tests.helpers import assert_count_equal, xfail_pypy38 import coverage from coverage import env +from coverage.data import sorted_lines from coverage.files import abs_file -skip_cpython_92236 = pytest.mark.skipif( - env.PYVERSION == (3, 11, 0, "beta", 1, 0), - reason="Avoid a CPython bug: https://github.com/python/cpython/issues/92236", - # #92236 is fixed in https://github.com/python/cpython/pull/92722 - # and in https://github.com/python/cpython/pull/92772 +# When a try block ends, does the finally block (incorrectly) jump to the +# last statement, or does it go the line outside the try block that it +# should? +xfail_pypy_3882 = pytest.mark.xfail( + env.PYPY and env.PYVERSION[:2] == (3, 8) and env.PYPYVERSION >= (7, 3, 11), + reason="https://foss.heptapod.net/pypy/pypy/-/issues/3882", ) class SimpleArcTest(CoverageTest): """Tests for coverage.py's arc measurement.""" - def test_simple_sequence(self): + def test_simple_sequence(self) -> None: self.check_coverage("""\ a = 1 b = 2 @@ -46,7 +50,7 @@ arcz="-{0}2 23 35 5-{0}".format(line1) ) - def test_function_def(self): + def test_function_def(self) -> None: self.check_coverage("""\ def foo(): a = 2 @@ -55,7 +59,7 @@ """, arcz=".1 .2 14 2. 4.") - def test_if(self): + def test_if(self) -> None: self.check_coverage("""\ a = 1 if len([]) == 0: @@ -71,7 +75,7 @@ """, arcz=".1 12 23 24 34 4.", arcz_missing="23 34") - def test_if_else(self): + def test_if_else(self) -> None: self.check_coverage("""\ if len([]) == 0: a = 2 @@ -89,7 +93,7 @@ """, arcz=".1 12 25 14 45 5.", arcz_missing="12 25") - def test_compact_if(self): + def test_compact_if(self) -> None: self.check_coverage("""\ a = 1 if len([]) == 0: a = 2 @@ -106,7 +110,7 @@ """, arcz=".1 14 45 5. .2 2. 23 3.", arcz_missing="23 3.") - def test_multiline(self): + def test_multiline(self) -> None: self.check_coverage("""\ a = ( 2 + @@ -118,7 +122,7 @@ arcz=".1 15 5.", ) - def test_if_return(self): + def test_if_return(self) -> None: self.check_coverage("""\ def if_ret(a): if a: @@ -131,7 +135,7 @@ arcz=".1 16 67 7. .2 23 24 3. 45 5.", ) - def test_dont_confuse_exit_and_else(self): + def test_dont_confuse_exit_and_else(self) -> None: self.check_coverage("""\ def foo(): if foo: @@ -154,14 +158,10 @@ arcz=".1 16 6. .2 23 3. 25 5.", arcz_missing="25 5." ) - def test_what_is_the_sound_of_no_lines_clapping(self): - if env.JYTHON: - # Jython reports no lines for an empty file. - arcz_missing=".1 1." # pragma: only jython - elif env.PYBEHAVIOR.empty_is_empty: + def test_what_is_the_sound_of_no_lines_clapping(self) -> None: + if env.PYBEHAVIOR.empty_is_empty: arcz_missing=".1 1." else: - # Other Pythons report one line. arcz_missing="" self.check_coverage("""\ # __init__.py @@ -170,7 +170,7 @@ arcz_missing=arcz_missing, ) - def test_bug_1184(self): + def test_bug_1184(self) -> None: self.check_coverage("""\ def foo(x): if x: @@ -191,7 +191,7 @@ class WithTest(CoverageTest): """Arc-measuring tests involving context managers.""" - def test_with(self): + def test_with(self) -> None: arcz = ".1 .2 23 34 4. 16 6." if env.PYBEHAVIOR.exit_through_with: arcz = arcz.replace("4.", "42 2.") @@ -206,7 +206,7 @@ arcz=arcz, ) - def test_with_return(self): + def test_with_return(self) -> None: arcz = ".1 .2 23 34 4. 16 6." if env.PYBEHAVIOR.exit_through_with: arcz = arcz.replace("4.", "42 2.") @@ -221,7 +221,7 @@ arcz=arcz, ) - def test_bug_146(self): + def test_bug_146(self) -> None: # https://github.com/nedbat/coveragepy/issues/146 arcz = ".1 12 23 34 41 15 5." if env.PYBEHAVIOR.exit_through_with: @@ -236,7 +236,7 @@ arcz=arcz, ) - def test_nested_with_return(self): + def test_nested_with_return(self) -> None: arcz = ".1 .2 23 34 45 56 6. 18 8." if env.PYBEHAVIOR.exit_through_with: arcz = arcz.replace("6.", "64 42 2.") @@ -253,7 +253,7 @@ arcz=arcz, ) - def test_break_through_with(self): + def test_break_through_with(self) -> None: arcz = ".1 12 23 34 45 15 5." if env.PYBEHAVIOR.exit_through_with: arcz = arcz.replace("45", "42 25") @@ -268,7 +268,7 @@ arcz_missing="15", ) - def test_continue_through_with(self): + def test_continue_through_with(self) -> None: arcz = ".1 12 23 34 41 15 5." if env.PYBEHAVIOR.exit_through_with: arcz = arcz.replace("41", "42 21") @@ -283,7 +283,7 @@ ) # https://github.com/nedbat/coveragepy/issues/1270 - def test_raise_through_with(self): + def test_raise_through_with(self) -> None: if env.PYBEHAVIOR.exit_through_with: arcz = ".1 12 27 78 8. 9A A. -23 34 45 53 6-2" arcz_missing = "6-2 8." @@ -311,7 +311,7 @@ expected = "line 3 didn't jump to the function exit" assert self.get_missing_arc_description(cov, 3, -2) == expected - def test_untaken_raise_through_with(self): + def test_untaken_raise_through_with(self) -> None: if env.PYBEHAVIOR.exit_through_with: arcz = ".1 12 28 89 9. AB B. -23 34 45 56 53 63 37 7-2" arcz_missing = "56 63 AB B." @@ -341,7 +341,7 @@ class LoopArcTest(CoverageTest): """Arc-measuring tests involving loops.""" - def test_loop(self): + def test_loop(self) -> None: self.check_coverage("""\ for i in range(10): a = i @@ -357,7 +357,7 @@ """, arcz=".1 12 23 32 24 4.", arcz_missing="23 32") - def test_nested_loop(self): + def test_nested_loop(self) -> None: self.check_coverage("""\ for i in range(3): for j in range(3): @@ -367,7 +367,7 @@ arcz=".1 12 23 32 21 14 4.", ) - def test_break(self): + def test_break(self) -> None: if env.PYBEHAVIOR.omit_after_jump: arcz = ".1 12 23 35 15 5." arcz_missing = "15" @@ -385,7 +385,7 @@ arcz=arcz, arcz_missing=arcz_missing ) - def test_continue(self): + def test_continue(self) -> None: if env.PYBEHAVIOR.omit_after_jump: arcz = ".1 12 23 31 15 5." arcz_missing = "" @@ -403,7 +403,7 @@ arcz=arcz, arcz_missing=arcz_missing ) - def test_nested_breaks(self): + def test_nested_breaks(self) -> None: self.check_coverage("""\ for i in range(3): for j in range(3): @@ -415,7 +415,7 @@ """, arcz=".1 12 23 34 45 25 56 51 67 17 7.", arcz_missing="17 25") - def test_while_1(self): + def test_while_1(self) -> None: # With "while 1", the loop knows it's constant. if env.PYBEHAVIOR.keep_constant_test: arcz = ".1 12 23 34 45 36 62 57 7." @@ -435,7 +435,7 @@ arcz=arcz, ) - def test_while_true(self): + def test_while_true(self) -> None: # With "while True", 2.x thinks it's computation, # 3.x thinks it's constant. if env.PYBEHAVIOR.keep_constant_test: @@ -456,7 +456,7 @@ arcz=arcz, ) - def test_zero_coverage_while_loop(self): + def test_zero_coverage_while_loop(self) -> None: # https://github.com/nedbat/coveragepy/issues/502 self.make_file("main.py", "print('done')") self.make_file("zero.py", """\ @@ -478,7 +478,7 @@ squeezed = self.squeezed_lines(report) assert expected in squeezed[3] - def test_bug_496_continue_in_constant_while(self): + def test_bug_496_continue_in_constant_while(self) -> None: # https://github.com/nedbat/coveragepy/issues/496 # A continue in a while-true needs to jump to the right place. if env.PYBEHAVIOR.keep_constant_test: @@ -499,7 +499,7 @@ arcz=arcz ) - def test_for_if_else_for(self): + def test_for_if_else_for(self) -> None: self.check_coverage("""\ def branches_2(l): if l: @@ -526,7 +526,7 @@ arcz_missing="26 6." ) - def test_for_else(self): + def test_for_else(self) -> None: self.check_coverage("""\ def forelse(seq): for n in seq: @@ -541,7 +541,7 @@ arcz=".1 .2 23 32 34 47 26 67 7. 18 89 9." ) - def test_while_else(self): + def test_while_else(self) -> None: self.check_coverage("""\ def whileelse(seq): while seq: @@ -557,7 +557,11 @@ arcz=".1 19 9A A. .2 23 34 45 58 42 27 78 8.", ) - def test_confusing_for_loop_bug_175(self): + def test_confusing_for_loop_bug_175(self) -> None: + if env.PYBEHAVIOR.comprehensions_are_functions: + extra_arcz = " -22 2-2" + else: + extra_arcz = "" self.check_coverage("""\ o = [(1,2), (3,4)] o = [a for a in o] @@ -565,7 +569,7 @@ x = tup[0] y = tup[1] """, - arcz=".1 -22 2-2 12 23 34 45 53 3.", + arcz=".1 12 23 34 45 53 3." + extra_arcz, ) self.check_coverage("""\ o = [(1,2), (3,4)] @@ -573,12 +577,12 @@ x = tup[0] y = tup[1] """, - arcz=".1 12 -22 2-2 23 34 42 2.", + arcz=".1 12 23 34 42 2." + extra_arcz, ) # https://bugs.python.org/issue44672 @pytest.mark.xfail(env.PYVERSION < (3, 10), reason="<3.10 traced final pass incorrectly") - def test_incorrect_loop_exit_bug_1175(self): + def test_incorrect_loop_exit_bug_1175(self) -> None: self.check_coverage("""\ def wrong_loop(x): if x: @@ -595,7 +599,7 @@ # https://bugs.python.org/issue44672 @pytest.mark.xfail(env.PYVERSION < (3, 10), reason="<3.10 traced final pass incorrectly") - def test_incorrect_if_bug_1175(self): + def test_incorrect_if_bug_1175(self) -> None: self.check_coverage("""\ def wrong_loop(x): if x: @@ -610,8 +614,7 @@ arcz_missing="26 3. 6.", ) - @skip_cpython_92236 - def test_generator_expression(self): + def test_generator_expression(self) -> None: # Generator expression: self.check_coverage("""\ o = ((1,2), (3,4)) @@ -623,8 +626,7 @@ arcz=".1 -22 2-2 12 23 34 45 53 3.", ) - @skip_cpython_92236 - def test_generator_expression_another_way(self): + def test_generator_expression_another_way(self) -> None: # https://bugs.python.org/issue44450 # Generator expression: self.check_coverage("""\ @@ -639,7 +641,11 @@ arcz=".1 -22 2-2 12 25 56 67 75 5.", ) - def test_other_comprehensions(self): + def test_other_comprehensions(self) -> None: + if env.PYBEHAVIOR.comprehensions_are_functions: + extra_arcz = " -22 2-2" + else: + extra_arcz = "" # Set comprehension: self.check_coverage("""\ o = ((1,2), (3,4)) @@ -648,7 +654,7 @@ x = tup[0] y = tup[1] """, - arcz=".1 -22 2-2 12 23 34 45 53 3.", + arcz=".1 12 23 34 45 53 3." + extra_arcz, ) # Dict comprehension: self.check_coverage("""\ @@ -658,10 +664,14 @@ x = tup[0] y = tup[1] """, - arcz=".1 -22 2-2 12 23 34 45 53 3.", + arcz=".1 12 23 34 45 53 3." + extra_arcz, ) - def test_multiline_dict_comp(self): + def test_multiline_dict_comp(self) -> None: + if env.PYBEHAVIOR.comprehensions_are_functions: + extra_arcz = " 2-2" + else: + extra_arcz = "" # Multiline dict comp: self.check_coverage("""\ # comment @@ -676,7 +686,7 @@ } x = 11 """, - arcz="-22 2B B-2 2-2" + arcz="-22 2B B-2" + extra_arcz, ) # Multi dict comp: self.check_coverage("""\ @@ -696,14 +706,14 @@ } x = 15 """, - arcz="-22 2F F-2 2-2" + arcz="-22 2F F-2" + extra_arcz, ) class ExceptionArcTest(CoverageTest): """Arc-measuring tests involving exception handling.""" - def test_try_except(self): + def test_try_except(self) -> None: self.check_coverage("""\ a, b = 1, 1 try: @@ -714,7 +724,7 @@ """, arcz=".1 12 23 36 45 56 6.", arcz_missing="45 56") - def test_raise_followed_by_statement(self): + def test_raise_followed_by_statement(self) -> None: if env.PYBEHAVIOR.omit_after_jump: arcz = ".1 12 23 34 46 67 78 8." arcz_missing = "" @@ -734,7 +744,7 @@ arcz=arcz, arcz_missing=arcz_missing, ) - def test_hidden_raise(self): + def test_hidden_raise(self) -> None: self.check_coverage("""\ a, b = 1, 1 def oops(x): @@ -752,7 +762,7 @@ arcz_missing="3-2 78 8B", arcz_unpredicted="79", ) - def test_except_with_type(self): + def test_except_with_type(self) -> None: self.check_coverage("""\ a, b = 1, 1 def oops(x): @@ -773,7 +783,8 @@ arcz_unpredicted="8A", ) - def test_try_finally(self): + @xfail_pypy_3882 + def test_try_finally(self) -> None: self.check_coverage("""\ a, c = 1, 1 try: @@ -815,7 +826,8 @@ arcz_missing="", ) - def test_finally_in_loop(self): + @xfail_pypy_3882 + def test_finally_in_loop(self) -> None: self.check_coverage("""\ a, c, d, i = 1, 1, 1, 99 try: @@ -854,7 +866,8 @@ ) - def test_break_through_finally(self): + @xfail_pypy_3882 + def test_break_through_finally(self) -> None: arcz = ".1 12 23 34 3D 45 56 67 68 7A AD 8A A3 BC CD D." if env.PYBEHAVIOR.finally_jumps_back: arcz = arcz.replace("AD", "A7 7D") @@ -877,7 +890,7 @@ arcz_missing="3D BC CD", ) - def test_break_continue_without_finally(self): + def test_break_continue_without_finally(self) -> None: self.check_coverage("""\ a, c, d, i = 1, 1, 1, 99 try: @@ -897,14 +910,15 @@ arcz_missing="3D 9A A3 BC CD", ) - def test_continue_through_finally(self): + @xfail_pypy_3882 + def test_continue_through_finally(self) -> None: arcz = ".1 12 23 34 3D 45 56 67 68 7A 8A A3 BC CD D." if env.PYBEHAVIOR.finally_jumps_back: arcz += " 73 A7" self.check_coverage("""\ a, b, c, d, i = 1, 1, 1, 1, 99 try: - for i in range(5): + for i in range(3): try: a = 5 if i > 0: @@ -920,7 +934,7 @@ arcz_missing="BC CD", ) - def test_finally_in_loop_bug_92(self): + def test_finally_in_loop_bug_92(self) -> None: self.check_coverage("""\ for i in range(5): try: @@ -933,7 +947,7 @@ arcz=".1 12 23 35 56 61 17 7.", ) - def test_bug_212(self): + def test_bug_212(self) -> None: # "except Exception as e" is crucial here. # Bug 212 said that the "if exc" line was incorrectly marked as only # partially covered. @@ -958,7 +972,7 @@ arcz_unpredicted="CD", ) - def test_except_finally(self): + def test_except_finally(self) -> None: self.check_coverage("""\ a, b, c = 1, 1, 1 try: @@ -987,7 +1001,7 @@ arcz=".1 12 -23 3-2 24 45 56 67 7B 89 9B BC C.", arcz_missing="67 7B", arcz_unpredicted="68") - def test_multiple_except_clauses(self): + def test_multiple_except_clauses(self) -> None: self.check_coverage("""\ a, b, c = 1, 1, 1 try: @@ -1055,7 +1069,7 @@ arcz_unpredicted="45 7A AB", ) - def test_return_finally(self): + def test_return_finally(self) -> None: arcz = ".1 12 29 9A AB BC C-1 -23 34 45 7-2 57 38 8-2" if env.PYBEHAVIOR.finally_jumps_back: arcz = arcz.replace("7-2", "75 5-2") @@ -1076,7 +1090,8 @@ arcz=arcz, ) - def test_except_jump_finally(self): + @xfail_pypy_3882 + def test_except_jump_finally(self) -> None: arcz = ( ".1 1Q QR RS ST TU U. " + ".2 23 34 45 56 4O 6L " + @@ -1123,7 +1138,8 @@ arcz_unpredicted="67", ) - def test_else_jump_finally(self): + @xfail_pypy_3882 + def test_else_jump_finally(self) -> None: arcz = ( ".1 1S ST TU UV VW W. " + ".2 23 34 45 56 6A 78 8N 4Q " + @@ -1176,8 +1192,7 @@ class YieldTest(CoverageTest): """Arc tests for generators.""" - @skip_cpython_92236 - def test_yield_in_loop(self): + def test_yield_in_loop(self) -> None: self.check_coverage("""\ def gen(inp): for n in inp: @@ -1188,8 +1203,7 @@ arcz=".1 .2 23 2. 32 15 5.", ) - @skip_cpython_92236 - def test_padded_yield_in_loop(self): + def test_padded_yield_in_loop(self) -> None: self.check_coverage("""\ def gen(inp): i = 2 @@ -1204,8 +1218,7 @@ arcz=".1 19 9. .2 23 34 45 56 63 37 7.", ) - @skip_cpython_92236 - def test_bug_308(self): + def test_bug_308(self) -> None: self.check_coverage("""\ def run(): for i in range(10): @@ -1239,8 +1252,7 @@ arcz=".1 14 45 54 4. .2 2. -22 2-2", ) - @skip_cpython_92236 - def test_bug_324(self): + def test_bug_324(self) -> None: # This code is tricky: the list() call pulls all the values from gen(), # but each of them is a generator itself that is never iterated. As a # result, the generator expression on line 3 is never entered or run. @@ -1258,8 +1270,7 @@ arcz_missing="-33 3-3", ) - @skip_cpython_92236 - def test_coroutines(self): + def test_coroutines(self) -> None: self.check_coverage("""\ def double_inputs(): while len([1]): # avoid compiler differences @@ -1278,8 +1289,7 @@ ) assert self.stdout() == "20\n12\n" - @skip_cpython_92236 - def test_yield_from(self): + def test_yield_from(self) -> None: self.check_coverage("""\ def gen(inp): i = 2 @@ -1294,8 +1304,7 @@ arcz=".1 19 9. .2 23 34 45 56 63 37 7.", ) - @skip_cpython_92236 - def test_abandoned_yield(self): + def test_abandoned_yield(self) -> None: # https://github.com/nedbat/coveragepy/issues/440 self.check_coverage("""\ def gen(): @@ -1315,7 +1324,7 @@ @pytest.mark.skipif(not env.PYBEHAVIOR.match_case, reason="Match-case is new in 3.10") class MatchCaseTest(CoverageTest): """Tests of match-case.""" - def test_match_case_with_default(self): + def test_match_case_with_default(self) -> None: self.check_coverage("""\ for command in ["huh", "go home", "go n"]: match command.split(): @@ -1331,7 +1340,7 @@ ) assert self.stdout() == "default\nno go\ngo: n\n" - def test_match_case_with_wildcard(self): + def test_match_case_with_wildcard(self) -> None: self.check_coverage("""\ for command in ["huh", "go home", "go n"]: match command.split(): @@ -1347,7 +1356,7 @@ ) assert self.stdout() == "default: ['huh']\nno go\ngo: n\n" - def test_match_case_without_wildcard(self): + def test_match_case_without_wildcard(self) -> None: self.check_coverage("""\ match = None for command in ["huh", "go home", "go n"]: @@ -1362,11 +1371,24 @@ ) assert self.stdout() == "None\nno go\ngo: n\n" + def test_absurd_wildcard(self) -> None: + # https://github.com/nedbat/coveragepy/issues/1421 + self.check_coverage("""\ + def absurd(x): + match x: + case (3 | 99 | (999 | _)): + print("default") + absurd(5) + """, + arcz=".1 15 5. .2 23 34 4.", + ) + assert self.stdout() == "default\n" + class OptimizedIfTest(CoverageTest): """Tests of if statements being optimized away.""" - def test_optimized_away_if_0(self): + def test_optimized_away_if_0(self) -> None: if env.PYBEHAVIOR.keep_constant_test: lines = [1, 2, 3, 4, 8, 9] arcz = ".1 12 23 24 34 48 49 89 9." @@ -1395,7 +1417,7 @@ arcz_missing=arcz_missing, ) - def test_optimized_away_if_1(self): + def test_optimized_away_if_1(self) -> None: if env.PYBEHAVIOR.keep_constant_test: lines = [1, 2, 3, 4, 5, 6, 9] arcz = ".1 12 23 24 34 45 49 56 69 59 9." @@ -1424,7 +1446,7 @@ arcz_missing=arcz_missing, ) - def test_optimized_away_if_1_no_else(self): + def test_optimized_away_if_1_no_else(self) -> None: if env.PYBEHAVIOR.keep_constant_test: lines = [1, 2, 3, 4, 5] arcz = ".1 12 23 25 34 45 5." @@ -1448,7 +1470,7 @@ arcz_missing=arcz_missing, ) - def test_optimized_if_nested(self): + def test_optimized_if_nested(self) -> None: if env.PYBEHAVIOR.keep_constant_test: lines = [1, 2, 8, 11, 12, 13, 14, 15] arcz = ".1 12 28 2F 8B 8F BC CD DE EF F." @@ -1483,7 +1505,7 @@ arcz_missing=arcz_missing, ) - def test_dunder_debug(self): + def test_dunder_debug(self) -> None: # Since some of our tests use __debug__, let's make sure it is true as # we expect assert __debug__ @@ -1499,7 +1521,7 @@ """ ) - def test_if_debug(self): + def test_if_debug(self) -> None: if env.PYBEHAVIOR.optimize_if_debug: arcz = ".1 12 24 41 26 61 1." arcz_missing = "" @@ -1518,7 +1540,8 @@ arcz_missing=arcz_missing, ) - def test_if_not_debug(self): + @xfail_pypy_3882 + def test_if_not_debug(self) -> None: if env.PYBEHAVIOR.optimize_if_not_debug == 1: arcz = ".1 12 23 34 42 37 72 28 8." elif env.PYBEHAVIOR.optimize_if_not_debug == 2: @@ -1544,7 +1567,7 @@ class MiscArcTest(CoverageTest): """Miscellaneous arc-measuring tests.""" - def test_dict_literal(self): + def test_dict_literal(self) -> None: self.check_coverage("""\ d = { 'a': 2, @@ -1572,7 +1595,7 @@ arcz=".1 19 9.", ) - def test_unpacked_literals(self): + def test_unpacked_literals(self) -> None: self.check_coverage("""\ d = { 'a': 2, @@ -1603,7 +1626,7 @@ ) @pytest.mark.parametrize("n", [10, 50, 100, 500, 1000, 2000, 10000]) - def test_pathologically_long_code_object(self, n): + def test_pathologically_long_code_object(self, n: int) -> None: # https://github.com/nedbat/coveragepy/issues/359 # Long code objects sometimes cause problems. Originally, it was # due to EXTENDED_ARG bytes codes. Then it showed a mistake in @@ -1622,8 +1645,7 @@ self.check_coverage(code, arcs=[(-1, 1), (1, 2*n+4), (2*n+4, -1)]) assert self.stdout() == f"{n}\n" - @skip_cpython_92236 - def test_partial_generators(self): + def test_partial_generators(self) -> None: # https://github.com/nedbat/coveragepy/issues/475 # Line 2 is executed completely. # Line 3 is started but not finished, because zip ends before it finishes. @@ -1649,8 +1671,7 @@ class DecoratorArcTest(CoverageTest): """Tests of arcs with decorators.""" - @xfail_pypy_3749 - def test_function_decorator(self): + def test_function_decorator(self) -> None: arcz = ( ".1 16 67 7A AE EF F. " # main line ".2 24 4. -23 3-2 " # decorators @@ -1678,8 +1699,8 @@ arcz=arcz, ) - @xfail_pypy_3749 - def test_class_decorator(self): + @xfail_pypy38 + def test_class_decorator(self) -> None: arcz = ( ".1 16 67 6D 7A AE E. " # main line ".2 24 4. -23 3-2 " # decorators @@ -1706,8 +1727,7 @@ arcz=arcz, ) - @xfail_pypy_3749 - def test_bug_466a(self): + def test_bug_466a(self) -> None: # A bad interaction between decorators and multi-line list assignments, # believe it or not...! arcz = ".1 1A A. 13 3. -35 58 8-3 " @@ -1731,8 +1751,7 @@ arcz=arcz, ) - @xfail_pypy_3749 - def test_bug_466b(self): + def test_bug_466b(self) -> None: # A bad interaction between decorators and multi-line list assignments, # believe it or not...! arcz = ".1 1A A. 13 3. -35 58 8-3 " @@ -1759,7 +1778,7 @@ class LambdaArcTest(CoverageTest): """Tests of lambdas""" - def test_multiline_lambda(self): + def test_multiline_lambda(self) -> None: self.check_coverage("""\ fn = (lambda x: x + 2 @@ -1783,7 +1802,7 @@ arcz="-22 2A A-2 2-2", ) - def test_unused_lambdas_are_confusing_bug_90(self): + def test_unused_lambdas_are_confusing_bug_90(self) -> None: self.check_coverage("""\ a = 1 fn = lambda x: x @@ -1792,7 +1811,7 @@ arcz=".1 12 -22 2-2 23 3.", arcz_missing="-22 2-2", ) - def test_raise_with_lambda_looks_like_partial_branch(self): + def test_raise_with_lambda_looks_like_partial_branch(self) -> None: self.check_coverage("""\ def ouch(fn): 2/0 @@ -1813,7 +1832,7 @@ arcz_unpredicted="58", ) - def test_lambda_in_dict(self): + def test_lambda_in_dict(self) -> None: self.check_coverage("""\ x = 1 x = 2 @@ -1843,8 +1862,7 @@ """Tests of the new async and await keywords in Python 3.5""" @xfail_eventlet_670 - @skip_cpython_92236 - def test_async(self): + def test_async(self) -> None: self.check_coverage("""\ import asyncio @@ -1871,8 +1889,7 @@ assert self.stdout() == "Compute 1 + 2 ...\n1 + 2 = 3\n" @xfail_eventlet_670 - @skip_cpython_92236 - def test_async_for(self): + def test_async_for(self) -> None: self.check_coverage("""\ import asyncio @@ -1909,7 +1926,7 @@ ) assert self.stdout() == "a\nb\nc\n.\n" - def test_async_with(self): + def test_async_with(self) -> None: if env.PYBEHAVIOR.exit_through_with: arcz = ".1 1. .2 23 32 2." arcz_missing = ".2 23 32 2." @@ -1925,8 +1942,7 @@ arcz_missing=arcz_missing, ) - @xfail_pypy_3749 - def test_async_decorator(self): + def test_async_decorator(self) -> None: arcz = ".1 14 4. .2 2. -46 6-4 " if env.PYBEHAVIOR.trace_decorated_def: arcz = arcz.replace("4.", "45 5.") @@ -1947,8 +1963,7 @@ # https://github.com/nedbat/coveragepy/issues/1158 # https://bugs.python.org/issue44621 @pytest.mark.skipif(env.PYVERSION[:2] == (3, 9), reason="avoid a 3.9 bug: 44621") - @skip_cpython_92236 - def test_bug_1158(self): + def test_bug_1158(self) -> None: self.check_coverage("""\ import asyncio @@ -1973,8 +1988,7 @@ # https://github.com/nedbat/coveragepy/issues/1176 # https://bugs.python.org/issue44622 @xfail_eventlet_670 - @skip_cpython_92236 - def test_bug_1176(self): + def test_bug_1176(self) -> None: self.check_coverage("""\ import asyncio @@ -1992,7 +2006,7 @@ assert self.stdout() == "12\n" # https://github.com/nedbat/coveragepy/issues/1205 - def test_bug_1205(self): + def test_bug_1205(self) -> None: self.check_coverage("""\ def func(): if T(2): @@ -2016,7 +2030,7 @@ class AnnotationTest(CoverageTest): """Tests using type annotations.""" - def test_annotations(self): + def test_annotations(self) -> None: self.check_coverage("""\ def f(x:str, y:int) -> str: a:int = 2 @@ -2031,7 +2045,7 @@ class ExcludeTest(CoverageTest): """Tests of exclusions to indicate known partial branches.""" - def test_default(self): + def test_default(self) -> None: # A number of forms of pragma comment are accepted. self.check_coverage("""\ a = 1 @@ -2048,7 +2062,7 @@ arcz=".1 12 23 24 34 45 56 57 67 78 89 9. 8.", ) - def test_custom_pragmas(self): + def test_custom_pragmas(self) -> None: self.check_coverage("""\ a = 1 while a: # [only some] @@ -2065,7 +2079,7 @@ class LineDataTest(CoverageTest): """Tests that line_data gives us what we expect.""" - def test_branch(self): + def test_branch(self) -> None: cov = coverage.Coverage(branch=True) self.make_file("fun1.py", """\ @@ -2079,5 +2093,5 @@ self.start_import_stop(cov, "fun1") data = cov.get_data() - fun1_lines = data.lines(abs_file("fun1.py")) + fun1_lines = sorted_lines(data, abs_file("fun1.py")) assert_count_equal(fun1_lines, [1, 2, 5]) diff -Nru python-coverage-6.5.0+dfsg1/tests/test_cmdline.py python-coverage-7.2.7+dfsg1/tests/test_cmdline.py --- python-coverage-6.5.0+dfsg1/tests/test_cmdline.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_cmdline.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Test cmdline.py for coverage.py.""" +from __future__ import annotations + import ast import pprint import re @@ -10,6 +12,8 @@ import textwrap from unittest import mock +from typing import Any, List, Mapping, Optional, Tuple + import pytest import coverage @@ -18,6 +22,7 @@ from coverage.control import DEFAULT_DATAFILE from coverage.config import CoverageConfig from coverage.exceptions import _ExceptionDuringRun +from coverage.types import TConfigValueIn, TConfigValueOut from coverage.version import __url__ from tests.coveragetest import CoverageTest, OK, ERR, command_line @@ -43,8 +48,8 @@ ) _defaults.Coverage().report( ignore_errors=None, include=None, omit=None, morfs=[], - show_missing=None, skip_covered=None, contexts=None, skip_empty=None, precision=None, - sort=None, + show_missing=None, skip_covered=None, contexts=None, skip_empty=None, + precision=None, sort=None, output_format=None, ) _defaults.Coverage().xml_report( ignore_errors=None, include=None, omit=None, morfs=[], outfile=None, @@ -67,7 +72,7 @@ DEFAULT_KWARGS = {name: kw for name, _, kw in _defaults.mock_calls} - def model_object(self): + def model_object(self) -> mock.Mock: """Return a Mock suitable for use in CoverageScript.""" mk = mock.Mock() @@ -90,7 +95,11 @@ # Global names in cmdline.py that will be mocked during the tests. MOCK_GLOBALS = ['Coverage', 'PyRunner', 'show_help'] - def mock_command_line(self, args, options=None): + def mock_command_line( + self, + args: str, + options: Optional[Mapping[str, TConfigValueIn]] = None, + ) -> Tuple[mock.Mock, int]: """Run `args` through the command line, with a Mock. `options` is a dict of names and values to pass to `set_option`. @@ -118,7 +127,13 @@ return mk, ret - def cmd_executes(self, args, code, ret=OK, options=None): + def cmd_executes( + self, + args: str, + code: str, + ret: int = OK, + options: Optional[Mapping[str, TConfigValueIn]] = None, + ) -> None: """Assert that the `args` end up executing the sequence in `code`.""" called, status = self.mock_command_line(args, options=options) assert status == ret, f"Wrong status: got {status!r}, wanted {ret!r}" @@ -127,7 +142,7 @@ code = textwrap.dedent(code) expected = self.model_object() globs = {n: getattr(expected, n) for n in self.MOCK_GLOBALS} - code_obj = compile(code, "", "exec") + code_obj = compile(code, "", "exec", dont_inherit=True) eval(code_obj, globs, {}) # pylint: disable=eval-used # Many of our functions take a lot of arguments, and cmdline.py @@ -140,14 +155,14 @@ self.assert_same_mock_calls(expected, called) - def cmd_executes_same(self, args1, args2): + def cmd_executes_same(self, args1: str, args2: str) -> None: """Assert that the `args1` executes the same as `args2`.""" m1, r1 = self.mock_command_line(args1) m2, r2 = self.mock_command_line(args2) assert r1 == r2 self.assert_same_mock_calls(m1, m2) - def assert_same_mock_calls(self, m1, m2): + def assert_same_mock_calls(self, m1: mock.Mock, m2: mock.Mock) -> None: """Assert that `m1.mock_calls` and `m2.mock_calls` are the same.""" # Use a real equality comparison, but if it fails, use a nicer assert # so we can tell what's going on. We have to use the real == first due @@ -157,7 +172,13 @@ pp2 = pprint.pformat(m2.mock_calls) assert pp1+'\n' == pp2+'\n' - def cmd_help(self, args, help_msg=None, topic=None, ret=ERR): + def cmd_help( + self, + args: str, + help_msg: Optional[str] = None, + topic: Optional[str] = None, + ret: int = ERR, + ) -> None: """Run a command line, and check that it prints the right help. Only the last function call in the mock is checked, which should be the @@ -174,7 +195,7 @@ class BaseCmdLineTestTest(BaseCmdLineTest): """Tests that our BaseCmdLineTest helpers work.""" - def test_cmd_executes_same(self): + def test_cmd_executes_same(self) -> None: # All the other tests here use self.cmd_executes_same in successful # ways, so here we just check that it fails. with pytest.raises(AssertionError): @@ -184,7 +205,7 @@ class CmdLineTest(BaseCmdLineTest): """Tests of the coverage.py command line.""" - def test_annotate(self): + def test_annotate(self) -> None: # coverage annotate [-d DIR] [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("annotate", """\ cov = Coverage() @@ -222,7 +243,7 @@ cov.annotate(morfs=["mod1", "mod2", "mod3"]) """) - def test_combine(self): + def test_combine(self) -> None: # coverage combine with args self.cmd_executes("combine datadir1", """\ cov = Coverage() @@ -259,7 +280,7 @@ cov.save() """) - def test_combine_doesnt_confuse_options_with_args(self): + def test_combine_doesnt_confuse_options_with_args(self) -> None: # https://github.com/nedbat/coveragepy/issues/385 self.cmd_executes("combine --rcfile cov.ini", """\ cov = Coverage(config_file='cov.ini') @@ -277,33 +298,39 @@ ("debug foo", "Don't know what you mean by 'foo'"), ("debug sys config", "Only one topic at a time, please"), ]) - def test_debug(self, cmd, output): + def test_debug(self, cmd: str, output: str) -> None: self.cmd_help(cmd, output) - def test_debug_sys(self): + def test_debug_sys(self) -> None: self.command_line("debug sys") out = self.stdout() assert "version:" in out assert "data_file:" in out - def test_debug_config(self): + def test_debug_config(self) -> None: self.command_line("debug config") out = self.stdout() assert "cover_pylib:" in out assert "skip_covered:" in out assert "skip_empty:" in out - def test_debug_pybehave(self): + def test_debug_pybehave(self) -> None: self.command_line("debug pybehave") out = self.stdout() assert " CPYTHON:" in out assert " PYVERSION:" in out assert " pep626:" in out + + # Some things that shouldn't appear.. + assert "typing." not in out # import from typing + assert ": <" not in out # objects without a good repr + + # It should report PYVERSION correctly. pyversion = re_line(r" PYVERSION:", out) vtuple = ast.literal_eval(pyversion.partition(":")[-1].strip()) assert vtuple[:5] == sys.version_info - def test_debug_premain(self): + def test_debug_premain(self) -> None: self.command_line("debug premain") out = self.stdout() # ... many lines ... @@ -317,7 +344,7 @@ assert re.search(r"(?m)^\s+command_line : .*[/\\]coverage[/\\]cmdline.py:\d+$", out) assert re.search(r"(?m)^\s+do_debug : .*[/\\]coverage[/\\]cmdline.py:\d+$", out) - def test_erase(self): + def test_erase(self) -> None: # coverage erase self.cmd_executes("erase", """\ cov = Coverage() @@ -328,23 +355,23 @@ cov.erase() """) - def test_version(self): + def test_version(self) -> None: # coverage --version self.cmd_help("--version", topic="version", ret=OK) - def test_help_option(self): + def test_help_option(self) -> None: # coverage -h self.cmd_help("-h", topic="help", ret=OK) self.cmd_help("--help", topic="help", ret=OK) - def test_help_command(self): + def test_help_command(self) -> None: self.cmd_executes("help", "show_help(topic='help')") - def test_cmd_help(self): + def test_cmd_help(self) -> None: self.cmd_executes("run --help", "show_help(parser='')") self.cmd_executes_same("help run", "run --help") - def test_html(self): + def test_html(self) -> None: # coverage html -d DIR [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("html", """\ cov = Coverage() @@ -402,7 +429,7 @@ cov.html_report() """) - def test_json(self): + def test_json(self) -> None: # coverage json [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("json", """\ cov = Coverage() @@ -465,7 +492,7 @@ cov.json_report() """) - def test_lcov(self): + def test_lcov(self) -> None: # coverage lcov [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("lcov", """\ cov = Coverage() @@ -508,7 +535,7 @@ cov.lcov_report() """) - def test_report(self): + def test_report(self) -> None: # coverage report [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...] self.cmd_executes("report", """\ cov = Coverage() @@ -585,8 +612,13 @@ cov.load() cov.report(show_missing=None) """) + self.cmd_executes("report --format=markdown", """\ + cov = Coverage() + cov.load() + cov.report(output_format="markdown") + """) - def test_run(self): + def test_run(self) -> None: # coverage run [-p] [-L] [--timid] MODULE.py [ARG1 ARG2 ...] # run calls coverage.erase first. @@ -721,7 +753,7 @@ cov.save() """) - def test_multiprocessing_needs_config_file(self): + def test_multiprocessing_needs_config_file(self) -> None: # You can't use command-line args to add options to multiprocessing # runs, since they won't make it to the subprocesses. You need to use a # config file. @@ -731,7 +763,7 @@ assert msg in err assert "Remove --branch from the command line." in err - def test_run_debug(self): + def test_run_debug(self) -> None: self.cmd_executes("run --debug=opt1 foo.py", """\ cov = Coverage(debug=["opt1"]) runner = PyRunner(['foo.py'], as_module=False) @@ -751,7 +783,7 @@ cov.save() """) - def test_run_module(self): + def test_run_module(self) -> None: self.cmd_executes("run -m mymodule", """\ cov = Coverage() runner = PyRunner(['mymodule'], as_module=True) @@ -781,11 +813,11 @@ """) self.cmd_executes_same("run -m mymodule", "run --module mymodule") - def test_run_nothing(self): + def test_run_nothing(self) -> None: self.command_line("run", ret=ERR) assert "Nothing to do" in self.stderr() - def test_run_from_config(self): + def test_run_from_config(self) -> None: options = {"run:command_line": "myprog.py a 123 'a quoted thing' xyz"} self.cmd_executes("run", """\ cov = Coverage() @@ -799,7 +831,7 @@ options=options, ) - def test_run_module_from_config(self): + def test_run_module_from_config(self) -> None: self.cmd_executes("run", """\ cov = Coverage() runner = PyRunner(['mymodule', 'thing1', 'thing2'], as_module=True) @@ -812,7 +844,7 @@ options={"run:command_line": "-m mymodule thing1 thing2"}, ) - def test_run_from_config_but_empty(self): + def test_run_from_config_but_empty(self) -> None: self.cmd_executes("run", """\ cov = Coverage() show_help('Nothing to do.') @@ -821,7 +853,7 @@ options={"run:command_line": ""}, ) - def test_run_dashm_only(self): + def test_run_dashm_only(self) -> None: self.cmd_executes("run -m", """\ cov = Coverage() show_help('No module specified for -m') @@ -836,11 +868,11 @@ options={"run:command_line": "myprog.py"} ) - def test_cant_append_parallel(self): + def test_cant_append_parallel(self) -> None: self.command_line("run --append --parallel-mode foo.py", ret=ERR) assert "Can't append to data files in parallel mode." in self.stderr() - def test_xml(self): + def test_xml(self) -> None: # coverage xml [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("xml", """\ cov = Coverage() @@ -893,10 +925,10 @@ cov.xml_report() """) - def test_no_arguments_at_all(self): + def test_no_arguments_at_all(self) -> None: self.cmd_help("", topic="minimum_help", ret=OK) - def test_bad_command(self): + def test_bad_command(self) -> None: self.cmd_help("xyzzy", "Unknown command: 'xyzzy'") @@ -905,7 +937,7 @@ run_in_temp_dir = True - def test_debug_data(self): + def test_debug_data(self) -> None: data = self.make_data_file( lines={ "file1.py": range(1, 18), @@ -924,7 +956,7 @@ file2.py: 23 lines """) - def test_debug_data_with_no_data_file(self): + def test_debug_data_with_no_data_file(self) -> None: data = self.make_data_file() self.command_line("debug data") assert self.stdout() == textwrap.dedent(f"""\ @@ -933,7 +965,7 @@ No data collected: file doesn't exist """) - def test_debug_combinable_data(self): + def test_debug_combinable_data(self) -> None: data1 = self.make_data_file(lines={"file1.py": range(1, 18), "file2.py": [1]}) data2 = self.make_data_file(suffix="123", lines={"file2.py": range(1, 10)}) @@ -956,13 +988,13 @@ class CmdLineStdoutTest(BaseCmdLineTest): """Test the command line with real stdout output.""" - def test_minimum_help(self): + def test_minimum_help(self) -> None: self.command_line("") out = self.stdout() assert "Code coverage for Python" in out assert out.count("\n") < 4 - def test_version(self): + def test_version(self) -> None: self.command_line("--version") out = self.stdout() assert "ersion " in out @@ -972,8 +1004,7 @@ assert "without C extension" in out assert out.count("\n") < 4 - @pytest.mark.skipif(env.JYTHON, reason="Jython gets mad if you patch sys.argv") - def test_help_contains_command_name(self): + def test_help_contains_command_name(self) -> None: # Command name should be present in help output. fake_command_path = os_sep("lorem/ipsum/dolor") expected_command_name = "dolor" @@ -983,8 +1014,7 @@ out = self.stdout() assert expected_command_name in out - @pytest.mark.skipif(env.JYTHON, reason="Jython gets mad if you patch sys.argv") - def test_help_contains_command_name_from_package(self): + def test_help_contains_command_name_from_package(self) -> None: # Command package name should be present in help output. # # When the main module is actually a package's `__main__` module, the resulting command line @@ -999,13 +1029,13 @@ out = self.stdout() assert expected_command_name in out - def test_help(self): + def test_help(self) -> None: self.command_line("help") lines = self.stdout().splitlines() assert len(lines) > 10 assert lines[-1] == f"Full documentation is at {__url__}" - def test_cmd_help(self): + def test_cmd_help(self) -> None: self.command_line("help run") out = self.stdout() lines = out.splitlines() @@ -1014,26 +1044,26 @@ assert len(lines) > 20 assert lines[-1] == f"Full documentation is at {__url__}" - def test_unknown_topic(self): + def test_unknown_topic(self) -> None: # Should probably be an ERR return, but meh. self.command_line("help foobar") lines = self.stdout().splitlines() assert lines[0] == "Don't know topic 'foobar'" assert lines[-1] == f"Full documentation is at {__url__}" - def test_error(self): + def test_error(self) -> None: self.command_line("fooey kablooey", ret=ERR) err = self.stderr() assert "fooey" in err assert "help" in err - def test_option_error(self): + def test_option_error(self) -> None: self.command_line("run --fooey", ret=ERR) err = self.stderr() assert "fooey" in err assert "help" in err - def test_doc_url(self): + def test_doc_url(self) -> None: assert __url__.startswith("https://coverage.readthedocs.io") @@ -1045,13 +1075,13 @@ class CoverageScriptStub: """A stub for coverage.cmdline.CoverageScript, used by CmdMainTest.""" - def command_line(self, argv): + def command_line(self, argv: List[str]) -> int: """Stub for command_line, the arg determines what it will do.""" if argv[0] == 'hello': print("Hello, world!") elif argv[0] == 'raise': try: - raise Exception("oh noes!") + raise RuntimeError("oh noes!") except: raise _ExceptionDuringRun(*sys.exc_info()) from None elif argv[0] == 'internalraise': @@ -1062,33 +1092,33 @@ raise AssertionError(f"Bad CoverageScriptStub: {argv!r}") return 0 - def setUp(self): + def setUp(self) -> None: super().setUp() old_CoverageScript = coverage.cmdline.CoverageScript - coverage.cmdline.CoverageScript = self.CoverageScriptStub + coverage.cmdline.CoverageScript = self.CoverageScriptStub # type: ignore self.addCleanup(setattr, coverage.cmdline, 'CoverageScript', old_CoverageScript) - def test_normal(self): + def test_normal(self) -> None: ret = coverage.cmdline.main(['hello']) assert ret == 0 assert self.stdout() == "Hello, world!\n" - def test_raise(self): + def test_raise(self) -> None: ret = coverage.cmdline.main(['raise']) assert ret == 1 out, err = self.stdouterr() assert out == "" print(err) - err = err.splitlines(keepends=True) - assert err[0] == 'Traceback (most recent call last):\n' - assert ' raise Exception("oh noes!")\n' in err - assert err[-1] == 'Exception: oh noes!\n' + err_parts = err.splitlines(keepends=True) + assert err_parts[0] == 'Traceback (most recent call last):\n' + assert ' raise RuntimeError("oh noes!")\n' in err_parts + assert err_parts[-1] == 'RuntimeError: oh noes!\n' - def test_internalraise(self): + def test_internalraise(self) -> None: with pytest.raises(ValueError, match="coverage is broken"): coverage.cmdline.main(['internalraise']) - def test_exit(self): + def test_exit(self) -> None: ret = coverage.cmdline.main(['exit']) assert ret == 23 @@ -1096,7 +1126,14 @@ class CoverageReportingFake: """A fake Coverage.coverage test double for FailUnderTest methods.""" # pylint: disable=missing-function-docstring - def __init__(self, report_result, html_result=0, xml_result=0, json_report=0, lcov_result=0): + def __init__( + self, + report_result: float, + html_result: float = 0, + xml_result: float = 0, + json_report: float = 0, + lcov_result: float = 0, + ) -> None: self.config = CoverageConfig() self.report_result = report_result self.html_result = html_result @@ -1104,28 +1141,28 @@ self.json_result = json_report self.lcov_result = lcov_result - def set_option(self, optname, optvalue): + def set_option(self, optname: str, optvalue: TConfigValueIn) -> None: self.config.set_option(optname, optvalue) - def get_option(self, optname): + def get_option(self, optname: str) -> TConfigValueOut: return self.config.get_option(optname) - def load(self): + def load(self) -> None: pass - def report(self, *args_unused, **kwargs_unused): + def report(self, *args_unused: Any, **kwargs_unused: Any) -> float: return self.report_result - def html_report(self, *args_unused, **kwargs_unused): + def html_report(self, *args_unused: Any, **kwargs_unused: Any) -> float: return self.html_result - def xml_report(self, *args_unused, **kwargs_unused): + def xml_report(self, *args_unused: Any, **kwargs_unused: Any) -> float: return self.xml_result - def json_report(self, *args_unused, **kwargs_unused): + def json_report(self, *args_unused: Any, **kwargs_unused: Any) -> float: return self.json_result - def lcov_report(self, *args_unused, **kwargs_unused): + def lcov_report(self, *args_unused: Any, **kwargs_unused: Any) -> float: return self.lcov_result @@ -1158,7 +1195,13 @@ # Command-line overrides configuration. ((20, 30, 40, 50, 60), 19, "report --fail-under=21", 2), ]) - def test_fail_under(self, results, fail_under, cmd, ret): + def test_fail_under( + self, + results: Tuple[float, float, float, float, float], + fail_under: Optional[float], + cmd: str, + ret: int, + ) -> None: cov = CoverageReportingFake(*results) if fail_under is not None: cov.set_option("report:fail_under", fail_under) @@ -1172,7 +1215,7 @@ (20.12345, "report --fail-under=20.1235 --precision=5", 2, "Coverage failure: total of 20.12345 is less than fail-under=20.12350\n"), ]) - def test_fail_under_with_precision(self, result, cmd, ret, msg): + def test_fail_under_with_precision(self, result: float, cmd: str, ret: int, msg: str) -> None: cov = CoverageReportingFake(report_result=result) with mock.patch("coverage.cmdline.Coverage", lambda *a,**kw: cov): self.command_line(cmd, ret) diff -Nru python-coverage-6.5.0+dfsg1/tests/test_collector.py python-coverage-7.2.7+dfsg1/tests/test_collector.py --- python-coverage-6.5.0+dfsg1/tests/test_collector.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_collector.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Tests of coverage/collector.py and other collectors.""" +from __future__ import annotations + import os.path import coverage @@ -14,7 +16,7 @@ class CollectorTest(CoverageTest): """Test specific aspects of the collection process.""" - def test_should_trace_cache(self): + def test_should_trace_cache(self) -> None: # The tracers should only invoke should_trace once for each file name. # Make some files that invoke each other. diff -Nru python-coverage-6.5.0+dfsg1/tests/test_concurrency.py python-coverage-7.2.7+dfsg1/tests/test_concurrency.py --- python-coverage-6.5.0+dfsg1/tests/test_concurrency.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_concurrency.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,14 +3,21 @@ """Tests for concurrency libraries.""" +from __future__ import annotations + import glob +import multiprocessing import os +import pathlib import random import re import sys import threading import time +from types import ModuleType +from typing import Iterable, Optional + from flaky import flaky import pytest @@ -27,11 +34,6 @@ # These libraries aren't always available, we'll skip tests if they aren't. try: - import multiprocessing -except ImportError: # pragma: only jython - multiprocessing = None - -try: import eventlet except ImportError: eventlet = None @@ -43,11 +45,11 @@ try: import greenlet -except ImportError: # pragma: only jython +except ImportError: greenlet = None -def measurable_line(l): +def measurable_line(l: str) -> bool: """Is this a line of code coverage will measure? Not blank, not a comment, and not "else" @@ -59,18 +61,15 @@ return False if l.startswith('else:'): return False - if env.JYTHON and l.startswith(('try:', 'except:', 'except ', 'break', 'with ')): - # Jython doesn't measure these statements. - return False # pragma: only jython return True -def line_count(s): +def line_count(s: str) -> int: """How many measurable lines are in `s`?""" return len(list(filter(measurable_line, s.splitlines()))) -def print_simple_annotation(code, linenos): +def print_simple_annotation(code: str, linenos: Iterable[int]) -> None: """Print the lines in `code` with X for each line number in `linenos`.""" for lineno, line in enumerate(code.splitlines(), start=1): print(" {} {}".format("X" if lineno in linenos else " ", line)) @@ -81,7 +80,7 @@ run_in_temp_dir = False - def test_line_count(self): + def test_line_count(self) -> None: CODE = """ # Hey there! x = 1 @@ -175,7 +174,7 @@ """ -def cant_trace_msg(concurrency, the_module): +def cant_trace_msg(concurrency: str, the_module: Optional[ModuleType]) -> Optional[str]: """What might coverage.py say about a concurrency setting and imported module?""" # In the concurrency choices, "multiprocessing" doesn't count, so remove it. if "multiprocessing" in concurrency: @@ -203,7 +202,13 @@ QLIMIT = 1000 - def try_some_code(self, code, concurrency, the_module, expected_out=None): + def try_some_code( + self, + code: str, + concurrency: str, + the_module: ModuleType, + expected_out: Optional[str] = None, + ) -> None: """Run some concurrency testing code and see that it was all covered. `code` is the Python code to execute. `concurrency` is the name of @@ -238,39 +243,40 @@ # If the test fails, it's helpful to see this info: fname = abs_file("try_it.py") linenos = data.lines(fname) + assert linenos is not None print(f"{len(linenos)}: {linenos}") print_simple_annotation(code, linenos) lines = line_count(code) assert line_counts(data)['try_it.py'] == lines - def test_threads(self): + def test_threads(self) -> None: code = (THREAD + SUM_RANGE_Q + PRINT_SUM_RANGE).format(QLIMIT=self.QLIMIT) self.try_some_code(code, "thread", threading) - def test_threads_simple_code(self): + def test_threads_simple_code(self) -> None: code = SIMPLE.format(QLIMIT=self.QLIMIT) self.try_some_code(code, "thread", threading) - def test_eventlet(self): + def test_eventlet(self) -> None: code = (EVENTLET + SUM_RANGE_Q + PRINT_SUM_RANGE).format(QLIMIT=self.QLIMIT) self.try_some_code(code, "eventlet", eventlet) - def test_eventlet_simple_code(self): + def test_eventlet_simple_code(self) -> None: code = SIMPLE.format(QLIMIT=self.QLIMIT) self.try_some_code(code, "eventlet", eventlet) # https://github.com/nedbat/coveragepy/issues/663 @pytest.mark.skipif(env.WINDOWS, reason="gevent has problems on Windows: #663") - def test_gevent(self): + def test_gevent(self) -> None: code = (GEVENT + SUM_RANGE_Q + PRINT_SUM_RANGE).format(QLIMIT=self.QLIMIT) self.try_some_code(code, "gevent", gevent) - def test_gevent_simple_code(self): + def test_gevent_simple_code(self) -> None: code = SIMPLE.format(QLIMIT=self.QLIMIT) self.try_some_code(code, "gevent", gevent) - def test_greenlet(self): + def test_greenlet(self) -> None: GREENLET = """\ from greenlet import greenlet @@ -288,11 +294,11 @@ """ self.try_some_code(GREENLET, "greenlet", greenlet, "hello world\n42\n") - def test_greenlet_simple_code(self): + def test_greenlet_simple_code(self) -> None: code = SIMPLE.format(QLIMIT=self.QLIMIT) self.try_some_code(code, "greenlet", greenlet) - def test_bug_330(self): + def test_bug_330(self) -> None: BUG_330 = """\ from weakref import WeakKeyDictionary import eventlet @@ -310,7 +316,7 @@ """ self.try_some_code(BUG_330, "eventlet", eventlet, "0\n") - def test_threads_with_gevent(self): + def test_threads_with_gevent(self) -> None: self.make_file("both.py", """\ import queue import threading @@ -351,25 +357,25 @@ last_line = self.squeezed_lines(out)[-1] assert re.search(r"TOTAL \d+ 0 100%", last_line) - def test_bad_concurrency(self): + def test_bad_concurrency(self) -> None: with pytest.raises(ConfigError, match="Unknown concurrency choices: nothing"): self.command_line("run --concurrency=nothing prog.py") - def test_bad_concurrency_in_config(self): + def test_bad_concurrency_in_config(self) -> None: self.make_file(".coveragerc", "[run]\nconcurrency = nothing\n") with pytest.raises(ConfigError, match="Unknown concurrency choices: nothing"): self.command_line("run prog.py") - def test_no_multiple_light_concurrency(self): + def test_no_multiple_light_concurrency(self) -> None: with pytest.raises(ConfigError, match="Conflicting concurrency settings: eventlet, gevent"): self.command_line("run --concurrency=gevent,eventlet prog.py") - def test_no_multiple_light_concurrency_in_config(self): + def test_no_multiple_light_concurrency_in_config(self) -> None: self.make_file(".coveragerc", "[run]\nconcurrency = gevent, eventlet\n") with pytest.raises(ConfigError, match="Conflicting concurrency settings: eventlet, gevent"): self.command_line("run prog.py") - def test_multiprocessing_needs_config_file(self): + def test_multiprocessing_needs_config_file(self) -> None: with pytest.raises(ConfigError, match="multiprocessing requires a configuration file"): self.command_line("run --concurrency=multiprocessing prog.py") @@ -378,9 +384,9 @@ """Tests of what happens if the requested concurrency isn't installed.""" @pytest.mark.parametrize("module", ["eventlet", "gevent", "greenlet"]) - def test_missing_module(self, module): + def test_missing_module(self, module: str) -> None: self.make_file("prog.py", "a = 1") - sys.modules[module] = None + sys.modules[module] = None # type: ignore[assignment] msg = f"Couldn't trace with concurrency={module}, the module isn't installed." with pytest.raises(ConfigError, match=msg): self.command_line(f"run --concurrency={module} prog.py") @@ -434,30 +440,29 @@ @pytest.fixture(params=["fork", "spawn"], name="start_method") -def start_method_fixture(request): +def start_method_fixture(request: pytest.FixtureRequest) -> str: """Parameterized fixture to choose the start_method for multiprocessing.""" - start_method = request.param + start_method: str = request.param if start_method not in multiprocessing.get_all_start_methods(): # Windows doesn't support "fork". pytest.skip(f"start_method={start_method} not supported here") return start_method -@pytest.mark.skipif(not multiprocessing, reason="No multiprocessing in this Python") @flaky(max_runs=30) # Sometimes a test fails due to inherent randomness. Try more times. class MultiprocessingTest(CoverageTest): """Test support of the multiprocessing module.""" def try_multiprocessing_code( self, - code, - expected_out, - the_module, - nprocs, - start_method, - concurrency="multiprocessing", - args="", - ): + code: str, + expected_out: Optional[str], + the_module: ModuleType, + nprocs: int, + start_method: str, + concurrency: str = "multiprocessing", + args: str = "", + ) -> None: """Run code using multiprocessing, it should produce `expected_out`.""" self.make_file("multi.py", code) self.make_file(".coveragerc", f"""\ @@ -466,9 +471,7 @@ source = . """) - cmd = "coverage run {args} multi.py {start_method}".format( - args=args, start_method=start_method, - ) + cmd = f"coverage run {args} multi.py {start_method}" out = self.run_command(cmd) expected_cant_trace = cant_trace_msg(concurrency, the_module) @@ -484,15 +487,19 @@ out_lines = out.splitlines() assert len(out_lines) == nprocs + 1 assert all( - re.fullmatch(r"Combined data file \.coverage\..*\.\d+\.\d+", line) + re.fullmatch( + r"(Combined data file|Skipping duplicate data) \.coverage\..*\.\d+\.\d+", + line + ) for line in out_lines ) + assert len(glob.glob(".coverage.*")) == 0 out = self.run_command("coverage report -m") last_line = self.squeezed_lines(out)[-1] assert re.search(r"TOTAL \d+ 0 100%", last_line) - def test_multiprocessing_simple(self, start_method): + def test_multiprocessing_simple(self, start_method: str) -> None: nprocs = 3 upto = 30 code = (SQUARE_OR_CUBE_WORK + MULTI_CODE).format(NPROCS=nprocs, UPTO=upto) @@ -506,7 +513,7 @@ start_method=start_method, ) - def test_multiprocessing_append(self, start_method): + def test_multiprocessing_append(self, start_method: str) -> None: nprocs = 3 upto = 30 code = (SQUARE_OR_CUBE_WORK + MULTI_CODE).format(NPROCS=nprocs, UPTO=upto) @@ -521,7 +528,7 @@ start_method=start_method, ) - def test_multiprocessing_and_gevent(self, start_method): + def test_multiprocessing_and_gevent(self, start_method: str) -> None: nprocs = 3 upto = 30 code = ( @@ -538,7 +545,7 @@ start_method=start_method, ) - def test_multiprocessing_with_branching(self, start_method): + def test_multiprocessing_with_branching(self, start_method: str) -> None: nprocs = 3 upto = 30 code = (SQUARE_OR_CUBE_WORK + MULTI_CODE).format(NPROCS=nprocs, UPTO=upto) @@ -562,7 +569,7 @@ last_line = self.squeezed_lines(out)[-1] assert re.search(r"TOTAL \d+ 0 \d+ 0 100%", last_line) - def test_multiprocessing_bootstrap_error_handling(self): + def test_multiprocessing_bootstrap_error_handling(self) -> None: # An exception during bootstrapping will be reported. self.make_file("multi.py", """\ import multiprocessing @@ -577,9 +584,9 @@ """) out = self.run_command("coverage run multi.py") assert "Exception during multiprocessing bootstrap init" in out - assert "Exception: Crashing because called by _bootstrap" in out + assert "RuntimeError: Crashing because called by _bootstrap" in out - def test_bug_890(self): + def test_bug_890(self) -> None: # chdir in multiprocessing shouldn't keep us from finding the # .coveragerc file. self.make_file("multi.py", """\ @@ -599,11 +606,11 @@ assert out.splitlines()[-1] == "ok" -def test_coverage_stop_in_threads(): +def test_coverage_stop_in_threads() -> None: has_started_coverage = [] has_stopped_coverage = [] - def run_thread(): # pragma: nested + def run_thread() -> None: # pragma: nested """Check that coverage is stopping properly in threads.""" deadline = time.time() + 5 ident = threading.current_thread().ident @@ -630,27 +637,28 @@ assert has_stopped_coverage == [t.ident] -def test_thread_safe_save_data(tmpdir): +def test_thread_safe_save_data(tmp_path: pathlib.Path) -> None: # Non-regression test for: https://github.com/nedbat/coveragepy/issues/581 # Create some Python modules and put them in the path - modules_dir = tmpdir.mkdir('test_modules') + modules_dir = tmp_path / "test_modules" + modules_dir.mkdir() module_names = [f"m{i:03d}" for i in range(1000)] for module_name in module_names: - modules_dir.join(module_name + ".py").write("def f(): pass\n") + (modules_dir / (module_name + ".py")).write_text("def f(): pass\n") # Shared variables for threads should_run = [True] imported = [] old_dir = os.getcwd() - os.chdir(modules_dir.strpath) + os.chdir(modules_dir) try: # Make sure that all dummy modules can be imported. for module_name in module_names: import_local_file(module_name) - def random_load(): # pragma: nested + def random_load() -> None: # pragma: nested """Import modules randomly to stress coverage.""" while should_run[0]: module_name = random.choice(module_names) @@ -697,7 +705,7 @@ """Tests of our handling of SIGTERM.""" @pytest.mark.parametrize("sigterm", [False, True]) - def test_sigterm_saves_data(self, sigterm): + def test_sigterm_multiprocessing_saves_data(self, sigterm: bool) -> None: # A terminated process should save its coverage data. self.make_file("clobbered.py", """\ import multiprocessing @@ -743,7 +751,33 @@ expected = "clobbered.py 17 5 71% 5-10" assert self.squeezed_lines(out)[2] == expected - def test_sigterm_still_runs(self): + def test_sigterm_threading_saves_data(self) -> None: + # A terminated process should save its coverage data. + self.make_file("handler.py", """\ + import os, signal + + print("START", flush=True) + print("SIGTERM", flush=True) + os.kill(os.getpid(), signal.SIGTERM) + print("NOT HERE", flush=True) + """) + self.make_file(".coveragerc", """\ + [run] + # The default concurrency option. + concurrency = thread + sigterm = true + """) + out = self.run_command("coverage run handler.py") + out_lines = out.splitlines() + assert len(out_lines) in [2, 3] + assert out_lines[:2] == ["START", "SIGTERM"] + if len(out_lines) == 3: + assert out_lines[2] == "Terminated" + out = self.run_command("coverage report -m") + expected = "handler.py 5 1 80% 6" + assert self.squeezed_lines(out)[2] == expected + + def test_sigterm_still_runs(self) -> None: # A terminated process still runs its own SIGTERM handler. self.make_file("handler.py", """\ import multiprocessing diff -Nru python-coverage-6.5.0+dfsg1/tests/test_config.py python-coverage-7.2.7+dfsg1/tests/test_config.py --- python-coverage-6.5.0+dfsg1/tests/test_config.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_config.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,32 +3,34 @@ """Test the config file handling for coverage.py""" -import math -import sys -from collections import OrderedDict +from __future__ import annotations +import sys from unittest import mock + import pytest import coverage +from coverage import Coverage from coverage.config import HandyConfigParser from coverage.exceptions import ConfigError, CoverageWarning +from coverage.tomlconfig import TomlConfigParser +from coverage.types import FilePathClasses, FilePathType from tests.coveragetest import CoverageTest, UsingModulesMixin -from tests.helpers import without_module class ConfigTest(CoverageTest): """Tests of the different sources of configuration settings.""" - def test_default_config(self): + def test_default_config(self) -> None: # Just constructing a coverage() object gets the right defaults. cov = coverage.Coverage() assert not cov.config.timid assert not cov.config.branch assert cov.config.data_file == ".coverage" - def test_arguments(self): + def test_arguments(self) -> None: # Arguments to the constructor are applied to the configuration. cov = coverage.Coverage(timid=True, data_file="fooey.dat", concurrency="multiprocessing") assert cov.config.timid @@ -36,7 +38,7 @@ assert cov.config.data_file == "fooey.dat" assert cov.config.concurrency == ["multiprocessing"] - def test_config_file(self): + def test_config_file(self) -> None: # A .coveragerc file will be read into the configuration. self.make_file(".coveragerc", """\ # This is just a bogus .rc file for testing. @@ -49,7 +51,8 @@ assert not cov.config.branch assert cov.config.data_file == ".hello_kitty.data" - def test_named_config_file(self): + @pytest.mark.parametrize("file_class", FilePathClasses) + def test_named_config_file(self, file_class: FilePathType) -> None: # You can name the config file what you like. self.make_file("my_cov.ini", """\ [run] @@ -57,13 +60,13 @@ ; I wouldn't really use this as a data file... data_file = delete.me """) - cov = coverage.Coverage(config_file="my_cov.ini") + cov = coverage.Coverage(config_file=file_class("my_cov.ini")) assert cov.config.timid assert not cov.config.branch assert cov.config.data_file == "delete.me" - def test_toml_config_file(self): - # A .coveragerc file will be read into the configuration. + def test_toml_config_file(self) -> None: + # A pyproject.toml file will be read into the configuration. self.make_file("pyproject.toml", """\ # This is just a bogus toml file for testing. [tool.somethingelse] @@ -81,7 +84,7 @@ [tool.coverage.plugins.a_plugin] hello = "world" """) - cov = coverage.Coverage(config_file="pyproject.toml") + cov = coverage.Coverage() assert cov.config.timid assert not cov.config.branch assert cov.config.concurrency == ["a", "b"] @@ -89,20 +92,21 @@ assert cov.config.plugins == ["plugins.a_plugin"] assert cov.config.precision == 3 assert cov.config.html_title == "tabblo & «ταБЬℓσ»" - assert math.isclose(cov.config.fail_under, 90.5) + assert cov.config.fail_under == 90.5 assert cov.config.get_plugin_options("plugins.a_plugin") == {"hello": "world"} + def test_toml_ints_can_be_floats(self) -> None: # Test that our class doesn't reject integers when loading floats self.make_file("pyproject.toml", """\ # This is just a bogus toml file for testing. [tool.coverage.report] fail_under = 90 """) - cov = coverage.Coverage(config_file="pyproject.toml") - assert math.isclose(cov.config.fail_under, 90) + cov = coverage.Coverage() + assert cov.config.fail_under == 90 assert isinstance(cov.config.fail_under, float) - def test_ignored_config_file(self): + def test_ignored_config_file(self) -> None: # You can disable reading the .coveragerc file. self.make_file(".coveragerc", """\ [run] @@ -114,7 +118,7 @@ assert not cov.config.branch assert cov.config.data_file == ".coverage" - def test_config_file_then_args(self): + def test_config_file_then_args(self) -> None: # The arguments override the .coveragerc file. self.make_file(".coveragerc", """\ [run] @@ -126,7 +130,7 @@ assert not cov.config.branch assert cov.config.data_file == ".mycov" - def test_data_file_from_environment(self): + def test_data_file_from_environment(self) -> None: # There's an environment variable for the data_file. self.make_file(".coveragerc", """\ [run] @@ -140,7 +144,7 @@ cov = coverage.Coverage(data_file="fromarg.dat") assert cov.config.data_file == "fromarg.dat" - def test_debug_from_environment(self): + def test_debug_from_environment(self) -> None: self.make_file(".coveragerc", """\ [run] debug = dataio, pids @@ -149,7 +153,7 @@ cov = coverage.Coverage() assert cov.config.debug == ["dataio", "pids", "callers", "fooey"] - def test_rcfile_from_environment(self): + def test_rcfile_from_environment(self) -> None: self.make_file("here.ini", """\ [run] data_file = overthere.dat @@ -158,7 +162,7 @@ cov = coverage.Coverage() assert cov.config.data_file == "overthere.dat" - def test_missing_rcfile_from_environment(self): + def test_missing_rcfile_from_environment(self) -> None: self.set_environ("COVERAGE_RCFILE", "nowhere.ini") msg = "Couldn't read 'nowhere.ini' as a config file" with pytest.raises(ConfigError, match=msg): @@ -179,7 +183,7 @@ r"'foo\*\*\*': " + r"multiple repeat"), ]) - def test_parse_errors(self, bad_config, msg): + def test_parse_errors(self, bad_config: str, msg: str) -> None: # Im-parsable values raise ConfigError, with details. self.make_file(".coveragerc", bad_config) with pytest.raises(ConfigError, match=msg): @@ -200,15 +204,15 @@ r"multiple repeat"), ('[tool.coverage.run]\nconcurrency="foo"', "not a list"), ("[tool.coverage.report]\nprecision=1.23", "not an integer"), - ('[tool.coverage.report]\nfail_under="s"', "not a float"), + ('[tool.coverage.report]\nfail_under="s"', "couldn't convert to a float"), ]) - def test_toml_parse_errors(self, bad_config, msg): + def test_toml_parse_errors(self, bad_config: str, msg: str) -> None: # Im-parsable values raise ConfigError, with details. self.make_file("pyproject.toml", bad_config) with pytest.raises(ConfigError, match=msg): coverage.Coverage() - def test_environment_vars_in_config(self): + def test_environment_vars_in_config(self) -> None: # Config files can have $envvars in them. self.make_file(".coveragerc", """\ [run] @@ -230,13 +234,15 @@ assert cov.config.branch is True assert cov.config.exclude_list == ["the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"] - def test_environment_vars_in_toml_config(self): + def test_environment_vars_in_toml_config(self) -> None: # Config files can have $envvars in them. self.make_file("pyproject.toml", """\ [tool.coverage.run] data_file = "$DATA_FILE.fooey" - branch = $BRANCH + branch = "$BRANCH" [tool.coverage.report] + precision = "$DIGITS" + fail_under = "$FAIL_UNDER" exclude_lines = [ "the_$$one", "another${THING}", @@ -244,16 +250,24 @@ "x${NOTHING}y", "huh$${X}what", ] + [othersection] + # This reproduces the failure from https://github.com/nedbat/coveragepy/issues/1481 + # When OTHER has a backslash that isn't a valid escape, like \\z (see below). + something = "if [ $OTHER ]; then printf '%s\\n' 'Hi'; fi" """) self.set_environ("BRANCH", "true") + self.set_environ("DIGITS", "3") + self.set_environ("FAIL_UNDER", "90.5") self.set_environ("DATA_FILE", "hello-world") self.set_environ("THING", "ZZZ") + self.set_environ("OTHER", "hi\\zebra") cov = coverage.Coverage() - assert cov.config.data_file == "hello-world.fooey" assert cov.config.branch is True + assert cov.config.precision == 3 + assert cov.config.data_file == "hello-world.fooey" assert cov.config.exclude_list == ["the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"] - def test_tilde_in_config(self): + def test_tilde_in_config(self) -> None: # Config entries that are file paths can be tilde-expanded. self.make_file(".coveragerc", """\ [run] @@ -276,7 +290,7 @@ ~/src ~joe/source """) - def expanduser(s): + def expanduser(s: str) -> str: """Fake tilde expansion""" s = s.replace("~/", "/Users/me/") s = s.replace("~joe/", "/Users/joe/") @@ -290,7 +304,7 @@ assert cov.config.exclude_list == ["~/data.file", "~joe/html_dir"] assert cov.config.paths == {'mapping': ['/Users/me/src', '/Users/joe/source']} - def test_tilde_in_toml_config(self): + def test_tilde_in_toml_config(self) -> None: # Config entries that are file paths can be tilde-expanded. self.make_file("pyproject.toml", """\ [tool.coverage.run] @@ -309,7 +323,7 @@ "~joe/html_dir", ] """) - def expanduser(s): + def expanduser(s: str) -> str: """Fake tilde expansion""" s = s.replace("~/", "/Users/me/") s = s.replace("~joe/", "/Users/joe/") @@ -322,7 +336,7 @@ assert cov.config.xml_output == "/Users/me/somewhere/xml.out" assert cov.config.exclude_list == ["~/data.file", "~joe/html_dir"] - def test_tweaks_after_constructor(self): + def test_tweaks_after_constructor(self) -> None: # set_option can be used after construction to affect the config. cov = coverage.Coverage(timid=True, data_file="fooey.dat") cov.set_option("run:timid", False) @@ -335,7 +349,7 @@ assert not cov.get_option("run:branch") assert cov.get_option("run:data_file") == "fooey.dat" - def test_tweaks_paths_after_constructor(self): + def test_tweaks_paths_after_constructor(self) -> None: self.make_file(".coveragerc", """\ [paths] first = @@ -346,20 +360,22 @@ /second/a /second/b """) - old_paths = OrderedDict() - old_paths["first"] = ["/first/1", "/first/2"] - old_paths["second"] = ["/second/a", "/second/b"] + old_paths = { + "first": ["/first/1", "/first/2"], + "second": ["/second/a", "/second/b"], + } cov = coverage.Coverage() paths = cov.get_option("paths") assert paths == old_paths - new_paths = OrderedDict() - new_paths['magic'] = ['src', 'ok'] + new_paths = { + "magic": ["src", "ok"], + } cov.set_option("paths", new_paths) assert cov.get_option("paths") == new_paths - def test_tweak_error_checking(self): + def test_tweak_error_checking(self) -> None: # Trying to set an unknown config value raises an error. cov = coverage.Coverage() with pytest.raises(ConfigError, match="No such option: 'run:xyzzy'"): @@ -371,7 +387,7 @@ with pytest.raises(ConfigError, match="No such option: 'xyzzy:foo'"): _ = cov.get_option("xyzzy:foo") - def test_tweak_plugin_options(self): + def test_tweak_plugin_options(self) -> None: # Plugin options have a more flexible syntax. cov = coverage.Coverage() cov.set_option("run:plugins", ["fooey.plugin", "xyzzy.coverage.plugin"]) @@ -385,7 +401,7 @@ with pytest.raises(ConfigError, match="No such option: 'no_such.plugin:foo'"): _ = cov.get_option("no_such.plugin:foo") - def test_unknown_option(self): + def test_unknown_option(self) -> None: self.make_file(".coveragerc", """\ [run] xyzzy = 17 @@ -394,7 +410,7 @@ with pytest.warns(CoverageWarning, match=msg): _ = coverage.Coverage() - def test_unknown_option_toml(self): + def test_unknown_option_toml(self) -> None: self.make_file("pyproject.toml", """\ [tool.coverage.run] xyzzy = 17 @@ -403,7 +419,7 @@ with pytest.warns(CoverageWarning, match=msg): _ = coverage.Coverage() - def test_misplaced_option(self): + def test_misplaced_option(self) -> None: self.make_file(".coveragerc", """\ [report] branch = True @@ -412,7 +428,7 @@ with pytest.warns(CoverageWarning, match=msg): _ = coverage.Coverage() - def test_unknown_option_in_other_ini_file(self): + def test_unknown_option_in_other_ini_file(self) -> None: self.make_file("setup.cfg", """\ [coverage:run] huh = what? @@ -421,17 +437,28 @@ with pytest.warns(CoverageWarning, match=msg): _ = coverage.Coverage() - def test_exceptions_from_missing_things(self): + def test_exceptions_from_missing_things(self) -> None: self.make_file("config.ini", """\ [run] branch = True """) - config = HandyConfigParser("config.ini") + config = HandyConfigParser(True) + config.read(["config.ini"]) with pytest.raises(ConfigError, match="No section: 'xyzzy'"): config.options("xyzzy") with pytest.raises(ConfigError, match="No option 'foo' in section: 'xyzzy'"): config.get("xyzzy", "foo") + def test_exclude_also(self) -> None: + self.make_file("pyproject.toml", """\ + [tool.coverage.report] + exclude_also = ["foobar", "raise .*Error"] + """) + cov = coverage.Coverage() + + expected = coverage.config.DEFAULT_EXCLUDE + ["foobar", "raise .*Error"] + assert cov.config.exclude_list == expected + class ConfigFileTest(UsingModulesMixin, CoverageTest): """Tests of the config file settings in particular.""" @@ -482,6 +509,8 @@ skip_covered = TruE skip_empty =TruE + include_namespace_packages = TRUE + [{section}html] directory = c:\\tricky\\dir.somewhere @@ -532,7 +561,7 @@ python igor.py zip_mods """ - def assert_config_settings_are_correct(self, cov): + def assert_config_settings_are_correct(self, cov: Coverage) -> None: """Check that `cov` has all the settings from LOTSA_SETTINGS.""" assert cov.config.timid assert cov.config.data_file == "something_or_other.dat" @@ -577,39 +606,40 @@ assert cov.config.get_plugin_options("plugins.another") == {} assert cov.config.json_show_contexts is True assert cov.config.json_pretty_print is True + assert cov.config.include_namespace_packages is True - def test_config_file_settings(self): + def test_config_file_settings(self) -> None: self.make_file(".coveragerc", self.LOTSA_SETTINGS.format(section="")) cov = coverage.Coverage() self.assert_config_settings_are_correct(cov) - def check_config_file_settings_in_other_file(self, fname, contents): + def check_config_file_settings_in_other_file(self, fname: str, contents: str) -> None: """Check config will be read from another file, with prefixed sections.""" nested = self.LOTSA_SETTINGS.format(section="coverage:") fname = self.make_file(fname, nested + "\n" + contents) cov = coverage.Coverage() self.assert_config_settings_are_correct(cov) - def test_config_file_settings_in_setupcfg(self): + def test_config_file_settings_in_setupcfg(self) -> None: self.check_config_file_settings_in_other_file("setup.cfg", self.SETUP_CFG) - def test_config_file_settings_in_toxini(self): + def test_config_file_settings_in_toxini(self) -> None: self.check_config_file_settings_in_other_file("tox.ini", self.TOX_INI) - def check_other_config_if_coveragerc_specified(self, fname, contents): + def check_other_config_if_coveragerc_specified(self, fname: str, contents: str) -> None: """Check that config `fname` is read if .coveragerc is missing, but specified.""" nested = self.LOTSA_SETTINGS.format(section="coverage:") self.make_file(fname, nested + "\n" + contents) cov = coverage.Coverage(config_file=".coveragerc") self.assert_config_settings_are_correct(cov) - def test_config_file_settings_in_setupcfg_if_coveragerc_specified(self): + def test_config_file_settings_in_setupcfg_if_coveragerc_specified(self) -> None: self.check_other_config_if_coveragerc_specified("setup.cfg", self.SETUP_CFG) - def test_config_file_settings_in_tox_if_coveragerc_specified(self): + def test_config_file_settings_in_tox_if_coveragerc_specified(self) -> None: self.check_other_config_if_coveragerc_specified("tox.ini", self.TOX_INI) - def check_other_not_read_if_coveragerc(self, fname): + def check_other_not_read_if_coveragerc(self, fname: str) -> None: """Check config `fname` is not read if .coveragerc exists.""" self.make_file(".coveragerc", """\ [run] @@ -622,16 +652,16 @@ """) cov = coverage.Coverage() assert cov.config.run_include == ["foo"] - assert cov.config.run_omit is None + assert cov.config.run_omit == [] assert cov.config.branch is False - def test_setupcfg_only_if_not_coveragerc(self): + def test_setupcfg_only_if_not_coveragerc(self) -> None: self.check_other_not_read_if_coveragerc("setup.cfg") - def test_toxini_only_if_not_coveragerc(self): + def test_toxini_only_if_not_coveragerc(self) -> None: self.check_other_not_read_if_coveragerc("tox.ini") - def check_other_config_need_prefixes(self, fname): + def check_other_config_need_prefixes(self, fname: str) -> None: """Check that `fname` sections won't be read if un-prefixed.""" self.make_file(fname, """\ [run] @@ -639,16 +669,16 @@ branch = true """) cov = coverage.Coverage() - assert cov.config.run_omit is None + assert cov.config.run_omit == [] assert cov.config.branch is False - def test_setupcfg_only_if_prefixed(self): + def test_setupcfg_only_if_prefixed(self) -> None: self.check_other_config_need_prefixes("setup.cfg") - def test_toxini_only_if_prefixed(self): + def test_toxini_only_if_prefixed(self) -> None: self.check_other_config_need_prefixes("tox.ini") - def test_tox_ini_even_if_setup_cfg(self): + def test_tox_ini_even_if_setup_cfg(self) -> None: # There's a setup.cfg, but no coverage settings in it, so tox.ini # is read. nested = self.LOTSA_SETTINGS.format(section="coverage:") @@ -657,14 +687,14 @@ cov = coverage.Coverage() self.assert_config_settings_are_correct(cov) - def test_read_prefixed_sections_from_explicit_file(self): + def test_read_prefixed_sections_from_explicit_file(self) -> None: # You can point to a tox.ini, and it will find [coverage:run] sections nested = self.LOTSA_SETTINGS.format(section="coverage:") self.make_file("tox.ini", self.TOX_INI + "\n" + nested) cov = coverage.Coverage(config_file="tox.ini") self.assert_config_settings_are_correct(cov) - def test_non_ascii(self): + def test_non_ascii(self) -> None: self.make_file(".coveragerc", """\ [report] exclude_lines = @@ -681,69 +711,86 @@ assert cov.config.html_title == "tabblo & «ταБЬℓσ» # numbers" @pytest.mark.parametrize("bad_file", ["nosuchfile.txt", "."]) - def test_unreadable_config(self, bad_file): + def test_unreadable_config(self, bad_file: str) -> None: # If a config file is explicitly specified, then it is an error for it # to not be readable. msg = f"Couldn't read {bad_file!r} as a config file" with pytest.raises(ConfigError, match=msg): coverage.Coverage(config_file=bad_file) - def test_nocoveragerc_file_when_specified(self): + def test_nocoveragerc_file_when_specified(self) -> None: cov = coverage.Coverage(config_file=".coveragerc") assert not cov.config.timid assert not cov.config.branch assert cov.config.data_file == ".coverage" - def test_note_is_obsolete(self): - self.make_file("main.py", "a = 1") - self.make_file(".coveragerc", """\ - [run] - note = I am here I am here I am here! - """) - cov = coverage.Coverage() - with self.assert_warnings(cov, [r"The '\[run] note' setting is no longer supported."]): - self.start_import_stop(cov, "main") - cov.report() - - def test_no_toml_installed_no_toml(self): + def test_no_toml_installed_no_toml(self) -> None: # Can't read a toml file that doesn't exist. - with without_module(coverage.tomlconfig, 'tomllib'): + with mock.patch.object(coverage.tomlconfig, "has_tomllib", False): msg = "Couldn't read 'cov.toml' as a config file" with pytest.raises(ConfigError, match=msg): coverage.Coverage(config_file="cov.toml") @pytest.mark.skipif(sys.version_info >= (3, 11), reason="Python 3.11 has toml in stdlib") - def test_no_toml_installed_explicit_toml(self): + def test_no_toml_installed_explicit_toml(self) -> None: # Can't specify a toml config file if toml isn't installed. self.make_file("cov.toml", "# A toml file!") - with without_module(coverage.tomlconfig, 'tomllib'): + with mock.patch.object(coverage.tomlconfig, "has_tomllib", False): msg = "Can't read 'cov.toml' without TOML support" with pytest.raises(ConfigError, match=msg): coverage.Coverage(config_file="cov.toml") @pytest.mark.skipif(sys.version_info >= (3, 11), reason="Python 3.11 has toml in stdlib") - def test_no_toml_installed_pyproject_toml(self): + def test_no_toml_installed_pyproject_toml(self) -> None: # Can't have coverage config in pyproject.toml without toml installed. self.make_file("pyproject.toml", """\ # A toml file! [tool.coverage.run] xyzzy = 17 """) - with without_module(coverage.tomlconfig, 'tomllib'): + with mock.patch.object(coverage.tomlconfig, "has_tomllib", False): + msg = "Can't read 'pyproject.toml' without TOML support" + with pytest.raises(ConfigError, match=msg): + coverage.Coverage() + + @pytest.mark.skipif(sys.version_info >= (3, 11), reason="Python 3.11 has toml in stdlib") + def test_no_toml_installed_pyproject_toml_shorter_syntax(self) -> None: + # Can't have coverage config in pyproject.toml without toml installed. + self.make_file("pyproject.toml", """\ + # A toml file! + [tool.coverage] + run.parallel = true + """) + with mock.patch.object(coverage.tomlconfig, "has_tomllib", False): msg = "Can't read 'pyproject.toml' without TOML support" with pytest.raises(ConfigError, match=msg): coverage.Coverage() - def test_no_toml_installed_pyproject_no_coverage(self): + @pytest.mark.skipif(sys.version_info >= (3, 11), reason="Python 3.11 has toml in stdlib") + def test_no_toml_installed_pyproject_no_coverage(self) -> None: # It's ok to have non-coverage pyproject.toml without toml installed. self.make_file("pyproject.toml", """\ # A toml file! [tool.something] xyzzy = 17 """) - with without_module(coverage.tomlconfig, 'tomllib'): + with mock.patch.object(coverage.tomlconfig, "has_tomllib", False): cov = coverage.Coverage() # We get default settings: assert not cov.config.timid assert not cov.config.branch assert cov.config.data_file == ".coverage" + + def test_exceptions_from_missing_toml_things(self) -> None: + self.make_file("pyproject.toml", """\ + [tool.coverage.run] + branch = true + """) + config = TomlConfigParser(False) + config.read("pyproject.toml") + with pytest.raises(ConfigError, match="No section: 'xyzzy'"): + config.options("xyzzy") + with pytest.raises(ConfigError, match="No section: 'xyzzy'"): + config.get("xyzzy", "foo") + with pytest.raises(ConfigError, match="No option 'foo' in section: 'tool.coverage.run'"): + config.get("run", "foo") diff -Nru python-coverage-6.5.0+dfsg1/tests/test_context.py python-coverage-7.2.7+dfsg1/tests/test_context.py --- python-coverage-6.5.0+dfsg1/tests/test_context.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_context.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,13 +3,18 @@ """Tests for context support.""" +from __future__ import annotations + import inspect import os.path + +from typing import Any, List, Optional, Tuple from unittest import mock import coverage from coverage.context import qualname_from_frame -from coverage.data import CoverageData +from coverage.data import CoverageData, sorted_lines +from coverage.types import TArc, TCovKwargs, TLineNo from tests.coveragetest import CoverageTest from tests.helpers import assert_count_equal @@ -18,14 +23,14 @@ class StaticContextTest(CoverageTest): """Tests of the static context.""" - def test_no_context(self): + def test_no_context(self) -> None: self.make_file("main.py", "a = 1") cov = coverage.Coverage() self.start_import_stop(cov, "main") data = cov.get_data() assert_count_equal(data.measured_contexts(), [""]) - def test_static_context(self): + def test_static_context(self) -> None: self.make_file("main.py", "a = 1") cov = coverage.Coverage(context="gooey") self.start_import_stop(cov, "main") @@ -42,7 +47,7 @@ LINES = [1, 2, 4] ARCS = [(-1, 1), (1, 2), (2, 4), (4, -1)] - def run_red_blue(self, **options): + def run_red_blue(self, **options: TCovKwargs) -> Tuple[CoverageData, CoverageData]: """Run red.py and blue.py, and return their CoverageData objects.""" self.make_file("red.py", self.SOURCE) red_cov = coverage.Coverage(context="red", data_suffix="r", source=["."], **options) @@ -58,7 +63,7 @@ return red_data, blue_data - def test_combining_line_contexts(self): + def test_combining_line_contexts(self) -> None: red_data, blue_data = self.run_red_blue() for datas in [[red_data, blue_data], [blue_data, red_data]]: combined = CoverageData(suffix="combined") @@ -73,7 +78,7 @@ fred = full_names['red.py'] fblue = full_names['blue.py'] - def assert_combined_lines(filename, context, lines): + def assert_combined_lines(filename: str, context: str, lines: List[TLineNo]) -> None: # pylint: disable=cell-var-from-loop combined.set_query_context(context) assert combined.lines(filename) == lines @@ -83,7 +88,7 @@ assert_combined_lines(fblue, 'red', []) assert_combined_lines(fblue, 'blue', self.LINES) - def test_combining_arc_contexts(self): + def test_combining_arc_contexts(self) -> None: red_data, blue_data = self.run_red_blue(branch=True) for datas in [[red_data, blue_data], [blue_data, red_data]]: combined = CoverageData(suffix="combined") @@ -98,7 +103,7 @@ fred = full_names['red.py'] fblue = full_names['blue.py'] - def assert_combined_lines(filename, context, lines): + def assert_combined_lines(filename: str, context: str, lines: List[TLineNo]) -> None: # pylint: disable=cell-var-from-loop combined.set_query_context(context) assert combined.lines(filename) == lines @@ -108,7 +113,7 @@ assert_combined_lines(fblue, 'red', []) assert_combined_lines(fblue, 'blue', self.LINES) - def assert_combined_arcs(filename, context, lines): + def assert_combined_arcs(filename: str, context: str, lines: List[TArc]) -> None: # pylint: disable=cell-var-from-loop combined.set_query_context(context) assert combined.arcs(filename) == lines @@ -149,7 +154,7 @@ TEST_ONE_LINES = [5, 6, 2] TEST_TWO_LINES = [9, 10, 11, 13, 14, 15, 2] - def test_dynamic_alone(self): + def test_dynamic_alone(self) -> None: self.make_file("two_tests.py", self.SOURCE) cov = coverage.Coverage(source=["."]) cov.set_option("run:dynamic_context", "test_function") @@ -163,15 +168,15 @@ ["", "two_tests.test_one", "two_tests.test_two"] ) - def assert_context_lines(context, lines): + def assert_context_lines(context: str, lines: List[TLineNo]) -> None: data.set_query_context(context) - assert_count_equal(lines, data.lines(fname)) + assert_count_equal(lines, sorted_lines(data, fname)) assert_context_lines("", self.OUTER_LINES) assert_context_lines("two_tests.test_one", self.TEST_ONE_LINES) assert_context_lines("two_tests.test_two", self.TEST_TWO_LINES) - def test_static_and_dynamic(self): + def test_static_and_dynamic(self) -> None: self.make_file("two_tests.py", self.SOURCE) cov = coverage.Coverage(context="stat", source=["."]) cov.set_option("run:dynamic_context", "test_function") @@ -185,33 +190,33 @@ ["stat", "stat|two_tests.test_one", "stat|two_tests.test_two"] ) - def assert_context_lines(context, lines): + def assert_context_lines(context: str, lines: List[TLineNo]) -> None: data.set_query_context(context) - assert_count_equal(lines, data.lines(fname)) + assert_count_equal(lines, sorted_lines(data, fname)) assert_context_lines("stat", self.OUTER_LINES) assert_context_lines("stat|two_tests.test_one", self.TEST_ONE_LINES) assert_context_lines("stat|two_tests.test_two", self.TEST_TWO_LINES) -def get_qualname(): +def get_qualname() -> Optional[str]: """Helper to return qualname_from_frame for the caller.""" stack = inspect.stack()[1:] if any(sinfo[0].f_code.co_name == "get_qualname" for sinfo in stack): # We're calling ourselves recursively, maybe because we're testing # properties. Return an int to try to get back on track. - return 17 + return 17 # type: ignore[return-value] caller_frame = stack[0][0] return qualname_from_frame(caller_frame) # pylint: disable=missing-class-docstring, missing-function-docstring, unused-argument class Parent: - def meth(self): + def meth(self) -> Optional[str]: return get_qualname() @property - def a_property(self): + def a_property(self) -> Optional[str]: return get_qualname() class Child(Parent): @@ -223,16 +228,16 @@ class MultiChild(SomethingElse, Child): pass -def no_arguments(): +def no_arguments() -> Optional[str]: return get_qualname() -def plain_old_function(a, b): +def plain_old_function(a: Any, b: Any) -> Optional[str]: return get_qualname() -def fake_out(self): +def fake_out(self: Any) -> Optional[str]: return get_qualname() -def patch_meth(self): +def patch_meth(self: Any) -> Optional[str]: return get_qualname() # pylint: enable=missing-class-docstring, missing-function-docstring, unused-argument @@ -246,38 +251,38 @@ run_in_temp_dir = False - def test_method(self): + def test_method(self) -> None: assert Parent().meth() == "tests.test_context.Parent.meth" - def test_inherited_method(self): + def test_inherited_method(self) -> None: assert Child().meth() == "tests.test_context.Parent.meth" - def test_mi_inherited_method(self): + def test_mi_inherited_method(self) -> None: assert MultiChild().meth() == "tests.test_context.Parent.meth" - def test_no_arguments(self): + def test_no_arguments(self) -> None: assert no_arguments() == "tests.test_context.no_arguments" - def test_plain_old_function(self): + def test_plain_old_function(self) -> None: assert plain_old_function(0, 1) == "tests.test_context.plain_old_function" - def test_fake_out(self): + def test_fake_out(self) -> None: assert fake_out(0) == "tests.test_context.fake_out" - def test_property(self): + def test_property(self) -> None: assert Parent().a_property == "tests.test_context.Parent.a_property" - def test_changeling(self): + def test_changeling(self) -> None: c = Child() - c.meth = patch_meth - assert c.meth(c) == "tests.test_context.patch_meth" + c.meth = patch_meth # type: ignore[assignment] + assert c.meth(c) == "tests.test_context.patch_meth" # type: ignore[call-arg] - def test_bug_829(self): + def test_bug_829(self) -> None: # 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): + def test_bug_1210(self) -> None: # 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"]) diff -Nru python-coverage-6.5.0+dfsg1/tests/test_coverage.py python-coverage-7.2.7+dfsg1/tests/test_coverage.py --- python-coverage-6.5.0+dfsg1/tests/test_coverage.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_coverage.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Tests for coverage.py.""" +from __future__ import annotations + import pytest import coverage @@ -10,13 +12,12 @@ from coverage.exceptions import NoDataError from tests.coveragetest import CoverageTest -from tests.helpers import xfail_pypy_3749 class TestCoverageTest(CoverageTest): """Make sure our complex self.check_coverage method works.""" - def test_successful_coverage(self): + def test_successful_coverage(self) -> None: # The simplest run possible. self.check_coverage("""\ a = 1 @@ -50,7 +51,7 @@ missing=("47-49", "3", "100,102") ) - def test_failed_coverage(self): + def test_failed_coverage(self) -> None: # If the lines are wrong, the message shows right and wrong. with pytest.raises(AssertionError, match=r"\[1, 2] != \[1]"): self.check_coverage("""\ @@ -90,7 +91,7 @@ missing=("37", "4-10"), ) - def test_exceptions_really_fail(self): + def test_exceptions_really_fail(self) -> None: # An assert in the checked code will really raise up to us. with pytest.raises(AssertionError, match="This is bad"): self.check_coverage("""\ @@ -111,7 +112,7 @@ class BasicCoverageTest(CoverageTest): """The simplest tests, for quick smoke testing of fundamental changes.""" - def test_simple(self): + def test_simple(self) -> None: self.check_coverage("""\ a = 1 b = 2 @@ -122,7 +123,7 @@ """, [1,2,4,6], report="4 0 0 0 100%") - def test_indentation_wackiness(self): + def test_indentation_wackiness(self) -> None: # Partial final lines are OK. self.check_coverage("""\ import sys @@ -131,7 +132,7 @@ """, # indented last line [1,2,3], "3") - def test_multiline_initializer(self): + def test_multiline_initializer(self) -> None: self.check_coverage("""\ d = { 'foo': 1+2, @@ -143,7 +144,7 @@ """, [1,7], "") - def test_list_comprehension(self): + def test_list_comprehension(self) -> None: self.check_coverage("""\ l = [ 2*i for i in range(10) @@ -157,7 +158,7 @@ class SimpleStatementTest(CoverageTest): """Testing simple single-line statements.""" - def test_expression(self): + def test_expression(self) -> None: # Bare expressions as statements are tricky: some implementations # optimize some of them away. All implementations seem to count # the implicit return at the end as executable. @@ -186,7 +187,7 @@ """, ([1,2,4], [4]), "") - def test_assert(self): + def test_assert(self) -> None: self.check_coverage("""\ assert (1 + 2) assert (1 + @@ -198,7 +199,7 @@ """, [1,2,4,5], "") - def test_assignment(self): + def test_assignment(self) -> None: # Simple variable assignment self.check_coverage("""\ a = (1 + 2) @@ -209,7 +210,7 @@ """, [1,2,4], "") - def test_assign_tuple(self): + def test_assign_tuple(self) -> None: self.check_coverage("""\ a = 1 a,b,c = 7,8,9 @@ -217,7 +218,7 @@ """, [1,2,3], "") - def test_more_assignments(self): + def test_more_assignments(self) -> None: self.check_coverage("""\ x = [] d = {} @@ -232,7 +233,7 @@ """, [1, 2, 3], "") - def test_attribute_assignment(self): + def test_attribute_assignment(self) -> None: # Attribute assignment self.check_coverage("""\ class obj: pass @@ -245,7 +246,7 @@ """, [1,2,3,4,6], "") - def test_list_of_attribute_assignment(self): + def test_list_of_attribute_assignment(self) -> None: self.check_coverage("""\ class obj: pass o = obj() @@ -259,7 +260,7 @@ """, [1,2,3,4,7], "") - def test_augmented_assignment(self): + def test_augmented_assignment(self) -> None: self.check_coverage("""\ a = 1 a += 1 @@ -270,7 +271,7 @@ """, [1,2,3,5], "") - def test_triple_string_stuff(self): + def test_triple_string_stuff(self) -> None: self.check_coverage("""\ a = ''' a multiline @@ -292,7 +293,7 @@ """, [1,5,11], "") - def test_pass(self): + def test_pass(self) -> None: # pass is tricky: if it's the only statement in a block, then it is # "executed". But if it is not the only statement, then it is not. self.check_coverage("""\ @@ -329,7 +330,7 @@ """, ([1,2,4,5], [1,2,5]), "") - def test_del(self): + def test_del(self) -> None: self.check_coverage("""\ d = { 'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1 } del d['a'] @@ -343,7 +344,7 @@ """, [1,2,3,6,9], "") - def test_raise(self): + def test_raise(self) -> None: self.check_coverage("""\ try: raise Exception( @@ -354,7 +355,7 @@ """, [1,2,5,6], "") - def test_raise_followed_by_statement(self): + def test_raise_followed_by_statement(self) -> None: if env.PYBEHAVIOR.omit_after_jump: lines = [1,2,4,5] missing = "" @@ -370,7 +371,7 @@ """, lines=lines, missing=missing) - def test_return(self): + def test_return(self) -> None: self.check_coverage("""\ def fn(): a = 1 @@ -403,7 +404,7 @@ """, [1,2,3,7,8], "") - def test_return_followed_by_statement(self): + def test_return_followed_by_statement(self) -> None: if env.PYBEHAVIOR.omit_after_return: lines = [1,2,3,6,7] missing = "" @@ -422,7 +423,7 @@ lines=lines, missing=missing, ) - def test_yield(self): + def test_yield(self) -> None: self.check_coverage("""\ def gen(): yield 1 @@ -436,7 +437,7 @@ """, [1,2,3,6,8,9], "") - def test_break(self): + def test_break(self) -> None: if env.PYBEHAVIOR.omit_after_jump: lines = [1,2,3,5] missing = "" @@ -453,7 +454,7 @@ """, lines=lines, missing=missing) - def test_continue(self): + def test_continue(self) -> None: if env.PYBEHAVIOR.omit_after_jump: lines = [1,2,3,5] missing = "" @@ -470,7 +471,7 @@ """, lines=lines, missing=missing) - def test_strange_unexecuted_continue(self): + def test_strange_unexecuted_continue(self) -> None: # Peephole optimization of jumps to jumps can mean that some statements # never hit the line tracer. The behavior is different in different # versions of Python, so be careful when running this test. @@ -501,7 +502,7 @@ missing=["", "6"], ) - def test_import(self): + def test_import(self) -> None: self.check_coverage("""\ import string from sys import path @@ -548,7 +549,7 @@ """, [1,3], "") - def test_global(self): + def test_global(self) -> None: self.check_coverage("""\ g = h = i = 1 def fn(): @@ -569,7 +570,7 @@ """, [1,2,3,4,5], "") - def test_exec(self): + def test_exec(self) -> None: self.check_coverage("""\ a = b = c = 1 exec("a = 2") @@ -599,7 +600,7 @@ """, [1,2,3,4,7], "") - def test_extra_doc_string(self): + def test_extra_doc_string(self) -> None: self.check_coverage("""\ a = 1 "An extra docstring, should be a comment." @@ -622,7 +623,7 @@ "", ) - def test_nonascii(self): + def test_nonascii(self) -> None: self.check_coverage("""\ # coding: utf-8 a = 2 @@ -631,7 +632,7 @@ [2, 3] ) - def test_module_docstring(self): + def test_module_docstring(self) -> None: self.check_coverage("""\ '''I am a module docstring.''' a = 2 @@ -653,7 +654,7 @@ class CompoundStatementTest(CoverageTest): """Testing coverage of multi-line compound statements.""" - def test_statement_list(self): + def test_statement_list(self) -> None: self.check_coverage("""\ a = 1; b = 2; c = 3 @@ -663,7 +664,7 @@ """, [1,2,3,5], "") - def test_if(self): + def test_if(self) -> None: self.check_coverage("""\ a = 1 if a == 1: @@ -706,7 +707,7 @@ """, [1,2,3,4,6,8,9], "6-8") - def test_elif(self): + def test_elif(self) -> None: self.check_coverage("""\ a = 1; b = 2; c = 3; if a == 1: @@ -744,7 +745,7 @@ [1,2,3,4,5,7,8], "3, 5", report="7 2 4 2 64% 3, 5", ) - def test_elif_no_else(self): + def test_elif_no_else(self) -> None: self.check_coverage("""\ a = 1; b = 2; c = 3; if a == 1: @@ -766,7 +767,7 @@ [1,2,3,4,5,6], "3", report="6 1 4 2 70% 3, 4->6", ) - def test_elif_bizarre(self): + def test_elif_bizarre(self) -> None: self.check_coverage("""\ def f(self): if self==1: @@ -784,7 +785,7 @@ """, [1,2,3,4,5,6,7,8,9,10,11,13], "2-13") - def test_split_if(self): + def test_split_if(self) -> None: self.check_coverage("""\ a = 1; b = 2; c = 3; if \\ @@ -825,7 +826,7 @@ """, [1,2,4,5,7,9,10], "4, 7") - def test_pathological_split_if(self): + def test_pathological_split_if(self) -> None: self.check_coverage("""\ a = 1; b = 2; c = 3; if ( @@ -872,7 +873,7 @@ """, [1,2,5,6,9,11,12], "5, 9") - def test_absurd_split_if(self): + def test_absurd_split_if(self) -> None: self.check_coverage("""\ a = 1; b = 2; c = 3; if a == 1 \\ @@ -913,7 +914,7 @@ """, [1,2,4,5,7,9,10], "4, 7") - def test_constant_if(self): + def test_constant_if(self) -> None: if env.PYBEHAVIOR.keep_constant_test: lines = [1, 2, 3] else: @@ -927,7 +928,7 @@ "", ) - def test_while(self): + def test_while(self) -> None: self.check_coverage("""\ a = 3; b = 0 while a: @@ -945,7 +946,7 @@ """, [1,2,3,4,5], "") - def test_while_else(self): + def test_while_else(self) -> None: # Take the else branch. self.check_coverage("""\ a = 3; b = 0 @@ -970,7 +971,7 @@ """, [1,2,3,4,5,7,8], "7") - def test_split_while(self): + def test_split_while(self) -> None: self.check_coverage("""\ a = 3; b = 0 while \\ @@ -991,7 +992,7 @@ """, [1,2,5,6,7], "") - def test_for(self): + def test_for(self) -> None: self.check_coverage("""\ a = 0 for i in [1,2,3,4,5]: @@ -1017,7 +1018,7 @@ """, [1,2,3,4,5], "") - def test_for_else(self): + def test_for_else(self) -> None: self.check_coverage("""\ a = 0 for i in range(5): @@ -1038,7 +1039,7 @@ """, [1,2,3,4,6,7], "6") - def test_split_for(self): + def test_split_for(self) -> None: self.check_coverage("""\ a = 0 for \\ @@ -1058,7 +1059,7 @@ """, [1,2,6,7], "") - def test_try_except(self): + def test_try_except(self) -> None: self.check_coverage("""\ a = 0 try: @@ -1119,8 +1120,8 @@ arcz_missing="45 58", ) - def test_try_except_stranded_else(self): - if env.PYBEHAVIOR.omit_after_jump: + def test_try_except_stranded_else(self) -> None: + if env.PYBEHAVIOR.optimize_unreachable_try_else: # The else can't be reached because the try ends with a raise. lines = [1,2,3,4,5,6,9] missing = "" @@ -1148,7 +1149,7 @@ arcz_missing=arcz_missing, ) - def test_try_finally(self): + def test_try_finally(self) -> None: self.check_coverage("""\ a = 0 try: @@ -1172,7 +1173,7 @@ """, [1,2,3,4,5,7,8,9,10], "") - def test_function_def(self): + def test_function_def(self) -> None: self.check_coverage("""\ a = 99 def foo(): @@ -1214,7 +1215,7 @@ """, [1,10,12,13], "") - def test_class_def(self): + def test_class_def(self) -> None: arcz="-22 2D DE E-2 23 36 6A A-2 -68 8-6 -AB B-A" self.check_coverage("""\ # A comment. @@ -1240,7 +1241,7 @@ class ExcludeTest(CoverageTest): """Tests of the exclusion feature to mark lines as not covered.""" - def test_default(self): + def test_default(self) -> None: # A number of forms of pragma comment are accepted. self.check_coverage("""\ a = 1 @@ -1254,7 +1255,7 @@ [1,3,5,7] ) - def test_simple(self): + def test_simple(self) -> None: self.check_coverage("""\ a = 1; b = 2 @@ -1263,7 +1264,7 @@ """, [1,3], "", excludes=['-cc']) - def test_two_excludes(self): + def test_two_excludes(self) -> None: self.check_coverage("""\ a = 1; b = 2 @@ -1275,7 +1276,7 @@ """, [1,3,5,7], "5", excludes=['-cc', '-xx']) - def test_excluding_if_suite(self): + def test_excluding_if_suite(self) -> None: self.check_coverage("""\ a = 1; b = 2 @@ -1287,7 +1288,7 @@ """, [1,7], "", excludes=['not-here']) - def test_excluding_if_but_not_else_suite(self): + def test_excluding_if_but_not_else_suite(self) -> None: self.check_coverage("""\ a = 1; b = 2 @@ -1302,7 +1303,7 @@ """, [1,8,9,10], "", excludes=['not-here']) - def test_excluding_else_suite(self): + def test_excluding_else_suite(self) -> None: self.check_coverage("""\ a = 1; b = 2 @@ -1337,7 +1338,7 @@ """, [1,3,4,5,6,17], "", excludes=['#pragma: NO COVER']) - def test_excluding_elif_suites(self): + def test_excluding_elif_suites(self) -> None: self.check_coverage("""\ a = 1; b = 2 @@ -1355,7 +1356,7 @@ """, [1,3,4,5,6,11,12,13], "11-12", excludes=['#pragma: NO COVER']) - def test_excluding_oneline_if(self): + def test_excluding_oneline_if(self) -> None: self.check_coverage("""\ def foo(): a = 2 @@ -1366,7 +1367,7 @@ """, [1,2,4,6], "", excludes=["no cover"]) - def test_excluding_a_colon_not_a_suite(self): + def test_excluding_a_colon_not_a_suite(self) -> None: self.check_coverage("""\ def foo(): l = list(range(10)) @@ -1377,7 +1378,7 @@ """, [1,2,4,6], "", excludes=["no cover"]) - def test_excluding_for_suite(self): + def test_excluding_for_suite(self) -> None: self.check_coverage("""\ a = 0 for i in [1,2,3,4,5]: #pragma: NO COVER @@ -1405,7 +1406,7 @@ """, [1,7], "", excludes=['#pragma: NO COVER']) - def test_excluding_for_else(self): + def test_excluding_for_else(self) -> None: self.check_coverage("""\ a = 0 for i in range(5): @@ -1417,7 +1418,7 @@ """, [1,2,3,4,7], "", excludes=['#pragma: NO COVER']) - def test_excluding_while(self): + def test_excluding_while(self) -> None: self.check_coverage("""\ a = 3; b = 0 while a*b: #pragma: NO COVER @@ -1437,7 +1438,7 @@ """, [1,7], "", excludes=['#pragma: NO COVER']) - def test_excluding_while_else(self): + def test_excluding_while_else(self) -> None: self.check_coverage("""\ a = 3; b = 0 while a: @@ -1449,7 +1450,7 @@ """, [1,2,3,4,7], "", excludes=['#pragma: NO COVER']) - def test_excluding_try_except(self): + def test_excluding_try_except(self) -> None: self.check_coverage("""\ a = 0 try: @@ -1496,8 +1497,8 @@ arcz_missing="58", ) - def test_excluding_try_except_stranded_else(self): - if env.PYBEHAVIOR.omit_after_jump: + def test_excluding_try_except_stranded_else(self) -> None: + if env.PYBEHAVIOR.optimize_unreachable_try_else: # The else can't be reached because the try ends with a raise. arcz = ".1 12 23 34 45 56 69 9." arcz_missing = "" @@ -1520,7 +1521,7 @@ arcz_missing=arcz_missing, ) - def test_excluding_if_pass(self): + def test_excluding_if_pass(self) -> None: # From a comment on the coverage.py page by Michael McNeil Forbes: self.check_coverage("""\ def f(): @@ -1533,7 +1534,7 @@ """, [1,7], "", excludes=["no cover"]) - def test_excluding_function(self): + def test_excluding_function(self) -> None: self.check_coverage("""\ def fn(foo): #pragma: NO COVER a = 1 @@ -1545,7 +1546,7 @@ """, [6,7], "", excludes=['#pragma: NO COVER']) - def test_excluding_method(self): + def test_excluding_method(self) -> None: self.check_coverage("""\ class Fooey: def __init__(self): @@ -1559,7 +1560,7 @@ """, [1,2,3,8,9], "", excludes=['#pragma: NO COVER']) - def test_excluding_class(self): + def test_excluding_class(self) -> None: self.check_coverage("""\ class Fooey: #pragma: NO COVER def __init__(self): @@ -1573,7 +1574,7 @@ """, [8,9], "", excludes=['#pragma: NO COVER']) - def test_excludes_non_ascii(self): + def test_excludes_non_ascii(self) -> None: self.check_coverage("""\ # coding: utf-8 a = 1; b = 2 @@ -1584,7 +1585,7 @@ [2, 4], "", excludes=['✘cover'] ) - def test_formfeed(self): + def test_formfeed(self) -> None: # https://github.com/nedbat/coveragepy/issues/461 self.check_coverage("""\ x = 1 @@ -1600,7 +1601,7 @@ [1, 6], "", excludes=['assert'], ) - def test_excluded_comprehension_branches(self): + def test_excluded_comprehension_branches(self) -> None: # https://github.com/nedbat/coveragepy/issues/1271 self.check_coverage("""\ x, y = [0], [1] @@ -1618,8 +1619,7 @@ class Py24Test(CoverageTest): """Tests of new syntax in Python 2.4.""" - @xfail_pypy_3749 - def test_function_decorators(self): + def test_function_decorators(self) -> None: lines = [1, 2, 3, 4, 6, 8, 10, 12] if env.PYBEHAVIOR.trace_decorated_def: lines = sorted(lines + [9]) @@ -1639,8 +1639,7 @@ """, lines, "") - @xfail_pypy_3749 - def test_function_decorators_with_args(self): + def test_function_decorators_with_args(self) -> None: lines = [1, 2, 3, 4, 5, 6, 8, 10, 12] if env.PYBEHAVIOR.trace_decorated_def: lines = sorted(lines + [9]) @@ -1660,8 +1659,7 @@ """, lines, "") - @xfail_pypy_3749 - def test_double_function_decorators(self): + def test_double_function_decorators(self) -> None: lines = [1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 14, 15, 17, 19, 21, 22, 24, 26] if env.PYBEHAVIOR.trace_decorated_def: lines = sorted(lines + [16, 23]) @@ -1699,7 +1697,7 @@ class Py25Test(CoverageTest): """Tests of new syntax in Python 2.5.""" - def test_with_statement(self): + def test_with_statement(self) -> None: self.check_coverage("""\ class Managed: def __enter__(self): @@ -1722,7 +1720,7 @@ """, [1,2,3,5,6,8,9,10,11,13,14,15,16,17,18], "") - def test_try_except_finally(self): + def test_try_except_finally(self) -> None: self.check_coverage("""\ a = 0; b = 0 try: @@ -1802,8 +1800,8 @@ arcz_missing="45 59", ) - def test_try_except_finally_stranded_else(self): - if env.PYBEHAVIOR.omit_after_jump: + def test_try_except_finally_stranded_else(self) -> None: + if env.PYBEHAVIOR.optimize_unreachable_try_else: # The else can't be reached because the try ends with a raise. lines = [1,2,3,4,5,6,10,11] missing = "" @@ -1839,33 +1837,33 @@ run_in_temp_dir = False - def test_not_singleton(self): + def test_not_singleton(self) -> None: # You *can* create another coverage object. coverage.Coverage() coverage.Coverage() - def test_old_name_and_new_name(self): + def test_old_name_and_new_name(self) -> None: assert coverage.coverage is coverage.Coverage class ReportingTest(CoverageTest): """Tests of some reporting behavior.""" - def test_no_data_to_report_on_annotate(self): + def test_no_data_to_report_on_annotate(self) -> None: # Reporting with no data produces a nice message and no output # directory. with pytest.raises(NoDataError, match="No data to report."): self.command_line("annotate -d ann") self.assert_doesnt_exist("ann") - def test_no_data_to_report_on_html(self): + def test_no_data_to_report_on_html(self) -> None: # Reporting with no data produces a nice message and no output # directory. with pytest.raises(NoDataError, match="No data to report."): self.command_line("html -d htmlcov") self.assert_doesnt_exist("htmlcov") - def test_no_data_to_report_on_xml(self): + def test_no_data_to_report_on_xml(self) -> None: # Reporting with no data produces a nice message. with pytest.raises(NoDataError, match="No data to report."): self.command_line("xml") diff -Nru python-coverage-6.5.0+dfsg1/tests/test_data.py python-coverage-7.2.7+dfsg1/tests/test_data.py --- python-coverage-6.5.0+dfsg1/tests/test_data.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_data.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Tests for coverage.data""" +from __future__ import annotations + import glob import os import os.path @@ -10,7 +12,11 @@ import sqlite3 import threading +from typing import ( + Any, Callable, Collection, Dict, Iterable, Mapping, Set, TypeVar, Union +) from unittest import mock + import pytest from coverage.data import CoverageData, combine_parallel_data @@ -18,6 +24,7 @@ from coverage.debug import DebugControlString from coverage.exceptions import DataError, NoDataError from coverage.files import PathAliases, canonical_filename +from coverage.types import FilePathClasses, FilePathType, TArc, TLineNo from tests.coveragetest import CoverageTest from tests.helpers import assert_count_equal @@ -58,7 +65,7 @@ MEASURED_FILES_3_4 = ['x.py', 'y.py', 'z.py'] -def DebugCoverageData(*args, **kwargs): +def DebugCoverageData(*args: Any, **kwargs: Any) -> CoverageData: """Factory for CovergeData instances with debugging turned on. This lets us exercise the debugging lines in sqldata.py. We don't make @@ -73,25 +80,31 @@ # This is a way to get a mix of debug options across the tests. options.extend(["sqldata"]) debug = DebugControlString(options=options) - return CoverageData(*args, debug=debug, **kwargs) + return CoverageData(*args, debug=debug, **kwargs) # type: ignore[misc] + +TCoverageData = Callable[..., CoverageData] -def assert_line_counts(covdata, counts, fullpath=False): +def assert_line_counts( + covdata: CoverageData, + counts: Mapping[str, int], + fullpath: bool = False, +) -> None: """Check that the line_counts of `covdata` is `counts`.""" assert line_counts(covdata, fullpath) == counts -def assert_measured_files(covdata, measured): +def assert_measured_files(covdata: CoverageData, measured: Iterable[str]) -> None: """Check that `covdata`'s measured files are `measured`.""" assert_count_equal(covdata.measured_files(), measured) -def assert_lines1_data(covdata): +def assert_lines1_data(covdata: CoverageData) -> None: """Check that `covdata` has the data from LINES1.""" assert_line_counts(covdata, SUMMARY_1) assert_measured_files(covdata, MEASURED_FILES_1) assert_count_equal(covdata.lines("a.py"), A_PY_LINES_1) assert not covdata.has_arcs() -def assert_arcs3_data(covdata): +def assert_arcs3_data(covdata: CoverageData) -> None: """Check that `covdata` has the data from ARCS3.""" assert_line_counts(covdata, SUMMARY_3) assert_measured_files(covdata, MEASURED_FILES_3) @@ -102,7 +115,9 @@ assert covdata.has_arcs() -def dicts_from_sets(file_data): +TData = TypeVar("TData", bound=Union[TLineNo, TArc]) + +def dicts_from_sets(file_data: Dict[str, Set[TData]]) -> Dict[str, Dict[TData, None]]: """Convert a dict of sets into a dict of dicts. Before 6.0, file data was a dict with None as the values. In 6.0, file @@ -115,65 +130,73 @@ class CoverageDataTest(CoverageTest): """Test cases for CoverageData.""" - def test_empty_data_is_false(self): + def test_empty_data_is_false(self) -> None: covdata = DebugCoverageData() assert not covdata self.assert_doesnt_exist(".coverage") - def test_empty_data_is_false_when_read(self): + def test_empty_data_is_false_when_read(self) -> None: covdata = DebugCoverageData() covdata.read() assert not covdata self.assert_doesnt_exist(".coverage") - def test_line_data_is_true(self): + def test_line_data_is_true(self) -> None: covdata = DebugCoverageData() covdata.add_lines(LINES_1) assert covdata - def test_arc_data_is_true(self): + def test_arc_data_is_true(self) -> None: covdata = DebugCoverageData() covdata.add_arcs(ARCS_3) assert covdata - def test_empty_line_data_is_false(self): + def test_empty_line_data_is_false(self) -> None: covdata = DebugCoverageData() covdata.add_lines({}) assert not covdata - def test_empty_arc_data_is_false(self): + def test_empty_arc_data_is_false(self) -> None: covdata = DebugCoverageData() covdata.add_arcs({}) assert not covdata @pytest.mark.parametrize("lines", [LINES_1, dicts_from_sets(LINES_1)]) - def test_adding_lines(self, lines): + def test_adding_lines(self, lines: Mapping[str, Collection[TLineNo]]) -> None: covdata = DebugCoverageData() covdata.add_lines(lines) assert_lines1_data(covdata) @pytest.mark.parametrize("arcs", [ARCS_3, dicts_from_sets(ARCS_3)]) - def test_adding_arcs(self, arcs): + def test_adding_arcs(self, arcs: Mapping[str, Collection[TArc]]) -> None: covdata = DebugCoverageData() covdata.add_arcs(arcs) assert_arcs3_data(covdata) - def test_ok_to_add_lines_twice(self): + def test_ok_to_add_lines_twice(self) -> None: covdata = DebugCoverageData() covdata.add_lines(LINES_1) covdata.add_lines(LINES_2) assert_line_counts(covdata, SUMMARY_1_2) assert_measured_files(covdata, MEASURED_FILES_1_2) - def test_ok_to_add_arcs_twice(self): + def test_ok_to_add_arcs_twice(self) -> None: + covdata = DebugCoverageData() + covdata.add_arcs(ARCS_3) + covdata.add_arcs(ARCS_4) + assert_line_counts(covdata, SUMMARY_3_4) + assert_measured_files(covdata, MEASURED_FILES_3_4) + + def test_ok_to_add_empty_arcs(self) -> None: covdata = DebugCoverageData() covdata.add_arcs(ARCS_3) covdata.add_arcs(ARCS_4) + covdata.add_arcs(dict.fromkeys(ARCS_3, set())) assert_line_counts(covdata, SUMMARY_3_4) assert_measured_files(covdata, MEASURED_FILES_3_4) @pytest.mark.parametrize("klass", [CoverageData, DebugCoverageData]) - def test_cant_add_arcs_with_lines(self, klass): + def test_cant_add_arcs_with_lines(self, klass: TCoverageData) -> None: covdata = klass() covdata.add_lines(LINES_1) msg = "Can't add branch measurements to existing line data" @@ -181,26 +204,26 @@ covdata.add_arcs(ARCS_3) @pytest.mark.parametrize("klass", [CoverageData, DebugCoverageData]) - def test_cant_add_lines_with_arcs(self, klass): + def test_cant_add_lines_with_arcs(self, klass: TCoverageData) -> None: covdata = klass() covdata.add_arcs(ARCS_3) msg = "Can't add line measurements to existing branch data" with pytest.raises(DataError, match=msg): covdata.add_lines(LINES_1) - def test_touch_file_with_lines(self): + def test_touch_file_with_lines(self) -> None: covdata = DebugCoverageData() covdata.add_lines(LINES_1) covdata.touch_file('zzz.py') assert_measured_files(covdata, MEASURED_FILES_1 + ['zzz.py']) - def test_touch_file_with_arcs(self): + def test_touch_file_with_arcs(self) -> None: covdata = DebugCoverageData() covdata.add_arcs(ARCS_3) covdata.touch_file('zzz.py') assert_measured_files(covdata, MEASURED_FILES_3 + ['zzz.py']) - def test_set_query_contexts(self): + def test_set_query_contexts(self) -> None: covdata = DebugCoverageData() covdata.set_context('test_a') covdata.add_lines(LINES_1) @@ -209,14 +232,14 @@ covdata.set_query_contexts(['other']) assert covdata.lines('a.py') == [] - def test_no_lines_vs_unmeasured_file(self): + def test_no_lines_vs_unmeasured_file(self) -> None: covdata = DebugCoverageData() covdata.add_lines(LINES_1) covdata.touch_file('zzz.py') assert covdata.lines('zzz.py') == [] assert covdata.lines('no_such_file.py') is None - def test_lines_with_contexts(self): + def test_lines_with_contexts(self) -> None: covdata = DebugCoverageData() covdata.set_context('test_a') covdata.add_lines(LINES_1) @@ -226,7 +249,7 @@ covdata.set_query_contexts(['other']) assert covdata.lines('a.py') == [] - def test_contexts_by_lineno_with_lines(self): + def test_contexts_by_lineno_with_lines(self) -> None: covdata = DebugCoverageData() covdata.set_context('test_a') covdata.add_lines(LINES_1) @@ -234,7 +257,7 @@ assert covdata.contexts_by_lineno('a.py') == expected @pytest.mark.parametrize("lines", [LINES_1, dicts_from_sets(LINES_1)]) - def test_no_duplicate_lines(self, lines): + def test_no_duplicate_lines(self, lines: Mapping[str, Collection[TLineNo]]) -> None: covdata = DebugCoverageData() covdata.set_context("context1") covdata.add_lines(lines) @@ -243,7 +266,7 @@ assert covdata.lines('a.py') == A_PY_LINES_1 @pytest.mark.parametrize("arcs", [ARCS_3, dicts_from_sets(ARCS_3)]) - def test_no_duplicate_arcs(self, arcs): + def test_no_duplicate_arcs(self, arcs: Mapping[str, Collection[TArc]]) -> None: covdata = DebugCoverageData() covdata.set_context("context1") covdata.add_arcs(arcs) @@ -251,7 +274,7 @@ covdata.add_arcs(arcs) assert covdata.arcs('x.py') == X_PY_ARCS_3 - def test_no_arcs_vs_unmeasured_file(self): + def test_no_arcs_vs_unmeasured_file(self) -> None: covdata = DebugCoverageData() covdata.add_arcs(ARCS_3) covdata.touch_file('zzz.py') @@ -260,7 +283,7 @@ assert covdata.arcs('zzz.py') == [] assert covdata.arcs('no_such_file.py') is None - def test_arcs_with_contexts(self): + def test_arcs_with_contexts(self) -> None: covdata = DebugCoverageData() covdata.set_context('test_x') covdata.add_arcs(ARCS_3) @@ -270,20 +293,20 @@ covdata.set_query_contexts(['other']) assert covdata.arcs('x.py') == [] - def test_contexts_by_lineno_with_arcs(self): + def test_contexts_by_lineno_with_arcs(self) -> None: covdata = DebugCoverageData() covdata.set_context('test_x') covdata.add_arcs(ARCS_3) expected = {1: ['test_x'], 2: ['test_x'], 3: ['test_x']} assert covdata.contexts_by_lineno('x.py') == expected - def test_contexts_by_lineno_with_unknown_file(self): + def test_contexts_by_lineno_with_unknown_file(self) -> None: covdata = DebugCoverageData() covdata.set_context('test_x') covdata.add_arcs(ARCS_3) assert covdata.contexts_by_lineno('xyz.py') == {} - def test_context_by_lineno_with_query_contexts_with_lines(self): + def test_context_by_lineno_with_query_contexts_with_lines(self) -> None: covdata = DebugCoverageData() covdata.set_context("test_1") covdata.add_lines(LINES_1) @@ -292,7 +315,7 @@ covdata.set_query_context("test_1") assert covdata.contexts_by_lineno("a.py") == dict.fromkeys([1,2], ["test_1"]) - def test_context_by_lineno_with_query_contexts_with_arcs(self): + def test_context_by_lineno_with_query_contexts_with_arcs(self) -> None: covdata = DebugCoverageData() covdata.set_context("test_1") covdata.add_arcs(ARCS_3) @@ -301,7 +324,7 @@ covdata.set_query_context("test_1") assert covdata.contexts_by_lineno("x.py") == dict.fromkeys([1,2,3], ["test_1"]) - def test_file_tracer_name(self): + def test_file_tracer_name(self) -> None: covdata = DebugCoverageData() covdata.add_lines({ "p1.foo": [1, 2, 3], @@ -314,7 +337,7 @@ assert covdata.file_tracer("main.py") == "" assert covdata.file_tracer("p3.not_here") is None - def test_ok_to_repeat_file_tracer(self): + def test_ok_to_repeat_file_tracer(self) -> None: covdata = DebugCoverageData() covdata.add_lines({ "p1.foo": [1, 2, 3], @@ -324,7 +347,7 @@ covdata.add_file_tracers({"p1.foo": "p1.plugin"}) assert covdata.file_tracer("p1.foo") == "p1.plugin" - def test_ok_to_set_empty_file_tracer(self): + def test_ok_to_set_empty_file_tracer(self) -> None: covdata = DebugCoverageData() covdata.add_lines({ "p1.foo": [1, 2, 3], @@ -335,17 +358,7 @@ assert covdata.file_tracer("p1.foo") == "p1.plugin" assert covdata.file_tracer("main.py") == "" - def test_cant_file_tracer_unmeasured_files(self): - covdata = DebugCoverageData() - msg = "Can't add file tracer data for unmeasured file 'p1.foo'" - with pytest.raises(DataError, match=msg): - covdata.add_file_tracers({"p1.foo": "p1.plugin"}) - - covdata.add_lines({"p2.html": [10, 11, 12]}) - with pytest.raises(DataError, match=msg): - covdata.add_file_tracers({"p1.foo": "p1.plugin"}) - - def test_cant_change_file_tracer_name(self): + def test_cant_change_file_tracer_name(self) -> None: covdata = DebugCoverageData() covdata.add_lines({"p1.foo": [1, 2, 3]}) covdata.add_file_tracers({"p1.foo": "p1.plugin"}) @@ -354,7 +367,7 @@ with pytest.raises(DataError, match=msg): covdata.add_file_tracers({"p1.foo": "p1.plugin.foo"}) - def test_update_lines(self): + def test_update_lines(self) -> None: covdata1 = DebugCoverageData(suffix='1') covdata1.add_lines(LINES_1) @@ -368,7 +381,7 @@ assert_line_counts(covdata3, SUMMARY_1_2) assert_measured_files(covdata3, MEASURED_FILES_1_2) - def test_update_arcs(self): + def test_update_arcs(self) -> None: covdata1 = DebugCoverageData(suffix='1') covdata1.add_arcs(ARCS_3) @@ -382,7 +395,7 @@ assert_line_counts(covdata3, SUMMARY_3_4) assert_measured_files(covdata3, MEASURED_FILES_3_4) - def test_update_cant_mix_lines_and_arcs(self): + def test_update_cant_mix_lines_and_arcs(self) -> None: covdata1 = DebugCoverageData(suffix='1') covdata1.add_lines(LINES_1) @@ -395,7 +408,7 @@ with pytest.raises(DataError, match="Can't combine line data with arc data"): covdata2.update(covdata1) - def test_update_file_tracers(self): + def test_update_file_tracers(self) -> None: covdata1 = DebugCoverageData(suffix='1') covdata1.add_lines({ "p1.html": [1, 2, 3, 4], @@ -428,7 +441,7 @@ assert covdata3.file_tracer("p3.foo") == "foo_plugin" assert covdata3.file_tracer("main.py") == "" - def test_update_conflicting_file_tracers(self): + def test_update_conflicting_file_tracers(self) -> None: covdata1 = DebugCoverageData(suffix='1') covdata1.add_lines({"p1.html": [1, 2, 3]}) covdata1.add_file_tracers({"p1.html": "html.plugin"}) @@ -445,7 +458,7 @@ with pytest.raises(DataError, match=msg): covdata2.update(covdata1) - def test_update_file_tracer_vs_no_file_tracer(self): + def test_update_file_tracer_vs_no_file_tracer(self) -> None: covdata1 = DebugCoverageData(suffix="1") covdata1.add_lines({"p1.html": [1, 2, 3]}) covdata1.add_file_tracers({"p1.html": "html.plugin"}) @@ -461,7 +474,7 @@ with pytest.raises(DataError, match=msg): covdata2.update(covdata1) - def test_update_lines_empty(self): + def test_update_lines_empty(self) -> None: covdata1 = DebugCoverageData(suffix='1') covdata1.add_lines(LINES_1) @@ -469,7 +482,7 @@ covdata1.update(covdata2) assert_line_counts(covdata1, SUMMARY_1) - def test_update_arcs_empty(self): + def test_update_arcs_empty(self) -> None: covdata1 = DebugCoverageData(suffix='1') covdata1.add_arcs(ARCS_3) @@ -477,14 +490,14 @@ covdata1.update(covdata2) assert_line_counts(covdata1, SUMMARY_3) - def test_asking_isnt_measuring(self): + def test_asking_isnt_measuring(self) -> None: # Asking about an unmeasured file shouldn't make it seem measured. covdata = DebugCoverageData() assert_measured_files(covdata, []) assert covdata.arcs("missing.py") is None assert_measured_files(covdata, []) - def test_add_to_hash_with_lines(self): + def test_add_to_hash_with_lines(self) -> None: covdata = DebugCoverageData() covdata.add_lines(LINES_1) hasher = mock.Mock() @@ -494,7 +507,7 @@ mock.call.update(""), # file_tracer name ] - def test_add_to_hash_with_arcs(self): + def test_add_to_hash_with_arcs(self) -> None: covdata = DebugCoverageData() covdata.add_arcs(ARCS_3) covdata.add_file_tracers({"y.py": "hologram_plugin"}) @@ -505,7 +518,7 @@ mock.call.update("hologram_plugin"), # file_tracer name ] - def test_add_to_lines_hash_with_missing_file(self): + def test_add_to_lines_hash_with_missing_file(self) -> None: # https://github.com/nedbat/coveragepy/issues/403 covdata = DebugCoverageData() covdata.add_lines(LINES_1) @@ -516,7 +529,7 @@ mock.call.update(None), ] - def test_add_to_arcs_hash_with_missing_file(self): + def test_add_to_arcs_hash_with_missing_file(self) -> None: # https://github.com/nedbat/coveragepy/issues/403 covdata = DebugCoverageData() covdata.add_arcs(ARCS_3) @@ -528,25 +541,25 @@ mock.call.update(None), ] - def test_empty_lines_are_still_lines(self): + def test_empty_lines_are_still_lines(self) -> None: covdata = DebugCoverageData() covdata.add_lines({}) covdata.touch_file("abc.py") assert not covdata.has_arcs() - def test_empty_arcs_are_still_arcs(self): + def test_empty_arcs_are_still_arcs(self) -> None: covdata = DebugCoverageData() covdata.add_arcs({}) covdata.touch_file("abc.py") assert covdata.has_arcs() - def test_cant_touch_in_empty_data(self): + def test_cant_touch_in_empty_data(self) -> None: covdata = DebugCoverageData() msg = "Can't touch files in an empty CoverageData" with pytest.raises(DataError, match=msg): covdata.touch_file("abc.py") - def test_read_and_write_are_opposites(self): + def test_read_and_write_are_opposites(self) -> None: covdata1 = DebugCoverageData() covdata1.add_arcs(ARCS_3) covdata1.write() @@ -555,11 +568,11 @@ covdata2.read() assert_arcs3_data(covdata2) - def test_thread_stress(self): + def test_thread_stress(self) -> None: covdata = DebugCoverageData() exceptions = [] - def thread_main(): + def thread_main() -> None: """Every thread will try to add the same data.""" try: covdata.add_lines(LINES_1) @@ -575,20 +588,52 @@ assert_lines1_data(covdata) assert not exceptions + def test_purge_files_lines(self) -> None: + covdata = DebugCoverageData() + covdata.add_lines(LINES_1) + covdata.add_lines(LINES_2) + assert_line_counts(covdata, SUMMARY_1_2) + covdata.purge_files(["a.py", "b.py"]) + assert_line_counts(covdata, {"a.py": 0, "b.py": 0, "c.py": 1}) + covdata.purge_files(["c.py"]) + assert_line_counts(covdata, {"a.py": 0, "b.py": 0, "c.py": 0}) + # It's OK to "purge" a file that wasn't measured. + covdata.purge_files(["xyz.py"]) + assert_line_counts(covdata, {"a.py": 0, "b.py": 0, "c.py": 0}) + + def test_purge_files_arcs(self) -> None: + covdata = CoverageData() + covdata.add_arcs(ARCS_3) + covdata.add_arcs(ARCS_4) + assert_line_counts(covdata, SUMMARY_3_4) + covdata.purge_files(["x.py", "y.py"]) + assert_line_counts(covdata, {"x.py": 0, "y.py": 0, "z.py": 1}) + covdata.purge_files(["z.py"]) + assert_line_counts(covdata, {"x.py": 0, "y.py": 0, "z.py": 0}) + + def test_cant_purge_in_empty_data(self) -> None: + covdata = DebugCoverageData() + msg = "Can't purge files in an empty CoverageData" + with pytest.raises(DataError, match=msg): + covdata.purge_files(["abc.py"]) + class CoverageDataInTempDirTest(CoverageTest): """Tests of CoverageData that need a temporary directory to make files.""" - def test_read_write_lines(self): - covdata1 = DebugCoverageData("lines.dat") + @pytest.mark.parametrize("file_class", FilePathClasses) + def test_read_write_lines(self, file_class: FilePathType) -> None: + self.assert_doesnt_exist("lines.dat") + covdata1 = DebugCoverageData(file_class("lines.dat")) covdata1.add_lines(LINES_1) covdata1.write() + self.assert_exists("lines.dat") covdata2 = DebugCoverageData("lines.dat") covdata2.read() assert_lines1_data(covdata2) - def test_read_write_arcs(self): + def test_read_write_arcs(self) -> None: covdata1 = DebugCoverageData("arcs.dat") covdata1.add_arcs(ARCS_3) covdata1.write() @@ -597,14 +642,14 @@ covdata2.read() assert_arcs3_data(covdata2) - def test_read_errors(self): + def test_read_errors(self) -> None: self.make_file("xyzzy.dat", "xyzzy") with pytest.raises(DataError, match=r"Couldn't .* '.*[/\\]xyzzy.dat': \S+"): covdata = DebugCoverageData("xyzzy.dat") covdata.read() assert not covdata - def test_hard_read_error(self): + def test_hard_read_error(self) -> None: self.make_file("noperms.dat", "go away") os.chmod("noperms.dat", 0) with pytest.raises(DataError, match=r"Couldn't .* '.*[/\\]noperms.dat': \S+"): @@ -612,17 +657,17 @@ covdata.read() @pytest.mark.parametrize("klass", [CoverageData, DebugCoverageData]) - def test_error_when_closing(self, klass): + def test_error_when_closing(self, klass: TCoverageData) -> None: msg = r"Couldn't .* '.*[/\\]flaked.dat': \S+" with pytest.raises(DataError, match=msg): covdata = klass("flaked.dat") covdata.add_lines(LINES_1) # I don't know how to make a real error, so let's fake one. sqldb = list(covdata._dbs.values())[0] - sqldb.close = lambda: 1/0 + sqldb.close = lambda: 1/0 # type: ignore[assignment] covdata.add_lines(LINES_1) - def test_wrong_schema_version(self): + def test_wrong_schema_version(self) -> None: with sqlite3.connect("wrong_schema.db") as con: con.execute("create table coverage_schema (version integer)") con.execute("insert into coverage_schema (version) values (99)") @@ -632,7 +677,7 @@ covdata.read() assert not covdata - def test_wrong_schema_schema(self): + def test_wrong_schema_schema(self) -> None: with sqlite3.connect("wrong_schema_schema.db") as con: con.execute("create table coverage_schema (xyzzy integer)") con.execute("insert into coverage_schema (xyzzy) values (99)") @@ -646,13 +691,13 @@ class CoverageDataFilesTest(CoverageTest): """Tests of CoverageData file handling.""" - def test_reading_missing(self): + def test_reading_missing(self) -> None: self.assert_doesnt_exist(".coverage") covdata = DebugCoverageData() covdata.read() assert_line_counts(covdata, {}) - def test_writing_and_reading(self): + def test_writing_and_reading(self) -> None: covdata1 = DebugCoverageData() covdata1.add_lines(LINES_1) covdata1.write() @@ -661,7 +706,7 @@ covdata2.read() assert_line_counts(covdata2, SUMMARY_1) - def test_debug_output_with_debug_option(self): + def test_debug_output_with_debug_option(self) -> None: # With debug option dataio, we get debug output about reading and # writing files. debug = DebugControlString(options=["dataio"]) @@ -681,7 +726,7 @@ debug.get_output() ) - def test_debug_output_without_debug_option(self): + def test_debug_output_without_debug_option(self) -> None: # With a debug object, but not the dataio option, we don't get debug # output. debug = DebugControlString(options=[]) @@ -695,7 +740,7 @@ assert debug.get_output() == "" - def test_explicit_suffix(self): + def test_explicit_suffix(self) -> None: self.assert_doesnt_exist(".coverage.SUFFIX") covdata = DebugCoverageData(suffix='SUFFIX') covdata.add_lines(LINES_1) @@ -703,7 +748,7 @@ self.assert_exists(".coverage.SUFFIX") self.assert_doesnt_exist(".coverage") - def test_true_suffix(self): + def test_true_suffix(self) -> None: self.assert_file_count(".coverage.*", 0) # suffix=True will make a randomly named data file. @@ -725,7 +770,7 @@ # In addition to being different, the suffixes have the pid in them. assert all(str(os.getpid()) in fn for fn in data_files2) - def test_combining(self): + def test_combining(self) -> None: self.assert_file_count(".coverage.*", 0) covdata1 = DebugCoverageData(suffix='1') @@ -746,7 +791,7 @@ assert_measured_files(covdata3, MEASURED_FILES_1_2) self.assert_file_count(".coverage.*", 0) - def test_erasing(self): + def test_erasing(self) -> None: covdata1 = DebugCoverageData() covdata1.add_lines(LINES_1) covdata1.write() @@ -758,7 +803,7 @@ covdata2.read() assert_line_counts(covdata2, {}) - def test_erasing_parallel(self): + def test_erasing_parallel(self) -> None: self.make_file("datafile.1") self.make_file("datafile.2") self.make_file(".coverage") @@ -767,7 +812,7 @@ self.assert_file_count("datafile.*", 0) self.assert_exists(".coverage") - def test_combining_with_aliases(self): + def test_combining_with_aliases(self) -> None: covdata1 = DebugCoverageData(suffix='1') covdata1.add_lines({ '/home/ned/proj/src/a.py': {1, 2}, @@ -788,14 +833,16 @@ self.assert_file_count(".coverage.*", 2) + self.make_file("a.py", "") + self.make_file("sub/b.py", "") + self.make_file("template.html", "") covdata3 = DebugCoverageData() aliases = PathAliases() aliases.add("/home/ned/proj/src/", "./") aliases.add(r"c:\ned\test", "./") combine_parallel_data(covdata3, aliases=aliases) self.assert_file_count(".coverage.*", 0) - # covdata3 hasn't been written yet. Should this file exist or not? - #self.assert_exists(".coverage") + self.assert_exists(".coverage") apy = canonical_filename('./a.py') sub_bpy = canonical_filename('./sub/b.py') @@ -805,7 +852,7 @@ assert_measured_files(covdata3, [apy, sub_bpy, template_html]) assert covdata3.file_tracer(template_html) == 'html.plugin' - def test_combining_from_different_directories(self): + def test_combining_from_different_directories(self) -> None: os.makedirs('cov1') covdata1 = DebugCoverageData('cov1/.coverage.1') covdata1.add_lines(LINES_1) @@ -830,7 +877,7 @@ self.assert_doesnt_exist("cov2/.coverage.2") self.assert_exists(".coverage.xxx") - def test_combining_from_files(self): + def test_combining_from_files(self) -> None: os.makedirs('cov1') covdata1 = DebugCoverageData('cov1/.coverage.1') covdata1.add_lines(LINES_1) @@ -860,13 +907,13 @@ self.assert_exists(".coverage.xxx") self.assert_exists("cov2/.coverage.xxx") - def test_combining_from_nonexistent_directories(self): + def test_combining_from_nonexistent_directories(self) -> None: covdata = DebugCoverageData() msg = "Couldn't combine from non-existent path 'xyzzy'" with pytest.raises(NoDataError, match=msg): combine_parallel_data(covdata, data_paths=['xyzzy']) - def test_interleaved_erasing_bug716(self): + def test_interleaved_erasing_bug716(self) -> None: # pytest-cov could produce this scenario. #716 covdata1 = DebugCoverageData() covdata2 = DebugCoverageData() @@ -886,7 +933,7 @@ ("[3-1]", "[b-a]"), ], ) - def test_combining_with_crazy_filename(self, dpart, fpart): + def test_combining_with_crazy_filename(self, dpart: str, fpart: str) -> None: dirname = f"py{dpart}" basename = f"{dirname}/.coverage{fpart}" os.makedirs(dirname) @@ -905,6 +952,26 @@ assert_measured_files(covdata3, MEASURED_FILES_1_2) self.assert_file_count(glob.escape(basename) + ".*", 0) + def test_meta_data(self) -> None: + # The metadata written to the data file shouldn't interfere with + # hashing to remove duplicates, except for debug=process, which + # writes debugging info as metadata. + debug = DebugControlString(options=[]) + covdata1 = CoverageData(basename="meta.1", debug=debug) + covdata1.add_lines(LINES_1) + covdata1.write() + with sqlite3.connect("meta.1") as con: + data = sorted(k for (k,) in con.execute("select key from meta")) + assert data == ["has_arcs", "version"] + + debug = DebugControlString(options=["process"]) + covdata2 = CoverageData(basename="meta.2", debug=debug) + covdata2.add_lines(LINES_1) + covdata2.write() + with sqlite3.connect("meta.2") as con: + data = sorted(k for (k,) in con.execute("select key from meta")) + assert data == ["has_arcs", "sys_argv", "version", "when"] + class DumpsLoadsTest(CoverageTest): """Tests of CoverageData.dumps and loads.""" @@ -912,7 +979,7 @@ run_in_temp_dir = False @pytest.mark.parametrize("klass", [CoverageData, DebugCoverageData]) - def test_serialization(self, klass): + def test_serialization(self, klass: TCoverageData) -> None: covdata1 = klass(no_disk=True) covdata1.add_lines(LINES_1) covdata1.add_lines(LINES_2) @@ -923,7 +990,7 @@ assert_line_counts(covdata2, SUMMARY_1_2) assert_measured_files(covdata2, MEASURED_FILES_1_2) - def test_misfed_serialization(self): + def test_misfed_serialization(self) -> None: covdata = CoverageData(no_disk=True) bad_data = b'Hello, world!\x07 ' + b'z' * 100 msg = r"Unrecognized serialization: {} \(head of {} bytes\)".format( @@ -939,7 +1006,7 @@ run_in_temp_dir = False - def test_updating(self): + def test_updating(self) -> None: # https://github.com/nedbat/coveragepy/issues/1323 a = CoverageData(no_disk=True) a.add_lines({'foo.py': [10, 20, 30]}) diff -Nru python-coverage-6.5.0+dfsg1/tests/test_debug.py python-coverage-7.2.7+dfsg1/tests/test_debug.py --- python-coverage-6.5.0+dfsg1/tests/test_debug.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_debug.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,18 +3,25 @@ """Tests of coverage/debug.py""" +from __future__ import annotations + import ast import io import os import re import sys +from typing import Any, Callable, Iterable + import pytest import coverage from coverage import env -from coverage.debug import filter_text, info_formatter, info_header, short_id, short_stack -from coverage.debug import clipped_repr +from coverage.debug import ( + DebugOutputFile, + clipped_repr, filter_text, info_formatter, info_header, relevant_environment_display, + short_id, short_stack, +) from tests.coveragetest import CoverageTest from tests.helpers import re_line, re_lines, re_lines_text @@ -25,7 +32,7 @@ run_in_temp_dir = False - def test_info_formatter(self): + def test_info_formatter(self) -> None: lines = list(info_formatter([ ('x', 'hello there'), ('very long label', ['one element']), @@ -43,7 +50,7 @@ ] assert expected == lines - def test_info_formatter_with_generator(self): + def test_info_formatter_with_generator(self) -> None: lines = list(info_formatter(('info%d' % i, i) for i in range(3))) expected = [ ' info0: 0', @@ -52,7 +59,7 @@ ] assert expected == lines - def test_too_long_label(self): + def test_too_long_label(self) -> None: with pytest.raises(AssertionError): list(info_formatter([('this label is way too long and will not fit', 23)])) @@ -61,7 +68,7 @@ ("x", "-- x ---------------------------------------------------------"), ("hello there", "-- hello there -----------------------------------------------"), ]) -def test_info_header(label, header): +def test_info_header(label: str, header: str) -> None: assert info_header(label) == header @@ -71,7 +78,7 @@ (0xA5A55A5A, 0xFFFF), (0x1234cba956780fed, 0x8008), ]) -def test_short_id(id64, id16): +def test_short_id(id64: int, id16: int) -> None: assert short_id(id64) == id16 @@ -79,7 +86,7 @@ ("hello", 10, "'hello'"), ("0123456789abcdefghijklmnopqrstuvwxyz", 15, "'01234...vwxyz'"), ]) -def test_clipped_repr(text, numchars, result): +def test_clipped_repr(text: str, numchars: int, result: str) -> None: assert clipped_repr(text, numchars) == result @@ -90,14 +97,18 @@ ("hello\nbye\n", [lambda x: "="+x], "=hello\n=bye\n"), ("hello\nbye\n", [lambda x: "="+x, lambda x: x+"\ndone\n"], "=hello\ndone\n=bye\ndone\n"), ]) -def test_filter_text(text, filters, result): +def test_filter_text( + text: str, + filters: Iterable[Callable[[str], str]], + result: str, +) -> None: assert filter_text(text, filters) == result class DebugTraceTest(CoverageTest): """Tests of debug output.""" - def f1_debug_output(self, debug): + def f1_debug_output(self, debug: Iterable[str]) -> str: """Runs some code with `debug` option, returns the debug output.""" # Make code to run. self.make_file("f1.py", """\ @@ -116,13 +127,13 @@ return debug_out.getvalue() - def test_debug_no_trace(self): + def test_debug_no_trace(self) -> None: out_text = self.f1_debug_output([]) # We should have no output at all. assert not out_text - def test_debug_trace(self): + def test_debug_trace(self) -> None: out_text = self.f1_debug_output(["trace"]) # We should have a line like "Tracing 'f1.py'", perhaps with an @@ -132,7 +143,7 @@ # We should have lines like "Not tracing 'collector.py'..." assert re_lines(r"^Not tracing .*: is part of coverage.py$", out_text) - def test_debug_trace_pid(self): + def test_debug_trace_pid(self) -> None: out_text = self.f1_debug_output(["trace", "pid"]) # Now our lines are always prefixed with the process id. @@ -144,7 +155,7 @@ assert re_lines(pid_prefix + "Tracing ", out_text) assert re_lines(pid_prefix + "Not tracing ", out_text) - def test_debug_callers(self): + def test_debug_callers(self) -> None: out_text = self.f1_debug_output(["pid", "dataop", "dataio", "callers", "lock"]) # For every real message, there should be a stack trace with a line like # "f1_debug_output : /Users/ned/coverage/tests/test_debug.py @71" @@ -161,7 +172,7 @@ assert re_lines(r"^\s*\d+\.\w{4}: Adding file tracers: 0 files", real_messages[-1]) assert re_lines(r"\s+add_file_tracers : .*coverage[/\\]sqldata.py:\d+$", last_line) - def test_debug_config(self): + def test_debug_config(self) -> None: out_text = self.f1_debug_output(["config"]) labels = """ @@ -176,21 +187,11 @@ msg = f"Incorrect lines for {label!r}" assert 1 == len(re_lines(label_pat, out_text)), msg - def test_debug_sys(self): + def test_debug_sys(self) -> None: out_text = self.f1_debug_output(["sys"]) + assert_good_debug_sys(out_text) - labels = """ - coverage_version coverage_module coverage_paths stdlib_paths third_party_paths - tracer configs_attempted config_file configs_read data_file - python platform implementation executable - pid cwd path environment command_line cover_match pylib_match - """.split() - for label in labels: - label_pat = fr"^\s*{label}: " - msg = f"Incorrect lines for {label!r}" - assert 1 == len(re_lines(label_pat, out_text)), msg - - def test_debug_sys_ctracer(self): + def test_debug_sys_ctracer(self) -> None: out_text = self.f1_debug_output(["sys"]) tracer_line = re_line(r"CTracer:", out_text).strip() if env.C_TRACER: @@ -199,7 +200,7 @@ expected = "CTracer: unavailable" assert expected == tracer_line - def test_debug_pybehave(self): + def test_debug_pybehave(self) -> None: out_text = self.f1_debug_output(["pybehave"]) out_lines = out_text.splitlines() assert 10 < len(out_lines) < 40 @@ -208,15 +209,72 @@ assert vtuple[:5] == sys.version_info -def f_one(*args, **kwargs): +def assert_good_debug_sys(out_text: str) -> None: + """Assert that `str` is good output for debug=sys.""" + labels = """ + coverage_version coverage_module coverage_paths stdlib_paths third_party_paths + tracer configs_attempted config_file configs_read data_file + python platform implementation executable + pid cwd path environment command_line cover_match pylib_match + """.split() + for label in labels: + label_pat = fr"^\s*{label}: " + msg = f"Incorrect lines for {label!r}" + assert 1 == len(re_lines(label_pat, out_text)), msg + + +class DebugOutputTest(CoverageTest): + """Tests that we can direct debug output where we want.""" + + def setUp(self) -> None: + super().setUp() + # DebugOutputFile aggressively tries to start just one output file. We + # need to manually force it to make a new one. + DebugOutputFile._del_singleton_data() + + def debug_sys(self) -> None: + """Run just enough coverage to get full debug=sys output.""" + cov = coverage.Coverage(debug=["sys"]) + cov.start() + cov.stop() + + def test_stderr_default(self) -> None: + self.debug_sys() + out, err = self.stdouterr() + assert out == "" + assert_good_debug_sys(err) + + def test_envvar(self) -> None: + self.set_environ("COVERAGE_DEBUG_FILE", "debug.out") + self.debug_sys() + assert self.stdouterr() == ("", "") + with open("debug.out") as f: + assert_good_debug_sys(f.read()) + + def test_config_file(self) -> None: + self.make_file(".coveragerc", "[run]\ndebug_file = lotsa_info.txt") + self.debug_sys() + assert self.stdouterr() == ("", "") + with open("lotsa_info.txt") as f: + assert_good_debug_sys(f.read()) + + def test_stdout_alias(self) -> None: + self.set_environ("COVERAGE_DEBUG_FILE", "stdout") + self.debug_sys() + out, err = self.stdouterr() + assert err == "" + assert_good_debug_sys(out) + + +def f_one(*args: Any, **kwargs: Any) -> str: """First of the chain of functions for testing `short_stack`.""" return f_two(*args, **kwargs) -def f_two(*args, **kwargs): +def f_two(*args: Any, **kwargs: Any) -> str: """Second of the chain of functions for testing `short_stack`.""" return f_three(*args, **kwargs) -def f_three(*args, **kwargs): +def f_three(*args: Any, **kwargs: Any) -> str: """Third of the chain of functions for testing `short_stack`.""" return short_stack(*args, **kwargs) @@ -226,17 +284,36 @@ run_in_temp_dir = False - def test_short_stack(self): + def test_short_stack(self) -> None: stack = f_one().splitlines() assert len(stack) > 10 assert "f_three" in stack[-1] assert "f_two" in stack[-2] assert "f_one" in stack[-3] - def test_short_stack_limit(self): + def test_short_stack_limit(self) -> None: stack = f_one(limit=5).splitlines() assert len(stack) == 5 - def test_short_stack_skip(self): + def test_short_stack_skip(self) -> None: stack = f_one(skip=1).splitlines() assert "f_two" in stack[-1] + + +def test_relevant_environment_display() -> None: + env_vars = { + "HOME": "my home", + "HOME_DIR": "other place", + "XYZ_NEVER_MIND": "doesn't matter", + "SOME_PYOTHER": "xyz123", + "COVERAGE_THING": "abcd", + "MY_PYPI_TOKEN": "secret.something", + "TMP": "temporary", + } + assert relevant_environment_display(env_vars) == [ + ("COVERAGE_THING", "abcd"), + ("HOME", "my home"), + ("MY_PYPI_TOKEN", "******.*********"), + ("SOME_PYOTHER", "xyz123"), + ("TMP", "temporary"), + ] diff -Nru python-coverage-6.5.0+dfsg1/tests/test_execfile.py python-coverage-7.2.7+dfsg1/tests/test_execfile.py --- python-coverage-6.5.0+dfsg1/tests/test_execfile.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_execfile.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Tests for coverage.execfile""" +from __future__ import annotations + import compileall import json import os @@ -12,9 +14,10 @@ import re import sys +from typing import Any, Iterator + import pytest -from coverage import env from coverage.exceptions import NoCode, NoSource, _ExceptionDuringRun from coverage.execfile import run_python_file, run_python_module from coverage.files import python_reported_file @@ -28,12 +31,12 @@ """Test cases for `run_python_file`.""" @pytest.fixture(autouse=True) - def clean_up(self): + def clean_up(self) -> Iterator[None]: """These tests all run in-process. Clean up global changes.""" yield sys.excepthook = sys.__excepthook__ - def test_run_python_file(self): + def test_run_python_file(self) -> None: run_python_file([TRY_EXECFILE, "arg1", "arg2"]) mod_globs = json.loads(self.stdout()) @@ -59,7 +62,7 @@ # __builtins__ should have the right values, like open(). assert mod_globs['__builtins__.has_open'] is True - def test_no_extra_file(self): + def test_no_extra_file(self) -> None: # Make sure that running a file doesn't create an extra compiled file. self.make_file("xxx", """\ desc = "a non-.py file!" @@ -69,7 +72,7 @@ run_python_file(["xxx"]) assert os.listdir(".") == ["xxx"] - def test_universal_newlines(self): + def test_universal_newlines(self) -> None: # Make sure we can read any sort of line ending. pylines = """# try newlines|print('Hello, world!')|""".split('|') for nl in ('\n', '\r\n', '\r'): @@ -78,7 +81,7 @@ run_python_file(['nl.py']) assert self.stdout() == "Hello, world!\n"*3 - def test_missing_final_newline(self): + def test_missing_final_newline(self) -> None: # Make sure we can deal with a Python file with no final newline. self.make_file("abrupt.py", """\ if 1: @@ -91,25 +94,25 @@ run_python_file(["abrupt.py"]) assert self.stdout() == "a is 1\n" - def test_no_such_file(self): + def test_no_such_file(self) -> None: path = python_reported_file('xyzzy.py') msg = re.escape(f"No file to run: '{path}'") with pytest.raises(NoSource, match=msg): run_python_file(["xyzzy.py"]) - def test_directory_with_main(self): + def test_directory_with_main(self) -> None: self.make_file("with_main/__main__.py", """\ print("I am __main__") """) run_python_file(["with_main"]) assert self.stdout() == "I am __main__\n" - def test_directory_without_main(self): + def test_directory_without_main(self) -> None: self.make_file("without_main/__init__.py", "") with pytest.raises(NoSource, match="Can't find '__main__' module in 'without_main'"): run_python_file(["without_main"]) - def test_code_throws(self): + def test_code_throws(self) -> None: self.make_file("throw.py", """\ class MyException(Exception): pass @@ -130,7 +133,7 @@ assert self.stdout() == "about to raise..\n" assert self.stderr() == "" - def test_code_exits(self): + def test_code_exits(self) -> None: self.make_file("exit.py", """\ import sys def f1(): @@ -149,7 +152,7 @@ assert self.stdout() == "about to exit..\n" assert self.stderr() == "" - def test_excepthook_exit(self): + def test_excepthook_exit(self) -> None: self.make_file("excepthook_exit.py", """\ import sys @@ -166,7 +169,7 @@ cov_out = self.stdout() assert cov_out == "in excepthook\n" - def test_excepthook_throw(self): + def test_excepthook_throw(self) -> None: self.make_file("excepthook_throw.py", """\ import sys @@ -194,11 +197,8 @@ class RunPycFileTest(CoverageTest): """Test cases for `run_python_file`.""" - def make_pyc(self, **kwargs): + def make_pyc(self, **kwargs: Any) -> str: """Create a .pyc file, and return the path to it.""" - if env.JYTHON: - pytest.skip("Can't make .pyc files on Jython") - self.make_file("compiled.py", """\ def doit(): print("I am here!") @@ -211,12 +211,12 @@ # Find the .pyc file! return str(next(pathlib.Path(".").rglob("compiled*.pyc"))) - def test_running_pyc(self): + def test_running_pyc(self) -> None: pycfile = self.make_pyc() run_python_file([pycfile]) assert self.stdout() == "I am here!\n" - def test_running_pyo(self): + def test_running_pyo(self) -> None: pycfile = self.make_pyc() pyofile = re.sub(r"[.]pyc$", ".pyo", pycfile) assert pycfile != pyofile @@ -224,7 +224,7 @@ run_python_file([pyofile]) assert self.stdout() == "I am here!\n" - def test_running_pyc_from_wrong_python(self): + def test_running_pyc_from_wrong_python(self) -> None: pycfile = self.make_pyc() # Jam Python 2.1 magic number into the .pyc file. @@ -238,18 +238,18 @@ # In some environments, the pycfile persists and pollutes another test. os.remove(pycfile) - def test_running_hashed_pyc(self): + def test_running_hashed_pyc(self) -> None: pycfile = self.make_pyc(invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH) run_python_file([pycfile]) assert self.stdout() == "I am here!\n" - def test_no_such_pyc_file(self): + def test_no_such_pyc_file(self) -> None: path = python_reported_file('xyzzy.pyc') msg = re.escape(f"No file to run: '{path}'") with pytest.raises(NoCode, match=msg): run_python_file(["xyzzy.pyc"]) - def test_running_py_from_binary(self): + def test_running_py_from_binary(self) -> None: # Use make_file to get the bookkeeping. Ideally, it would # be able to write binary files. bf = self.make_file("binary") @@ -259,7 +259,7 @@ path = python_reported_file('binary') msg = ( re.escape(f"Couldn't run '{path}' as Python code: ") + - r"(TypeError|ValueError): source code string cannot contain null bytes" + r"(ValueError|SyntaxError): source code string cannot contain null bytes" ) with pytest.raises(Exception, match=msg): run_python_file([bf]) @@ -270,43 +270,43 @@ run_in_temp_dir = False - def test_runmod1(self): + def test_runmod1(self) -> None: run_python_module(["runmod1", "hello"]) out, err = self.stdouterr() assert out == "runmod1: passed hello\n" assert err == "" - def test_runmod2(self): + def test_runmod2(self) -> None: run_python_module(["pkg1.runmod2", "hello"]) out, err = self.stdouterr() assert out == "pkg1.__init__: pkg1\nrunmod2: passed hello\n" assert err == "" - def test_runmod3(self): + def test_runmod3(self) -> None: run_python_module(["pkg1.sub.runmod3", "hello"]) out, err = self.stdouterr() assert out == "pkg1.__init__: pkg1\nrunmod3: passed hello\n" assert err == "" - def test_pkg1_main(self): + def test_pkg1_main(self) -> None: run_python_module(["pkg1", "hello"]) out, err = self.stdouterr() assert out == "pkg1.__init__: pkg1\npkg1.__main__: passed hello\n" assert err == "" - def test_pkg1_sub_main(self): + def test_pkg1_sub_main(self) -> None: run_python_module(["pkg1.sub", "hello"]) out, err = self.stdouterr() assert out == "pkg1.__init__: pkg1\npkg1.sub.__main__: passed hello\n" assert err == "" - def test_pkg1_init(self): + def test_pkg1_init(self) -> None: run_python_module(["pkg1.__init__", "wut?"]) out, err = self.stdouterr() assert out == "pkg1.__init__: pkg1\npkg1.__init__: __main__\n" assert err == "" - def test_no_such_module(self): + def test_no_such_module(self) -> None: with pytest.raises(NoSource, match="No module named '?i_dont_exist'?"): run_python_module(["i_dont_exist"]) with pytest.raises(NoSource, match="No module named '?i'?"): @@ -314,6 +314,6 @@ with pytest.raises(NoSource, match="No module named '?i'?"): run_python_module(["i.dont.exist"]) - def test_no_main(self): + def test_no_main(self) -> None: with pytest.raises(NoSource): run_python_module(["pkg2", "hi"]) diff -Nru python-coverage-6.5.0+dfsg1/tests/test_filereporter.py python-coverage-7.2.7+dfsg1/tests/test_filereporter.py --- python-coverage-6.5.0+dfsg1/tests/test_filereporter.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_filereporter.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Tests for FileReporters""" +from __future__ import annotations + import sys from coverage.plugin import FileReporter @@ -20,7 +22,7 @@ run_in_temp_dir = False - def test_filenames(self): + def test_filenames(self) -> None: acu = PythonFileReporter("aa/afile.py") bcu = PythonFileReporter("aa/bb/bfile.py") ccu = PythonFileReporter("aa/bb/cc/cfile.py") @@ -31,7 +33,7 @@ assert bcu.source() == "# bfile.py\n" assert ccu.source() == "# cfile.py\n" - def test_odd_filenames(self): + def test_odd_filenames(self) -> None: acu = PythonFileReporter("aa/afile.odd.py") bcu = PythonFileReporter("aa/bb/bfile.odd.py") b2cu = PythonFileReporter("aa/bb.odd/bfile.py") @@ -42,7 +44,7 @@ assert bcu.source() == "# bfile.odd.py\n" assert b2cu.source() == "# bfile.py\n" - def test_modules(self): + def test_modules(self) -> None: import aa import aa.bb import aa.bb.cc @@ -57,7 +59,7 @@ assert bcu.source() == "# bb\n" assert ccu.source() == "" # yes, empty - def test_module_files(self): + def test_module_files(self) -> None: import aa.afile import aa.bb.bfile import aa.bb.cc.cfile @@ -72,7 +74,7 @@ assert bcu.source() == "# bfile.py\n" assert ccu.source() == "# cfile.py\n" - def test_comparison(self): + def test_comparison(self) -> None: acu = FileReporter("aa/afile.py") acu2 = FileReporter("aa/afile.py") zcu = FileReporter("aa/zfile.py") @@ -83,7 +85,7 @@ assert acu < bcu and acu <= bcu and acu != bcu assert bcu > acu and bcu >= acu and bcu != acu - def test_zipfile(self): + def test_zipfile(self) -> None: sys.path.append("tests/zip1.zip") # Test that we can get files out of zipfiles, and read their source files. diff -Nru python-coverage-6.5.0+dfsg1/tests/test_files.py python-coverage-7.2.7+dfsg1/tests/test_files.py --- python-coverage-6.5.0+dfsg1/tests/test_files.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_files.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,8 +3,14 @@ """Tests for files.py""" +from __future__ import annotations + +import itertools import os import os.path +import re + +from typing import Any, Iterable, Iterator, List from unittest import mock import pytest @@ -12,20 +18,23 @@ from coverage import env, files from coverage.exceptions import ConfigError from coverage.files import ( - FnmatchMatcher, ModuleMatcher, PathAliases, TreeMatcher, abs_file, - actual_path, find_python_files, flat_rootname, fnmatches_to_regex, + GlobMatcher, ModuleMatcher, PathAliases, TreeMatcher, abs_file, + actual_path, find_python_files, flat_rootname, globs_to_regex, ) +from coverage.types import Protocol + from tests.coveragetest import CoverageTest +from tests.helpers import os_sep class FilesTest(CoverageTest): """Tests of coverage.files.""" - def abs_path(self, p): + def abs_path(self, p: str) -> str: """Return the absolute path for `p`.""" return os.path.join(abs_file(os.getcwd()), os.path.normpath(p)) - def test_simple(self): + def test_simple(self) -> None: self.make_file("hello.py") files.set_relative_directory() assert files.relative_filename("hello.py") == "hello.py" @@ -33,7 +42,7 @@ assert a != "hello.py" assert files.relative_filename(a) == "hello.py" - def test_peer_directories(self): + def test_peer_directories(self) -> None: self.make_file("sub/proj1/file1.py") self.make_file("sub/proj2/file2.py") a1 = self.abs_path("sub/proj1/file1.py") @@ -44,7 +53,7 @@ assert files.relative_filename(a1) == "file1.py" assert files.relative_filename(a2) == a2 - def test_filepath_contains_absolute_prefix_twice(self): + def test_filepath_contains_absolute_prefix_twice(self) -> None: # https://github.com/nedbat/coveragepy/issues/194 # Build a path that has two pieces matching the absolute path prefix. # Technically, this test doesn't do that on Windows, but drive @@ -55,7 +64,7 @@ rel = os.path.join('sub', trick, 'file1.py') assert files.relative_filename(abs_file(rel)) == rel - def test_canonical_filename_ensure_cache_hit(self): + def test_canonical_filename_ensure_cache_hit(self) -> None: self.make_file("sub/proj1/file1.py") d = actual_path(self.abs_path("sub/proj1")) os.chdir(d) @@ -67,18 +76,36 @@ assert files.canonical_filename('sub/proj1/file1.py') == self.abs_path('file1.py') @pytest.mark.parametrize( - ["curdir", "sep"], [ + "curdir, sep", [ ("/", "/"), ("X:\\", "\\"), ] ) - def test_relative_dir_for_root(self, curdir, sep): + def test_relative_dir_for_root(self, curdir: str, sep: str) -> None: with mock.patch.object(files.os, 'curdir', new=curdir): with mock.patch.object(files.os, 'sep', new=sep): with mock.patch('coverage.files.os.path.normcase', return_value=curdir): files.set_relative_directory() assert files.relative_directory() == curdir + @pytest.mark.parametrize( + "to_make, to_check, answer", [ + ("a/b/c/foo.py", "a/b/c/foo.py", True), + ("a/b/c/foo.py", "a/b/c/bar.py", False), + ("src/files.zip", "src/files.zip/foo.py", True), + ("src/files.whl", "src/files.whl/foo.py", True), + ("src/files.egg", "src/files.egg/foo.py", True), + ("src/files.pex", "src/files.pex/foo.py", True), + ("src/files.zip", "src/morefiles.zip/foo.py", False), + ("src/files.pex", "src/files.pex/zipfiles/files.zip/foo.py", True), + ] + ) + def test_source_exists(self, to_make: str, to_check: str, answer: bool) -> None: + # source_exists won't look inside the zipfile, so it's fine to make + # an empty file with the zipfile name. + self.make_file(to_make, "") + assert files.source_exists(to_check) == answer + @pytest.mark.parametrize("original, flat", [ ("abc.py", "abc_py"), @@ -100,73 +127,179 @@ "d_e597dfacb73a23d5_my_program_py" ), ]) -def test_flat_rootname(original, flat): +def test_flat_rootname(original: str, flat: str) -> None: assert flat_rootname(original) == flat +def globs_to_regex_params( + patterns: Iterable[str], + case_insensitive: bool = False, + partial: bool = False, + matches: Iterable[str] = (), + nomatches: Iterable[str] = (), +) -> Iterator[Any]: + """Generate parameters for `test_globs_to_regex`. + + `patterns`, `case_insensitive`, and `partial` are arguments for + `globs_to_regex`. `matches` is a list of strings that should match, and + `nomatches` is a list of strings that should not match. + + Everything is yielded so that `test_globs_to_regex` can call + `globs_to_regex` once and check one result. + + """ + pat_id = "|".join(patterns) + for text in matches: + yield pytest.param( + patterns, case_insensitive, partial, text, True, + id=f"{pat_id}:ci{case_insensitive}:par{partial}:{text}:match", + ) + for text in nomatches: + yield pytest.param( + patterns, case_insensitive, partial, text, False, + id=f"{pat_id}:ci{case_insensitive}:par{partial}:{text}:nomatch", + ) + @pytest.mark.parametrize( - "patterns, case_insensitive, partial," + - "matches," + - "nomatches", -[ - ( - ["abc", "xyz"], False, False, + "patterns, case_insensitive, partial, text, result", + list(itertools.chain.from_iterable([ + globs_to_regex_params( ["abc", "xyz"], - ["ABC", "xYz", "abcx", "xabc", "axyz", "xyza"], - ), - ( - ["abc", "xyz"], True, False, - ["abc", "xyz", "Abc", "XYZ", "AbC"], - ["abcx", "xabc", "axyz", "xyza"], - ), - ( - ["abc/hi.py"], True, False, - ["abc/hi.py", "ABC/hi.py", r"ABC\hi.py"], - ["abc_hi.py", "abc/hi.pyc"], - ), - ( - [r"abc\hi.py"], True, False, - [r"abc\hi.py", r"ABC\hi.py"], - ["abc/hi.py", "ABC/hi.py", "abc_hi.py", "abc/hi.pyc"], - ), - ( - ["abc/*/hi.py"], True, False, - ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"], - ["abc/hi.py", "abc/hi.pyc"], - ), - ( - ["abc/[a-f]*/hi.py"], True, False, - ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"], - ["abc/zoo/hi.py", "abc/hi.py", "abc/hi.pyc"], - ), - ( - ["abc/"], True, True, - ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"], - ["abcd/foo.py", "xabc/hi.py"], - ), + matches=["abc", "xyz", "sub/mod/abc"], + nomatches=[ + "ABC", "xYz", "abcx", "xabc", "axyz", "xyza", "sub/mod/abcd", "sub/abc/more", + ], + ), + globs_to_regex_params( + ["abc", "xyz"], case_insensitive=True, + matches=["abc", "xyz", "Abc", "XYZ", "AbC"], + nomatches=["abcx", "xabc", "axyz", "xyza"], + ), + globs_to_regex_params( + ["a*c", "x*z"], + matches=["abc", "xyz", "xYz", "azc", "xaz", "axyzc"], + nomatches=["ABC", "abcx", "xabc", "axyz", "xyza", "a/c"], + ), + globs_to_regex_params( + ["a?c", "x?z"], + matches=["abc", "xyz", "xYz", "azc", "xaz"], + nomatches=["ABC", "abcx", "xabc", "axyz", "xyza", "a/c"], + ), + globs_to_regex_params( + ["a??d"], + matches=["abcd", "azcd", "a12d"], + nomatches=["ABCD", "abcx", "axyz", "abcde"], + ), + globs_to_regex_params( + ["abc/hi.py"], case_insensitive=True, + matches=["abc/hi.py", "ABC/hi.py", r"ABC\hi.py"], + nomatches=["abc_hi.py", "abc/hi.pyc"], + ), + globs_to_regex_params( + [r"abc\hi.py"], case_insensitive=True, + matches=[r"abc\hi.py", r"ABC\hi.py", "abc/hi.py", "ABC/hi.py"], + nomatches=["abc_hi.py", "abc/hi.pyc"], + ), + globs_to_regex_params( + ["abc/*/hi.py"], case_insensitive=True, + matches=["abc/foo/hi.py", r"ABC\foo/hi.py"], + nomatches=["abc/hi.py", "abc/hi.pyc", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"], + ), + globs_to_regex_params( + ["abc/**/hi.py"], case_insensitive=True, + matches=[ + "abc/foo/hi.py", r"ABC\foo/hi.py", "abc/hi.py", "ABC/foo/bar/hi.py", + r"ABC\foo/bar/hi.py", + ], + nomatches=["abc/hi.pyc"], + ), + globs_to_regex_params( + ["abc/[a-f]*/hi.py"], case_insensitive=True, + matches=["abc/foo/hi.py", r"ABC\boo/hi.py"], + nomatches=[ + "abc/zoo/hi.py", "abc/hi.py", "abc/hi.pyc", "abc/foo/bar/hi.py", + r"abc\foo/bar/hi.py", + ], + ), + globs_to_regex_params( + ["abc/[a-f]/hi.py"], case_insensitive=True, + matches=["abc/f/hi.py", r"ABC\b/hi.py"], + nomatches=[ + "abc/foo/hi.py", "abc/zoo/hi.py", "abc/hi.py", "abc/hi.pyc", "abc/foo/bar/hi.py", + r"abc\foo/bar/hi.py", + ], + ), + globs_to_regex_params( + ["abc/"], case_insensitive=True, partial=True, + matches=["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"], + nomatches=["abcd/foo.py", "xabc/hi.py"], + ), + globs_to_regex_params( + ["*/foo"], case_insensitive=False, partial=True, + matches=["abc/foo/hi.py", "foo/hi.py"], + nomatches=["abc/xfoo/hi.py"], + ), + globs_to_regex_params( + ["**/foo"], + matches=["foo", "hello/foo", "hi/there/foo"], + nomatches=["foob", "hello/foob", "hello/Foo"], + ), + globs_to_regex_params( + ["a+b/foo*", "x{y}z/foo*"], + matches=["a+b/foo", "a+b/foobar", "x{y}z/foobar"], + nomatches=["aab/foo", "ab/foo", "xyz/foo"], + ), + ])) +) +def test_globs_to_regex( + patterns: Iterable[str], + case_insensitive: bool, + partial: bool, + text: str, + result: bool, +) -> None: + regex = globs_to_regex(patterns, case_insensitive=case_insensitive, partial=partial) + assert bool(regex.match(text)) == result + + +@pytest.mark.parametrize("pattern, bad_word", [ + ("***/foo.py", "***"), + ("bar/***/foo.py", "***"), + ("*****/foo.py", "*****"), + ("Hello]there", "]"), + ("Hello[there", "["), + ("x/a**/b.py", "a**"), + ("x/abcd**/b.py", "abcd**"), + ("x/**a/b.py", "**a"), + ("x/**/**/b.py", "**/**"), ]) -def test_fnmatches_to_regex(patterns, case_insensitive, partial, matches, nomatches): - regex = fnmatches_to_regex(patterns, case_insensitive=case_insensitive, partial=partial) - for s in matches: - assert regex.match(s) - for s in nomatches: - assert not regex.match(s) +def test_invalid_globs(pattern: str, bad_word: str) -> None: + msg = f"File pattern can't include {bad_word!r}" + with pytest.raises(ConfigError, match=re.escape(msg)): + globs_to_regex([pattern]) + + +class TMatcher(Protocol): + """The shape all Matchers have.""" + + def match(self, s: str) -> bool: + """Does this string match?""" class MatcherTest(CoverageTest): """Tests of file matchers.""" - def setUp(self): + def setUp(self) -> None: super().setUp() files.set_relative_directory() - def assertMatches(self, matcher, filepath, matches): + def assertMatches(self, matcher: TMatcher, filepath: str, matches: bool) -> None: """The `matcher` should agree with `matches` about `filepath`.""" canonical = files.canonical_filename(filepath) msg = f"File {filepath} should have matched as {matches}" assert matches == matcher.match(canonical), msg - def test_tree_matcher(self): + def test_tree_matcher(self) -> None: case_folding = env.WINDOWS matches_to_try = [ (self.make_file("sub/file1.py"), True), @@ -188,7 +321,7 @@ for filepath, matches in matches_to_try: self.assertMatches(tm, filepath, matches) - def test_module_matcher(self): + def test_module_matcher(self) -> None: matches_to_try = [ ('test', True), ('trash', False), @@ -211,7 +344,7 @@ for modulename, matches in matches_to_try: assert mm.match(modulename) == matches, modulename - def test_fnmatch_matcher(self): + def test_glob_matcher(self) -> None: matches_to_try = [ (self.make_file("sub/file1.py"), True), (self.make_file("sub/file2.c"), False), @@ -219,30 +352,32 @@ (self.make_file("sub3/file4.py"), True), (self.make_file("sub3/file5.c"), False), ] - fnm = FnmatchMatcher(["*.py", "*/sub2/*"]) + fnm = GlobMatcher(["*.py", "*/sub2/*"]) assert fnm.info() == ["*.py", "*/sub2/*"] for filepath, matches in matches_to_try: self.assertMatches(fnm, filepath, matches) - def test_fnmatch_matcher_overload(self): - fnm = FnmatchMatcher(["*x%03d*.txt" % i for i in range(500)]) + def test_glob_matcher_overload(self) -> None: + fnm = GlobMatcher(["*x%03d*.txt" % i for i in range(500)]) self.assertMatches(fnm, "x007foo.txt", True) self.assertMatches(fnm, "x123foo.txt", True) self.assertMatches(fnm, "x798bar.txt", False) + self.assertMatches(fnm, "x499.txt", True) + self.assertMatches(fnm, "x500.txt", False) - def test_fnmatch_windows_paths(self): + def test_glob_windows_paths(self) -> None: # We should be able to match Windows paths even if we are running on # a non-Windows OS. - fnm = FnmatchMatcher(["*/foo.py"]) + fnm = GlobMatcher(["*/foo.py"]) self.assertMatches(fnm, r"dir\foo.py", True) - fnm = FnmatchMatcher([r"*\foo.py"]) + fnm = GlobMatcher([r"*\foo.py"]) self.assertMatches(fnm, r"dir\foo.py", True) @pytest.fixture(params=[False, True], name="rel_yn") -def relative_setting(request): +def relative_setting(request: pytest.FixtureRequest) -> bool: """Parameterized fixture to choose whether PathAliases is relative or not.""" - return request.param + return request.param # type: ignore[no-any-return] class PathAliasesTest(CoverageTest): @@ -250,59 +385,83 @@ run_in_temp_dir = False - def assert_mapped(self, aliases, inp, out, relative=False): + def assert_mapped(self, aliases: PathAliases, inp: str, out: str) -> None: """Assert that `inp` mapped through `aliases` produces `out`. - `out` is canonicalized first, since aliases produce canonicalized - paths by default. + If the aliases are not relative, then `out` is canonicalized first, + since aliases produce canonicalized paths by default. """ - mapped = aliases.map(inp) - expected = files.canonical_filename(out) if not relative else out + mapped = aliases.map(inp, exists=lambda p: True) + if aliases.relative: + expected = out + else: + expected = files.canonical_filename(out) assert mapped == expected - def assert_unchanged(self, aliases, inp): + def assert_unchanged(self, aliases: PathAliases, inp: str, exists: bool = True) -> None: """Assert that `inp` mapped through `aliases` is unchanged.""" - assert aliases.map(inp) == inp + assert aliases.map(inp, exists=lambda p: exists) == inp - def test_noop(self, rel_yn): + def test_noop(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) self.assert_unchanged(aliases, '/ned/home/a.py') - def test_nomatch(self, rel_yn): + def test_nomatch(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) aliases.add('/home/*/src', './mysrc') self.assert_unchanged(aliases, '/home/foo/a.py') - def test_wildcard(self, rel_yn): + def test_wildcard(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) aliases.add('/ned/home/*/src', './mysrc') - self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py', relative=rel_yn) + self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py') aliases = PathAliases(relative=rel_yn) aliases.add('/ned/home/*/src/', './mysrc') - self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py', relative=rel_yn) + self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py') - def test_no_accidental_match(self, rel_yn): + def test_no_accidental_match(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) aliases.add('/home/*/src', './mysrc') self.assert_unchanged(aliases, '/home/foo/srcetc') - def test_multiple_patterns(self, rel_yn): + def test_no_map_if_not_exist(self, rel_yn: bool) -> None: + aliases = PathAliases(relative=rel_yn) + aliases.add('/ned/home/*/src', './mysrc') + self.assert_unchanged(aliases, '/ned/home/foo/src/a.py', exists=False) + self.assert_unchanged(aliases, 'foo/src/a.py', exists=False) + + def test_no_dotslash(self, rel_yn: bool) -> None: + # The result shouldn't start with "./" if the map result didn't. + aliases = PathAliases(relative=rel_yn) + aliases.add('*/project', '.') + self.assert_mapped(aliases, '/ned/home/project/src/a.py', os_sep('src/a.py')) + + def test_relative_pattern(self) -> None: + aliases = PathAliases(relative=True) + aliases.add(".tox/*/site-packages", "src") + self.assert_mapped( + aliases, + ".tox/py314/site-packages/proj/a.py", + os_sep("src/proj/a.py"), + ) + + def test_multiple_patterns(self, rel_yn: bool) -> None: # also test the debugfn... - msgs = [] + msgs: List[str] = [] aliases = PathAliases(debugfn=msgs.append, relative=rel_yn) aliases.add('/home/*/src', './mysrc') aliases.add('/lib/*/libsrc', './mylib') - self.assert_mapped(aliases, '/home/foo/src/a.py', './mysrc/a.py', relative=rel_yn) - self.assert_mapped(aliases, '/lib/foo/libsrc/a.py', './mylib/a.py', relative=rel_yn) + self.assert_mapped(aliases, '/home/foo/src/a.py', './mysrc/a.py') + self.assert_mapped(aliases, '/lib/foo/libsrc/a.py', './mylib/a.py') if rel_yn: assert msgs == [ "Aliases (relative=True):", " Rule: '/home/*/src' -> './mysrc/' using regex " + - "'(?:(?s:[\\\\\\\\/]home[\\\\\\\\/].*[\\\\\\\\/]src[\\\\\\\\/]))'", + "'[/\\\\\\\\]home[/\\\\\\\\][^/\\\\\\\\]*[/\\\\\\\\]src[/\\\\\\\\]'", " Rule: '/lib/*/libsrc' -> './mylib/' using regex " + - "'(?:(?s:[\\\\\\\\/]lib[\\\\\\\\/].*[\\\\\\\\/]libsrc[\\\\\\\\/]))'", + "'[/\\\\\\\\]lib[/\\\\\\\\][^/\\\\\\\\]*[/\\\\\\\\]libsrc[/\\\\\\\\]'", "Matched path '/home/foo/src/a.py' to rule '/home/*/src' -> './mysrc/', " + "producing './mysrc/a.py'", "Matched path '/lib/foo/libsrc/a.py' to rule '/lib/*/libsrc' -> './mylib/', " + @@ -312,9 +471,9 @@ assert msgs == [ "Aliases (relative=False):", " Rule: '/home/*/src' -> './mysrc/' using regex " + - "'(?:(?s:[\\\\\\\\/]home[\\\\\\\\/].*[\\\\\\\\/]src[\\\\\\\\/]))'", + "'[/\\\\\\\\]home[/\\\\\\\\][^/\\\\\\\\]*[/\\\\\\\\]src[/\\\\\\\\]'", " Rule: '/lib/*/libsrc' -> './mylib/' using regex " + - "'(?:(?s:[\\\\\\\\/]lib[\\\\\\\\/].*[\\\\\\\\/]libsrc[\\\\\\\\/]))'", + "'[/\\\\\\\\]lib[/\\\\\\\\][^/\\\\\\\\]*[/\\\\\\\\]libsrc[/\\\\\\\\]'", "Matched path '/home/foo/src/a.py' to rule '/home/*/src' -> './mysrc/', " + f"producing {files.canonical_filename('./mysrc/a.py')!r}", "Matched path '/lib/foo/libsrc/a.py' to rule '/lib/*/libsrc' -> './mylib/', " + @@ -326,24 +485,24 @@ "/ned/home/*/", "/ned/home/*/*/", ]) - def test_cant_have_wildcard_at_end(self, badpat): + def test_cant_have_wildcard_at_end(self, badpat: str) -> None: aliases = PathAliases() msg = "Pattern must not end with wildcards." with pytest.raises(ConfigError, match=msg): aliases.add(badpat, "fooey") - def test_no_accidental_munging(self): + def test_no_accidental_munging(self) -> None: aliases = PathAliases() aliases.add(r'c:\Zoo\boo', 'src/') aliases.add('/home/ned$', 'src/') self.assert_mapped(aliases, r'c:\Zoo\boo\foo.py', 'src/foo.py') self.assert_mapped(aliases, r'/home/ned$/foo.py', 'src/foo.py') - def test_paths_are_os_corrected(self, rel_yn): + def test_paths_are_os_corrected(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) aliases.add('/home/ned/*/src', './mysrc') aliases.add(r'c:\ned\src', './mysrc') - self.assert_mapped(aliases, r'C:\Ned\src\sub\a.py', './mysrc/sub/a.py', relative=rel_yn) + self.assert_mapped(aliases, r'C:\Ned\src\sub\a.py', './mysrc/sub/a.py') aliases = PathAliases(relative=rel_yn) aliases.add('/home/ned/*/src', r'.\mysrc') @@ -352,83 +511,117 @@ aliases, r'/home/ned/foo/src/sub/a.py', r'.\mysrc\sub\a.py', - relative=rel_yn, ) - def test_windows_on_linux(self, rel_yn): - # https://github.com/nedbat/coveragepy/issues/618 - lin = "*/project/module/" - win = "*\\project\\module\\" + # Try the paths in both orders. + lin = "*/project/module/" + win = "*\\project\\module\\" + lin_win_paths = [[lin, win], [win, lin]] - # Try the paths in both orders. - for paths in [[lin, win], [win, lin]]: - aliases = PathAliases(relative=rel_yn) - for path in paths: - aliases.add(path, "project/module") - self.assert_mapped( - aliases, - "C:\\a\\path\\somewhere\\coveragepy_test\\project\\module\\tests\\file.py", - "project/module/tests/file.py", - relative=rel_yn, - ) + @pytest.mark.parametrize("paths", lin_win_paths) + def test_windows_on_linux(self, paths: Iterable[str], rel_yn: bool) -> None: + # https://github.com/nedbat/coveragepy/issues/618 + aliases = PathAliases(relative=rel_yn) + for path in paths: + aliases.add(path, "project/module") + self.assert_mapped( + aliases, + "C:\\a\\path\\somewhere\\coveragepy_test\\project\\module\\tests\\file.py", + "project/module/tests/file.py", + ) - def test_linux_on_windows(self, rel_yn): + @pytest.mark.parametrize("paths", lin_win_paths) + def test_linux_on_windows(self, paths: Iterable[str], rel_yn: bool) -> None: # https://github.com/nedbat/coveragepy/issues/618 - lin = "*/project/module/" - win = "*\\project\\module\\" + aliases = PathAliases(relative=rel_yn) + for path in paths: + aliases.add(path, "project\\module") + self.assert_mapped( + aliases, + "C:/a/path/somewhere/coveragepy_test/project/module/tests/file.py", + "project\\module\\tests\\file.py", + ) - # Try the paths in both orders. - for paths in [[lin, win], [win, lin]]: - aliases = PathAliases(relative=rel_yn) - for path in paths: - aliases.add(path, "project\\module") - self.assert_mapped( - aliases, - "C:/a/path/somewhere/coveragepy_test/project/module/tests/file.py", - "project\\module\\tests\\file.py", - relative=rel_yn, - ) + @pytest.mark.parametrize("paths", lin_win_paths) + def test_relative_windows_on_linux(self, paths: Iterable[str]) -> None: + # https://github.com/nedbat/coveragepy/issues/991 + aliases = PathAliases(relative=True) + for path in paths: + aliases.add(path, "project/module") + self.assert_mapped( + aliases, + r"project\module\tests\file.py", + r"project/module/tests/file.py", + ) - def test_multiple_wildcard(self, rel_yn): + @pytest.mark.parametrize("paths", lin_win_paths) + def test_relative_linux_on_windows(self, paths: Iterable[str]) -> None: + # https://github.com/nedbat/coveragepy/issues/991 + aliases = PathAliases(relative=True) + for path in paths: + aliases.add(path, r"project\module") + self.assert_mapped( + aliases, + r"project/module/tests/file.py", + r"project\module\tests\file.py", + ) + + @pytest.mark.skipif(env.WINDOWS, reason="This test assumes Unix file system") + def test_implicit_relative_windows_on_linux(self) -> None: + # https://github.com/nedbat/coveragepy/issues/991 + aliases = PathAliases(relative=True) + self.assert_mapped( + aliases, + r"project\module\tests\file.py", + r"project/module/tests/file.py", + ) + + @pytest.mark.skipif(not env.WINDOWS, reason="This test assumes Windows file system") + def test_implicit_relative_linux_on_windows(self) -> None: + # https://github.com/nedbat/coveragepy/issues/991 + aliases = PathAliases(relative=True) + self.assert_mapped( + aliases, + r"project/module/tests/file.py", + r"project\module\tests\file.py", + ) + + def test_multiple_wildcard(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) aliases.add('/home/jenkins/*/a/*/b/*/django', './django') self.assert_mapped( aliases, '/home/jenkins/xx/a/yy/b/zz/django/foo/bar.py', './django/foo/bar.py', - relative=rel_yn, ) - def test_windows_root_paths(self, rel_yn): + def test_windows_root_paths(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) aliases.add('X:\\', '/tmp/src') self.assert_mapped( aliases, "X:\\a\\file.py", "/tmp/src/a/file.py", - relative=rel_yn, ) self.assert_mapped( aliases, "X:\\file.py", "/tmp/src/file.py", - relative=rel_yn, ) - def test_leading_wildcard(self, rel_yn): + def test_leading_wildcard(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) aliases.add('*/d1', './mysrc1') aliases.add('*/d2', './mysrc2') - self.assert_mapped(aliases, '/foo/bar/d1/x.py', './mysrc1/x.py', relative=rel_yn) - self.assert_mapped(aliases, '/foo/bar/d2/y.py', './mysrc2/y.py', relative=rel_yn) + self.assert_mapped(aliases, '/foo/bar/d1/x.py', './mysrc1/x.py') + self.assert_mapped(aliases, '/foo/bar/d2/y.py', './mysrc2/y.py') - # The root test case was added for the manylinux Docker images, - # and I'm not sure how it should work on Windows, so skip it. - cases = [".", "..", "../other"] - if not env.WINDOWS: - cases += ["/"] - @pytest.mark.parametrize("dirname", cases) - def test_dot(self, dirname): + @pytest.mark.parametrize("dirname", [".", "..", "../other", "/"]) + def test_dot(self, dirname: str) -> None: + if env.WINDOWS and dirname == "/": + # The root test case was added for the manylinux Docker images, + # and I'm not sure how it should work on Windows, so skip it. + pytest.skip("Don't know how to handle root on Windows") aliases = PathAliases() aliases.add(dirname, '/the/source') the_file = os.path.join(dirname, 'a.py') @@ -439,10 +632,23 @@ self.assert_mapped(aliases, the_file, '/the/source/a.py') +class PathAliasesRealFilesTest(CoverageTest): + """Tests for coverage/files.py:PathAliases using real files.""" + + def test_aliasing_zip_files(self) -> None: + self.make_file("src/zipfiles/code.zip", "fake zip, doesn't matter") + aliases = PathAliases() + aliases.add("*/d1", "./src") + aliases.add("*/d2", "./src") + + expected = files.canonical_filename("src/zipfiles/code.zip/p1.py") + assert aliases.map("tox/d1/zipfiles/code.zip/p1.py") == expected + + class FindPythonFilesTest(CoverageTest): """Tests of `find_python_files`.""" - def test_find_python_files(self): + def test_find_python_files(self) -> None: self.make_file("sub/a.py") self.make_file("sub/b.py") self.make_file("sub/x.c") # nope: not .py @@ -451,10 +657,27 @@ self.make_file("sub/ssub/~s.py") # nope: editor effluvia self.make_file("sub/lab/exp.py") # nope: no __init__.py self.make_file("sub/windows.pyw") - py_files = set(find_python_files("sub")) + py_files = set(find_python_files("sub", include_namespace_packages=False)) + self.assert_same_files(py_files, [ + "sub/a.py", "sub/b.py", + "sub/ssub/__init__.py", "sub/ssub/s.py", + "sub/windows.pyw", + ]) + + def test_find_python_files_include_namespace_packages(self) -> None: + self.make_file("sub/a.py") + self.make_file("sub/b.py") + self.make_file("sub/x.c") # nope: not .py + self.make_file("sub/ssub/__init__.py") + self.make_file("sub/ssub/s.py") + self.make_file("sub/ssub/~s.py") # nope: editor effluvia + self.make_file("sub/lab/exp.py") + self.make_file("sub/windows.pyw") + py_files = set(find_python_files("sub", include_namespace_packages=True)) self.assert_same_files(py_files, [ "sub/a.py", "sub/b.py", "sub/ssub/__init__.py", "sub/ssub/s.py", + "sub/lab/exp.py", "sub/windows.pyw", ]) @@ -465,5 +688,5 @@ run_in_temp_dir = False - def test_actual_path(self): + def test_actual_path(self) -> None: assert actual_path(r'c:\Windows') == actual_path(r'C:\wINDOWS') diff -Nru python-coverage-6.5.0+dfsg1/tests/test_goldtest.py python-coverage-7.2.7+dfsg1/tests/test_goldtest.py --- python-coverage-6.5.0+dfsg1/tests/test_goldtest.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_goldtest.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Tests of the helpers in goldtest.py""" +from __future__ import annotations + import os.path import re @@ -11,7 +13,7 @@ from tests.coveragetest import CoverageTest, TESTS_DIR from tests.goldtest import compare, gold_path from tests.goldtest import contains, contains_any, contains_rx, doesnt_contain -from tests.helpers import re_line, remove_tree +from tests.helpers import os_sep, re_line, remove_tree GOOD_GETTY = """\ Four score and seven years ago our fathers brought forth upon this continent, a @@ -33,7 +35,7 @@ (r'G\w+', 'Gxxx'), ] -def path_regex(path): +def path_regex(path: str) -> str: """Convert a file path into a regex that will match that path on any OS.""" return re.sub(r"[/\\]", r"[/\\\\]", path.replace(".", "[.]")) @@ -48,16 +50,16 @@ class CompareTest(CoverageTest): """Tests of goldtest.py:compare()""" - def setUp(self): + def setUp(self) -> None: super().setUp() self.addCleanup(remove_tree, ACTUAL_DIR) - def test_good(self): + def test_good(self) -> None: self.make_file("out/gettysburg.txt", GOOD_GETTY) compare(gold_path("testing/getty"), "out", scrubs=SCRUBS) self.assert_doesnt_exist(ACTUAL_GETTY_FILE) - def test_bad(self): + def test_bad(self) -> None: self.make_file("out/gettysburg.txt", BAD_GETTY) # compare() raises an assertion. @@ -71,6 +73,10 @@ assert "+ Five score" in stdout assert re_line(rf"^:::: diff '.*{GOLD_PATH_RX}' and '{OUT_PATH_RX}'", stdout) assert re_line(rf"^:::: end diff '.*{GOLD_PATH_RX}' and '{OUT_PATH_RX}'", stdout) + assert ( + os_sep(f"Saved actual output to '{ACTUAL_GETTY_FILE}': see tests/gold/README.rst") + in os_sep(stdout) + ) assert " D/D/D, Gxxx, Pennsylvania" in stdout # The actual file was saved. @@ -78,7 +84,7 @@ saved = f.read() assert saved == BAD_GETTY - def test_good_needs_scrubs(self): + def test_good_needs_scrubs(self) -> None: # Comparing the "good" result without scrubbing the variable parts will fail. self.make_file("out/gettysburg.txt", GOOD_GETTY) @@ -91,7 +97,7 @@ assert "- 11/19/1863, Gettysburg, Pennsylvania" in stdout assert "+ 11/19/9999, Gettysburg, Pennsylvania" in stdout - def test_actual_extra(self): + def test_actual_extra(self) -> None: self.make_file("out/gettysburg.txt", GOOD_GETTY) self.make_file("out/another.more", "hi") @@ -107,7 +113,7 @@ # But only the files matching the file_pattern are considered. compare(gold_path("testing/getty"), "out", file_pattern="*.txt", scrubs=SCRUBS) - def test_xml_good(self): + def test_xml_good(self) -> None: self.make_file("out/output.xml", """\ @@ -118,7 +124,7 @@ """) compare(gold_path("testing/xml"), "out", scrubs=SCRUBS) - def test_xml_bad(self): + def test_xml_bad(self) -> None: self.make_file("out/output.xml", """\ @@ -147,25 +153,25 @@ run_in_temp_dir = False - def test_contains(self): + def test_contains(self) -> None: contains(GOLD_GETTY_FILE, "Four", "fathers", "dedicated") msg = rf"Missing content in {GOLD_GETTY_FILE_RX}: 'xyzzy'" with pytest.raises(AssertionError, match=msg): contains(GOLD_GETTY_FILE, "Four", "fathers", "xyzzy", "dedicated") - def test_contains_rx(self): + def test_contains_rx(self) -> None: contains_rx(GOLD_GETTY_FILE, r"Fo.r", r"f[abc]thers", "dedi[cdef]ated") msg = rf"Missing regex in {GOLD_GETTY_FILE_RX}: r'm\[opq\]thers'" with pytest.raises(AssertionError, match=msg): contains_rx(GOLD_GETTY_FILE, r"Fo.r", r"m[opq]thers") - def test_contains_any(self): + def test_contains_any(self) -> None: contains_any(GOLD_GETTY_FILE, "Five", "Four", "Three") msg = rf"Missing content in {GOLD_GETTY_FILE_RX}: 'One' \[1 of 3\]" with pytest.raises(AssertionError, match=msg): contains_any(GOLD_GETTY_FILE, "One", "Two", "Three") - def test_doesnt_contain(self): + def test_doesnt_contain(self) -> None: doesnt_contain(GOLD_GETTY_FILE, "One", "Two", "Three") msg = rf"Forbidden content in {GOLD_GETTY_FILE_RX}: 'Four'" with pytest.raises(AssertionError, match=msg): diff -Nru python-coverage-6.5.0+dfsg1/tests/test_html.py python-coverage-7.2.7+dfsg1/tests/test_html.py --- python-coverage-6.5.0+dfsg1/tests/test_html.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_html.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Tests that HTML generation is awesome.""" +from __future__ import annotations + import collections import datetime import glob @@ -13,14 +15,17 @@ import sys from unittest import mock +from typing import Any, Dict, IO, List, Optional, Set, Tuple + import pytest import coverage -from coverage import env +from coverage import env, Coverage from coverage.exceptions import NoDataError, NotPython, NoSource from coverage.files import abs_file, flat_rootname import coverage.html -from coverage.report import get_analysis_to_report +from coverage.report_core import get_analysis_to_report +from coverage.types import TLineNo, TMorf from tests.coveragetest import CoverageTest, TESTS_DIR from tests.goldtest import gold_path @@ -31,7 +36,7 @@ class HtmlTestHelpers(CoverageTest): """Methods that help with HTML tests.""" - def create_initial_files(self): + def create_initial_files(self) -> None: """Create the source files we need to run these tests.""" self.make_file("main_file.py", """\ import helper1, helper2 @@ -48,7 +53,11 @@ print("x is %d" % x) """) - def run_coverage(self, covargs=None, htmlargs=None): + def run_coverage( + self, + covargs: Optional[Dict[str, Any]] = None, + htmlargs: Optional[Dict[str, Any]] = None, + ) -> float: """Run coverage.py on main_file.py, and create an HTML report.""" self.clean_local_file_imports() cov = coverage.Coverage(**(covargs or {})) @@ -57,17 +66,17 @@ self.assert_valid_hrefs() return ret - def get_html_report_content(self, module): + def get_html_report_content(self, module: str) -> str: """Return the content of the HTML report for `module`.""" filename = flat_rootname(module) + ".html" filename = os.path.join("htmlcov", filename) with open(filename) as f: return f.read() - def get_html_index_content(self): + def get_html_index_content(self) -> str: """Return the content of index.html. - Timestamps are replaced with a placeholder so that clocks don't matter. + Time stamps are replaced with a placeholder so that clocks don't matter. """ with open("htmlcov/index.html") as f: @@ -84,21 +93,21 @@ ) return index - def assert_correct_timestamp(self, html): - """Extract the timestamp from `html`, and assert it is recent.""" + def assert_correct_timestamp(self, html: str) -> None: + """Extract the time stamp from `html`, and assert it is recent.""" timestamp_pat = r"created at (\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})" m = re.search(timestamp_pat, html) - assert m, "Didn't find a timestamp!" - timestamp = datetime.datetime(*map(int, m.groups())) - # The timestamp only records the minute, so the delta could be from + assert m, "Didn't find a time stamp!" + timestamp = datetime.datetime(*[int(v) for v in m.groups()]) # type: ignore[arg-type] + # The time stamp only records the minute, so the delta could be from # 12:00 to 12:01:59, or two minutes. self.assert_recent_datetime( timestamp, seconds=120, - msg=f"Timestamp is wrong: {timestamp}", + msg=f"Time stamp is wrong: {timestamp}", ) - def assert_valid_hrefs(self): + def assert_valid_hrefs(self) -> None: """Assert that the hrefs in htmlcov/*.html to see the references are valid. Doesn't check external links (those with a protocol). @@ -124,10 +133,10 @@ class FileWriteTracker: """A fake object to track how `open` is used to write files.""" - def __init__(self, written): + def __init__(self, written: Set[str]) -> None: self.written = written - def open(self, filename, mode="r"): + def open(self, filename: str, mode: str = "r") -> IO[str]: """Be just like `open`, but write written file names to `self.written`.""" if mode.startswith("w"): self.written.add(filename.replace('\\', '/')) @@ -137,7 +146,7 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): """Tests of the HTML delta speed-ups.""" - def setUp(self): + def setUp(self) -> None: super().setUp() # At least one of our tests monkey-patches the version of coverage.py, @@ -145,9 +154,13 @@ self.real_coverage_version = coverage.__version__ self.addCleanup(setattr, coverage, "__version__", self.real_coverage_version) - self.files_written = None + self.files_written: Set[str] - def run_coverage(self, covargs=None, htmlargs=None): + def run_coverage( + self, + covargs: Optional[Dict[str, Any]] = None, + htmlargs: Optional[Dict[str, Any]] = None, + ) -> float: """Run coverage in-process for the delta tests. For the delta tests, we always want `source=.` and we want to track @@ -162,7 +175,7 @@ with mock.patch("coverage.html.open", mock_open): return super().run_coverage(covargs=covargs, htmlargs=htmlargs) - def assert_htmlcov_files_exist(self): + def assert_htmlcov_files_exist(self) -> None: """Assert that all the expected htmlcov files exist.""" self.assert_exists("htmlcov/index.html") self.assert_exists("htmlcov/main_file_py.html") @@ -172,13 +185,13 @@ self.assert_exists("htmlcov/coverage_html.js") self.assert_exists("htmlcov/.gitignore") - def test_html_created(self): + def test_html_created(self) -> None: # Test basic HTML generation: files should be created. self.create_initial_files() self.run_coverage() self.assert_htmlcov_files_exist() - def test_html_delta_from_source_change(self): + def test_html_delta_from_source_change(self) -> None: # HTML generation can create only the files that have changed. # In this case, helper1 changes because its source is different. self.create_initial_files() @@ -205,7 +218,7 @@ index2 = self.get_html_index_content() assert index1 == index2 - def test_html_delta_from_coverage_change(self): + def test_html_delta_from_coverage_change(self) -> None: # HTML generation can create only the files that have changed. # In this case, helper1 changes because its coverage is different. self.create_initial_files() @@ -228,7 +241,7 @@ assert "htmlcov/helper2_py.html" not in self.files_written assert "htmlcov/main_file_py.html" in self.files_written - def test_html_delta_from_settings_change(self): + def test_html_delta_from_settings_change(self) -> None: # HTML generation can create only the files that have changed. # In this case, everything changes because the coverage.py settings # have changed. @@ -248,7 +261,7 @@ index2 = self.get_html_index_content() assert index1 == index2 - def test_html_delta_from_coverage_version_change(self): + def test_html_delta_from_coverage_version_change(self) -> None: # HTML generation can create only the files that have changed. # In this case, everything changes because the coverage.py version has # changed. @@ -272,7 +285,7 @@ fixed_index2 = index2.replace("XYZZY", self.real_coverage_version) assert index1 == fixed_index2 - def test_file_becomes_100(self): + def test_file_becomes_100(self) -> None: self.create_initial_files() self.run_coverage() @@ -289,7 +302,7 @@ # The 100% file, skipped, shouldn't be here. self.assert_doesnt_exist("htmlcov/helper1_py.html") - def test_status_format_change(self): + def test_status_format_change(self) -> None: self.create_initial_files() self.run_coverage() @@ -310,14 +323,14 @@ assert "htmlcov/helper2_py.html" in self.files_written assert "htmlcov/main_file_py.html" in self.files_written - def test_dont_overwrite_gitignore(self): + def test_dont_overwrite_gitignore(self) -> None: self.create_initial_files() self.make_file("htmlcov/.gitignore", "# ignore nothing") self.run_coverage() with open("htmlcov/.gitignore") as fgi: assert fgi.read() == "# ignore nothing" - def test_dont_write_gitignore_into_existing_directory(self): + def test_dont_write_gitignore_into_existing_directory(self) -> None: self.create_initial_files() self.make_file("htmlcov/README", "My files: don't touch!") self.run_coverage() @@ -328,14 +341,14 @@ class HtmlTitleTest(HtmlTestHelpers, CoverageTest): """Tests of the HTML title support.""" - def test_default_title(self): + def test_default_title(self) -> None: self.create_initial_files() self.run_coverage() index = self.get_html_index_content() assert "Coverage report" in index assert "

Coverage report:" in index - def test_title_set_in_config_file(self): + def test_title_set_in_config_file(self) -> None: self.create_initial_files() self.make_file(".coveragerc", "[html]\ntitle = Metrics & stuff!\n") self.run_coverage() @@ -343,7 +356,7 @@ assert "Metrics & stuff!" in index assert "

Metrics & stuff!:" in index - def test_non_ascii_title_set_in_config_file(self): + def test_non_ascii_title_set_in_config_file(self) -> None: self.create_initial_files() self.make_file(".coveragerc", "[html]\ntitle = «ταБЬℓσ» numbers") self.run_coverage() @@ -351,7 +364,7 @@ assert "«ταБЬℓσ» numbers" in index assert "<h1>«ταБЬℓσ» numbers" in index - def test_title_set_in_args(self): + def test_title_set_in_args(self) -> None: self.create_initial_files() self.make_file(".coveragerc", "[html]\ntitle = Good title\n") self.run_coverage(htmlargs=dict(title="«ταБЬℓσ» & stüff!")) @@ -367,7 +380,7 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): """Test the behavior when measuring unparsable files.""" - def test_dotpy_not_python(self): + def test_dotpy_not_python(self) -> None: self.make_file("main.py", "import innocuous") self.make_file("innocuous.py", "a = 1") cov = coverage.Coverage() @@ -377,7 +390,7 @@ with pytest.raises(NotPython, match=msg): cov.html_report() - def test_dotpy_not_python_ignored(self): + def test_dotpy_not_python_ignored(self) -> None: self.make_file("main.py", "import innocuous") self.make_file("innocuous.py", "a = 2") cov = coverage.Coverage() @@ -394,7 +407,7 @@ # This would be better as a glob, if the HTML layout changes: self.assert_doesnt_exist("htmlcov/innocuous.html") - def test_dothtml_not_python(self): + def test_dothtml_not_python(self) -> None: # Run an "HTML" file self.make_file("innocuous.html", "a = 3") self.make_data_file(lines={abs_file("innocuous.html"): [1]}) @@ -405,7 +418,7 @@ with pytest.raises(NoDataError, match="No data to report."): cov.html_report() - def test_execed_liar_ignored(self): + def test_execed_liar_ignored(self) -> None: # Jinja2 sets __file__ to be a non-Python file, and then execs code. # If that file contains non-Python code, a TokenError shouldn't # have been raised when writing the HTML report. @@ -417,7 +430,7 @@ cov.html_report() self.assert_exists("htmlcov/index.html") - def test_execed_liar_ignored_indentation_error(self): + def test_execed_liar_ignored_indentation_error(self) -> None: # Jinja2 sets __file__ to be a non-Python file, and then execs code. # If that file contains untokenizable code, we shouldn't get an # exception. @@ -430,7 +443,7 @@ cov.html_report() self.assert_exists("htmlcov/index.html") - def test_decode_error(self): + def test_decode_error(self) -> None: # https://github.com/nedbat/coveragepy/issues/351 # imp.load_module won't load a file with an undecodable character # in a comment, though Python will run them. So we'll change the @@ -459,7 +472,7 @@ expected = "# Isn't this great?�!" assert expected in html_report - def test_formfeeds(self): + def test_formfeeds(self) -> None: # https://github.com/nedbat/coveragepy/issues/360 self.make_file("formfeed.py", "line_one = 1\n\f\nline_two = 2\n") cov = coverage.Coverage() @@ -469,11 +482,43 @@ formfeed_html = self.get_html_report_content("formfeed.py") assert "line_two" in formfeed_html + def test_splitlines_special_chars(self) -> None: + # https://github.com/nedbat/coveragepy/issues/1512 + # See https://docs.python.org/3/library/stdtypes.html#str.splitlines for + # the characters splitlines treats specially that readlines does not. + + # I'm not exactly sure why we need the "a" strings here, but the old + # code wasn't failing without them. + self.make_file("splitlines_is_weird.py", """\ + test = { + "0b": ["\x0b0"], "a1": "this is line 2", + "0c": ["\x0c0"], "a2": "this is line 3", + "1c": ["\x1c0"], "a3": "this is line 4", + "1d": ["\x1d0"], "a4": "this is line 5", + "1e": ["\x1e0"], "a5": "this is line 6", + "85": ["\x850"], "a6": "this is line 7", + "2028": ["\u20280"], "a7": "this is line 8", + "2029": ["\u20290"], "a8": "this is line 9", + } + DONE = 1 + """) + cov = coverage.Coverage() + self.start_import_stop(cov, "splitlines_is_weird") + cov.html_report() + + the_html = self.get_html_report_content("splitlines_is_weird.py") + assert "DONE" in the_html + + # Check that the lines are properly decoded and reported... + html_lines = the_html.split("\n") + assert any(re.search(r'id="t2".*"this is line 2"', line) for line in html_lines) + assert any(re.search(r'id="t9".*"this is line 9"', line) for line in html_lines) + class HtmlTest(HtmlTestHelpers, CoverageTest): """Moar HTML tests.""" - def test_missing_source_file_incorrect_message(self): + def test_missing_source_file_incorrect_message(self) -> None: # https://github.com/nedbat/coveragepy/issues/60 self.make_file("thefile.py", "import sub.another\n") self.make_file("sub/__init__.py", "") @@ -488,7 +533,7 @@ with pytest.raises(NoSource, match=msg): cov.html_report() - def test_extensionless_file_collides_with_extension(self): + def test_extensionless_file_collides_with_extension(self) -> None: # It used to be that "program" and "program.py" would both be reported # to "program.html". Now they are not. # https://github.com/nedbat/coveragepy/issues/69 @@ -505,7 +550,7 @@ self.assert_exists("htmlcov/program.html") self.assert_exists("htmlcov/program_py.html") - def test_has_date_stamp_in_files(self): + def test_has_date_stamp_in_files(self) -> None: self.create_initial_files() self.run_coverage() @@ -514,7 +559,7 @@ with open("htmlcov/main_file_py.html") as f: self.assert_correct_timestamp(f.read()) - def test_reporting_on_unmeasured_file(self): + def test_reporting_on_unmeasured_file(self) -> None: # It should be ok to ask for an HTML report on a file that wasn't even # measured at all. https://github.com/nedbat/coveragepy/issues/403 self.create_initial_files() @@ -523,7 +568,7 @@ self.assert_exists("htmlcov/index.html") self.assert_exists("htmlcov/other_py.html") - def make_main_and_not_covered(self): + def make_main_and_not_covered(self) -> None: """Helper to create files for skip_covered scenarios.""" self.make_file("main_file.py", """ import not_covered @@ -537,14 +582,14 @@ print("n") """) - def test_report_skip_covered(self): + def test_report_skip_covered(self) -> None: self.make_main_and_not_covered() self.run_coverage(htmlargs=dict(skip_covered=True)) self.assert_exists("htmlcov/index.html") self.assert_doesnt_exist("htmlcov/main_file_py.html") self.assert_exists("htmlcov/not_covered_py.html") - def test_html_skip_covered(self): + def test_html_skip_covered(self) -> None: self.make_main_and_not_covered() self.make_file(".coveragerc", "[html]\nskip_covered = True") self.run_coverage() @@ -554,14 +599,14 @@ index = self.get_html_index_content() assert "1 file skipped due to complete coverage." in index - def test_report_skip_covered_branches(self): + def test_report_skip_covered_branches(self) -> None: self.make_main_and_not_covered() self.run_coverage(covargs=dict(branch=True), htmlargs=dict(skip_covered=True)) self.assert_exists("htmlcov/index.html") self.assert_doesnt_exist("htmlcov/main_file_py.html") self.assert_exists("htmlcov/not_covered_py.html") - def test_report_skip_covered_100(self): + def test_report_skip_covered_100(self) -> None: self.make_file("main_file.py", """ def normal(): print("z") @@ -571,7 +616,7 @@ assert res == 100.0 self.assert_doesnt_exist("htmlcov/main_file_py.html") - def make_init_and_main(self): + def make_init_and_main(self) -> None: """Helper to create files for skip_empty scenarios.""" self.make_file("submodule/__init__.py", "") self.make_file("main_file.py", """ @@ -582,7 +627,7 @@ normal() """) - def test_report_skip_empty(self): + def test_report_skip_empty(self) -> None: self.make_init_and_main() self.run_coverage(htmlargs=dict(skip_empty=True)) self.assert_exists("htmlcov/index.html") @@ -591,7 +636,7 @@ index = self.get_html_index_content() assert "1 empty file skipped." in index - def test_html_skip_empty(self): + def test_html_skip_empty(self) -> None: self.make_init_and_main() self.make_file(".coveragerc", "[html]\nskip_empty = True") self.run_coverage() @@ -600,7 +645,7 @@ self.assert_doesnt_exist("htmlcov/submodule___init___py.html") -def filepath_to_regex(path): +def filepath_to_regex(path: str) -> str: """Create a regex for scrubbing a file path.""" regex = re.escape(path) # If there's a backslash, let it match either slash. @@ -610,12 +655,16 @@ return regex -def compare_html(expected, actual, extra_scrubs=None): +def compare_html( + expected: str, + actual: str, + extra_scrubs: Optional[List[Tuple[str, str]]] = None, +) -> None: """Specialized compare function for our HTML files.""" __tracebackhide__ = True # pytest, please don't show me this function. scrubs = [ - (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'), - (r'coverage.py v[\d.abc]+', 'coverage.py vVER'), + (r'/coverage\.readthedocs\.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'), + (r'coverage\.py v[\d.abcdev]+', 'coverage.py vVER'), (r'created at \d\d\d\d-\d\d-\d\d \d\d:\d\d [-+]\d\d\d\d', 'created at DATE'), (r'created at \d\d\d\d-\d\d-\d\d \d\d:\d\d', 'created at DATE'), # Occasionally an absolute path is in the HTML report. @@ -639,7 +688,7 @@ class HtmlGoldTest(CoverageTest): """Tests of HTML reporting that use gold files.""" - def test_a(self): + def test_a(self) -> None: self.make_file("a.py", """\ if 1 < 2: # Needed a < to look at HTML entities. @@ -668,7 +717,7 @@ '<td class="right" data-ratio="2 3">67%</td>', ) - def test_b_branch(self): + def test_b_branch(self) -> None: self.make_file("b.py", """\ def one(x): # This will be a branch that misses the else. @@ -733,7 +782,7 @@ '<td class="right" data-ratio="16 23">70%</td>', ) - def test_bom(self): + def test_bom(self) -> None: self.make_file("bom.py", bytes=b"""\ \xef\xbb\xbf# A Python source file in utf-8, with BOM. math = "3\xc3\x974 = 12, \xc3\xb72 = 6\xc2\xb10" @@ -766,7 +815,7 @@ '<span class="str">"3×4 = 12, ÷2 = 6±0"</span>', ) - def test_isolatin1(self): + def test_isolatin1(self) -> None: self.make_file("isolatin1.py", bytes=b"""\ # -*- coding: iso8859-1 -*- # A Python source file in another encoding. @@ -785,7 +834,7 @@ '<span class="str">"3×4 = 12, ÷2 = 6±0"</span>', ) - def make_main_etc(self): + def make_main_etc(self) -> None: """Make main.py and m1-m3.py for other tests.""" self.make_file("main.py", """\ import m1 @@ -812,28 +861,28 @@ m3b = 2 """) - def test_omit_1(self): + def test_omit_1(self) -> None: self.make_main_etc() cov = coverage.Coverage(include=["./*"]) self.start_import_stop(cov, "main") cov.html_report(directory="out/omit_1") compare_html(gold_path("html/omit_1"), "out/omit_1") - def test_omit_2(self): + def test_omit_2(self) -> None: self.make_main_etc() cov = coverage.Coverage(include=["./*"]) self.start_import_stop(cov, "main") cov.html_report(directory="out/omit_2", omit=["m1.py"]) compare_html(gold_path("html/omit_2"), "out/omit_2") - def test_omit_3(self): + def test_omit_3(self) -> None: self.make_main_etc() cov = coverage.Coverage(include=["./*"]) self.start_import_stop(cov, "main") cov.html_report(directory="out/omit_3", omit=["m1.py", "m2.py"]) compare_html(gold_path("html/omit_3"), "out/omit_3") - def test_omit_4(self): + def test_omit_4(self) -> None: self.make_main_etc() self.make_file("omit4.ini", """\ [report] @@ -845,7 +894,7 @@ cov.html_report(directory="out/omit_4") compare_html(gold_path("html/omit_4"), "out/omit_4") - def test_omit_5(self): + def test_omit_5(self) -> None: self.make_main_etc() self.make_file("omit5.ini", """\ [report] @@ -863,7 +912,7 @@ cov.html_report() compare_html(gold_path("html/omit_5"), "out/omit_5") - def test_other(self): + def test_other(self) -> None: self.make_file("src/here.py", """\ import other @@ -903,7 +952,7 @@ 'other.py</a>', ) - def test_partial(self): + def test_partial(self) -> None: self.make_file("partial.py", """\ # partial branches and excluded lines a = 2 @@ -970,7 +1019,7 @@ '<span class="pc_cov">91%</span>', ) - def test_styled(self): + def test_styled(self) -> None: self.make_file("a.py", """\ if 1 < 2: # Needed a < to look at HTML entities. @@ -1003,7 +1052,7 @@ '<span class="pc_cov">67%</span>', ) - def test_tabbed(self): + def test_tabbed(self) -> None: # The file contents would look like this with 8-space tabs: # x = 1 # if x: @@ -1037,7 +1086,7 @@ doesnt_contain("out/tabbed_py.html", "\t") - def test_unicode(self): + def test_unicode(self) -> None: surrogate = "\U000e0100" self.make_file("unicode.py", """\ @@ -1064,7 +1113,7 @@ '<span class="str">"db40,dd00: x󠄀"</span>', ) - def test_accented_dot_py(self): + def test_accented_dot_py(self) -> None: # Make a file with a non-ascii character in the filename. self.make_file("h\xe2t.py", "print('accented')") self.make_data_file(lines={abs_file("h\xe2t.py"): [1]}) @@ -1076,7 +1125,7 @@ index = indexf.read() assert '<a href="hât_py.html">hât.py</a>' in index - def test_accented_directory(self): + def test_accented_directory(self) -> None: # Make a file with a non-ascii character in the directory name. self.make_file("\xe2/accented.py", "print('accented')") self.make_data_file(lines={abs_file("\xe2/accented.py"): [1]}) @@ -1097,7 +1146,7 @@ EMPTY = coverage.html.HtmlDataGeneration.EMPTY - def html_data_from_cov(self, cov, morf): + def html_data_from_cov(self, cov: Coverage, morf: TMorf) -> coverage.html.FileData: """Get HTML report data from a `Coverage` object for a morf.""" with self.assert_warnings(cov, []): datagen = coverage.html.HtmlDataGeneration(cov) @@ -1134,7 +1183,7 @@ TEST_ONE_LINES = [5, 6, 2] TEST_TWO_LINES = [9, 10, 11, 13, 14, 15, 2] - def test_dynamic_contexts(self): + def test_dynamic_contexts(self) -> None: self.make_file("two_tests.py", self.SOURCE) cov = coverage.Coverage(source=["."]) cov.set_option("run:dynamic_context", "test_function") @@ -1150,7 +1199,10 @@ ] assert sorted(expected) == sorted(actual) - def test_filtered_dynamic_contexts(self): + cov.html_report(mod, directory="out/contexts") + compare_html(gold_path("html/contexts"), "out/contexts") + + def test_filtered_dynamic_contexts(self) -> None: self.make_file("two_tests.py", self.SOURCE) cov = coverage.Coverage(source=["."]) cov.set_option("run:dynamic_context", "test_function") @@ -1160,12 +1212,12 @@ d = self.html_data_from_cov(cov, mod) context_labels = [self.EMPTY, 'two_tests.test_one', 'two_tests.test_two'] - expected_lines = [[], self.TEST_ONE_LINES, []] + expected_lines: List[List[TLineNo]] = [[], self.TEST_ONE_LINES, []] for label, expected in zip(context_labels, expected_lines): actual = [ld.number for ld in d.lines if label in (ld.contexts or ())] assert sorted(expected) == sorted(actual) - def test_no_contexts_warns_no_contexts(self): + def test_no_contexts_warns_no_contexts(self) -> None: # If no contexts were collected, then show_contexts emits a warning. self.make_file("two_tests.py", self.SOURCE) cov = coverage.Coverage(source=["."]) @@ -1174,7 +1226,7 @@ with self.assert_warnings(cov, ["No contexts were measured"]): cov.html_report() - def test_dynamic_contexts_relative_files(self): + def test_dynamic_contexts_relative_files(self) -> None: self.make_file("two_tests.py", self.SOURCE) self.make_file("config", "[run]\nrelative_files = True") cov = coverage.Coverage(source=["."], config_file="config") @@ -1195,16 +1247,25 @@ class HtmlHelpersTest(HtmlTestHelpers, CoverageTest): """Tests of the helpers in HtmlTestHelpers.""" - def test_bad_link(self): + def test_bad_link(self) -> None: # Does assert_valid_hrefs detect links to non-existent files? self.make_file("htmlcov/index.html", "<a href='nothing.html'>Nothing</a>") msg = "These files link to 'nothing.html', which doesn't exist: htmlcov.index.html" with pytest.raises(AssertionError, match=msg): self.assert_valid_hrefs() - def test_bad_anchor(self): + def test_bad_anchor(self) -> None: # Does assert_valid_hrefs detect fragments that go nowhere? self.make_file("htmlcov/index.html", "<a href='#nothing'>Nothing</a>") msg = "Fragment '#nothing' in htmlcov.index.html has no anchor" with pytest.raises(AssertionError, match=msg): self.assert_valid_hrefs() + + +@pytest.mark.parametrize("n, key", [ + (0, "a"), + (1, "b"), + (999999999, "e9S_p"), +]) +def test_encode_int(n: int, key: str) -> None: + assert coverage.html.encode_int(n) == key diff -Nru python-coverage-6.5.0+dfsg1/tests/test_json.py python-coverage-7.2.7+dfsg1/tests/test_json.py --- python-coverage-6.5.0+dfsg1/tests/test_json.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_json.py 2023-05-29 19:46:30.000000000 +0000 @@ -2,17 +2,29 @@ # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Test json-based summary reporting for coverage.py""" -from datetime import datetime + +from __future__ import annotations + import json import os +from datetime import datetime +from typing import Any, Dict + import coverage +from coverage import Coverage + from tests.coveragetest import UsingModulesMixin, CoverageTest class JsonReportTest(UsingModulesMixin, CoverageTest): """Tests of the JSON reports from coverage.py.""" - def _assert_expected_json_report(self, cov, expected_result): + + def _assert_expected_json_report( + self, + cov: Coverage, + expected_result: Dict[str, Any], + ) -> None: """ Helper for tests that handles the common ceremony so the tests can be clearly show the consequences of setting various arguments. @@ -39,7 +51,7 @@ del (parsed_result['meta']['timestamp']) assert parsed_result == expected_result - def test_branch_coverage(self): + def test_branch_coverage(self) -> None: cov = coverage.Coverage(branch=True) expected_result = { 'meta': { @@ -91,7 +103,7 @@ } self._assert_expected_json_report(cov, expected_result) - def test_simple_line_coverage(self): + def test_simple_line_coverage(self) -> None: cov = coverage.Coverage() expected_result = { 'meta': { @@ -125,7 +137,7 @@ } self._assert_expected_json_report(cov, expected_result) - def run_context_test(self, relative_files): + def run_context_test(self, relative_files: bool) -> None: """A helper for two tests below.""" self.make_file("config", """\ [run] @@ -187,8 +199,8 @@ } self._assert_expected_json_report(cov, expected_result) - def test_context_non_relative(self): + def test_context_non_relative(self) -> None: self.run_context_test(relative_files=False) - def test_context_relative(self): + def test_context_relative(self) -> None: self.run_context_test(relative_files=True) diff -Nru python-coverage-6.5.0+dfsg1/tests/test_lcov.py python-coverage-7.2.7+dfsg1/tests/test_lcov.py --- python-coverage-6.5.0+dfsg1/tests/test_lcov.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_lcov.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,9 @@ """Test LCOV-based summary reporting for coverage.py.""" +from __future__ import annotations + +import math import textwrap from tests.coveragetest import CoverageTest @@ -14,14 +17,12 @@ class LcovTest(CoverageTest): """Tests of the LCOV reports from coverage.py.""" - def create_initial_files(self): + def create_initial_files(self) -> None: """ Helper for tests that handles the common ceremony so the tests can show the consequences of changes in the setup. """ self.make_file("main_file.py", """\ - #!/usr/bin/env python3 - def cuboid_volume(l): return (l*l*l) @@ -30,8 +31,6 @@ """) self.make_file("test_file.py", """\ - #!/usr/bin/env python3 - from main_file import cuboid_volume import unittest @@ -43,18 +42,15 @@ self.assertAlmostEqual(cuboid_volume(5.5),166.375) """) - def get_lcov_report_content(self, filename="coverage.lcov"): + def get_lcov_report_content(self, filename: str = "coverage.lcov") -> str: """Return the content of an LCOV report.""" with open(filename, "r") as file: - file_contents = file.read() - return file_contents + return file.read() - def test_lone_file(self): - """For a single file with a couple of functions, the lcov should cover - the function definitions themselves, but not the returns.""" + def test_lone_file(self) -> None: + # For a single file with a couple of functions, the lcov should cover + # the function definitions themselves, but not the returns. self.make_file("main_file.py", """\ - #!/usr/bin/env python3 - def cuboid_volume(l): return (l*l*l) @@ -64,10 +60,10 @@ expected_result = textwrap.dedent("""\ TN: SF:main_file.py - DA:3,1,7URou3io0zReBkk69lEb/Q - DA:6,1,ilhb4KUfytxtEuClijZPlQ - DA:4,0,Xqj6H1iz/nsARMCAbE90ng - DA:7,0,LWILTcvARcydjFFyo9qM0A + DA:1,1,7URou3io0zReBkk69lEb/Q + DA:4,1,ilhb4KUfytxtEuClijZPlQ + DA:2,0,Xqj6H1iz/nsARMCAbE90ng + DA:5,0,LWILTcvARcydjFFyo9qM0A LF:4 LH:2 end_of_record @@ -75,40 +71,42 @@ self.assert_doesnt_exist(".coverage") cov = coverage.Coverage(source=["."]) self.start_import_stop(cov, "main_file") - cov.lcov_report() + pct = cov.lcov_report() + assert pct == 50.0 actual_result = self.get_lcov_report_content() assert expected_result == actual_result - def test_simple_line_coverage_two_files(self): - """Test that line coverage is created when coverage is run, - and matches the output of the file below.""" + def test_simple_line_coverage_two_files(self) -> None: + # Test that line coverage is created when coverage is run, + # and matches the output of the file below. self.create_initial_files() self.assert_doesnt_exist(".coverage") self.make_file(".coveragerc", "[lcov]\noutput = data.lcov\n") cov = coverage.Coverage(source=".") self.start_import_stop(cov, "test_file") - cov.lcov_report() + pct = cov.lcov_report() + assert pct == 50.0 self.assert_exists("data.lcov") expected_result = textwrap.dedent("""\ TN: SF:main_file.py - DA:3,1,7URou3io0zReBkk69lEb/Q - DA:6,1,ilhb4KUfytxtEuClijZPlQ - DA:4,0,Xqj6H1iz/nsARMCAbE90ng - DA:7,0,LWILTcvARcydjFFyo9qM0A + DA:1,1,7URou3io0zReBkk69lEb/Q + DA:4,1,ilhb4KUfytxtEuClijZPlQ + DA:2,0,Xqj6H1iz/nsARMCAbE90ng + DA:5,0,LWILTcvARcydjFFyo9qM0A LF:4 LH:2 end_of_record TN: SF:test_file.py - DA:3,1,R5Rb4IzmjKRgY/vFFc1TRg - DA:4,1,E/tvV9JPVDhEcTCkgrwOFw - DA:6,1,GP08LPBYJq8EzYveHJy2qA - DA:7,1,MV+jSLi6PFEl+WatEAptog - DA:8,0,qyqd1mF289dg6oQAQHA+gQ - DA:9,0,nmEYd5F1KrxemgC9iVjlqg - DA:10,0,jodMK26WYDizOO1C7ekBbg - DA:11,0,LtxfKehkX8o4KvC5GnN52g + DA:1,1,R5Rb4IzmjKRgY/vFFc1TRg + DA:2,1,E/tvV9JPVDhEcTCkgrwOFw + DA:4,1,GP08LPBYJq8EzYveHJy2qA + DA:5,1,MV+jSLi6PFEl+WatEAptog + DA:6,0,qyqd1mF289dg6oQAQHA+gQ + DA:7,0,nmEYd5F1KrxemgC9iVjlqg + DA:8,0,jodMK26WYDizOO1C7ekBbg + DA:9,0,LtxfKehkX8o4KvC5GnN52g LF:8 LH:4 end_of_record @@ -116,11 +114,9 @@ actual_result = self.get_lcov_report_content(filename="data.lcov") assert expected_result == actual_result - def test_branch_coverage_one_file(self): - """Test that the reporter produces valid branch coverage.""" + def test_branch_coverage_one_file(self) -> None: + # Test that the reporter produces valid branch coverage. self.make_file("main_file.py", """\ - #!/usr/bin/env python3 - def is_it_x(x): if x == 3: return x @@ -130,19 +126,20 @@ self.assert_doesnt_exist(".coverage") cov = coverage.Coverage(branch=True, source=".") self.start_import_stop(cov, "main_file") - cov.lcov_report() + pct = cov.lcov_report() + assert math.isclose(pct, 16.666666666666668) self.assert_exists("coverage.lcov") expected_result = textwrap.dedent("""\ TN: SF:main_file.py - DA:3,1,4MDXMbvwQ3L7va1tsphVzw - DA:4,0,MuERA6EYyZNpKPqoJfzwkA - DA:5,0,sAyiiE6iAuPMte9kyd0+3g - DA:7,0,W/g8GJDAYJkSSurt59Mzfw + DA:1,1,4MDXMbvwQ3L7va1tsphVzw + DA:2,0,MuERA6EYyZNpKPqoJfzwkA + DA:3,0,sAyiiE6iAuPMte9kyd0+3g + DA:5,0,W/g8GJDAYJkSSurt59Mzfw LF:4 LH:1 - BRDA:5,0,0,- - BRDA:7,0,1,- + BRDA:3,0,0,- + BRDA:5,0,1,- BRF:2 BRH:0 end_of_record @@ -150,12 +147,10 @@ actual_result = self.get_lcov_report_content() assert expected_result == actual_result - def test_branch_coverage_two_files(self): - """Test that valid branch coverage is generated - in the case of two files.""" + def test_branch_coverage_two_files(self) -> None: + # Test that valid branch coverage is generated + # in the case of two files. self.make_file("main_file.py", """\ - #!/usr/bin/env python3 - def is_it_x(x): if x == 3: return x @@ -164,8 +159,6 @@ """) self.make_file("test_file.py", """\ - #!/usr/bin/env python3 - from main_file import * import unittest @@ -177,30 +170,31 @@ self.assert_doesnt_exist(".coverage") cov = coverage.Coverage(branch=True, source=".") self.start_import_stop(cov, "test_file") - cov.lcov_report() + pct = cov.lcov_report() + assert math.isclose(pct, 41.666666666666664) self.assert_exists("coverage.lcov") expected_result = textwrap.dedent("""\ TN: SF:main_file.py - DA:3,1,4MDXMbvwQ3L7va1tsphVzw - DA:4,0,MuERA6EYyZNpKPqoJfzwkA - DA:5,0,sAyiiE6iAuPMte9kyd0+3g - DA:7,0,W/g8GJDAYJkSSurt59Mzfw + DA:1,1,4MDXMbvwQ3L7va1tsphVzw + DA:2,0,MuERA6EYyZNpKPqoJfzwkA + DA:3,0,sAyiiE6iAuPMte9kyd0+3g + DA:5,0,W/g8GJDAYJkSSurt59Mzfw LF:4 LH:1 - BRDA:5,0,0,- - BRDA:7,0,1,- + BRDA:3,0,0,- + BRDA:5,0,1,- BRF:2 BRH:0 end_of_record TN: SF:test_file.py - DA:3,1,9TxKIyoBtmhopmlbDNa8FQ - DA:4,1,E/tvV9JPVDhEcTCkgrwOFw - DA:6,1,C3s/c8C1Yd/zoNG1GnGexg - DA:7,1,9qPyWexYysgeKtB+YvuzAg - DA:8,0,LycuNcdqoUhPXeuXUTf5lA - DA:9,0,FPTWzd68bDx76HN7VHu1wA + DA:1,1,9TxKIyoBtmhopmlbDNa8FQ + DA:2,1,E/tvV9JPVDhEcTCkgrwOFw + DA:4,1,C3s/c8C1Yd/zoNG1GnGexg + DA:5,1,9qPyWexYysgeKtB+YvuzAg + DA:6,0,LycuNcdqoUhPXeuXUTf5lA + DA:7,0,FPTWzd68bDx76HN7VHu1wA LF:6 LH:4 BRF:0 @@ -210,10 +204,9 @@ actual_result = self.get_lcov_report_content() assert actual_result == expected_result - def test_half_covered_branch(self): - """Test that for a given branch that is only half covered, - the block numbers remain the same, and produces valid lcov. - """ + def test_half_covered_branch(self) -> None: + # Test that for a given branch that is only half covered, + # the block numbers remain the same, and produces valid lcov. self.make_file("main_file.py", """\ something = True @@ -225,7 +218,8 @@ self.assert_doesnt_exist(".coverage") cov = coverage.Coverage(branch=True, source=".") self.start_import_stop(cov, "main_file") - cov.lcov_report() + pct = cov.lcov_report() + assert math.isclose(pct, 66.66666666666667) self.assert_exists("coverage.lcov") expected_result = textwrap.dedent("""\ TN: @@ -245,21 +239,21 @@ actual_result = self.get_lcov_report_content() assert actual_result == expected_result - def test_empty_init_files(self): - """Test that in the case of an empty __init__.py file, the lcov - reporter will note that the file is there, and will note the empty - line. It will also note the lack of branches, and the checksum for - the line. - - Although there are no lines found, it will note one line as hit in - old Pythons, and no lines hit in newer Pythons. - """ + def test_empty_init_files(self) -> None: + # Test that in the case of an empty __init__.py file, the lcov + # reporter will note that the file is there, and will note the empty + # line. It will also note the lack of branches, and the checksum for + # the line. + # + # Although there are no lines found, it will note one line as hit in + # old Pythons, and no lines hit in newer Pythons. self.make_file("__init__.py", "") self.assert_doesnt_exist(".coverage") cov = coverage.Coverage(branch=True, source=".") self.start_import_stop(cov, "__init__") - cov.lcov_report() + pct = cov.lcov_report() + assert pct == 0.0 self.assert_exists("coverage.lcov") # Newer Pythons have truly empty empty files. if env.PYBEHAVIOR.empty_is_empty: @@ -278,7 +272,7 @@ SF:__init__.py DA:1,1,1B2M2Y8AsgTpgAmY7PhCfg LF:0 - LH:1 + LH:0 BRF:0 BRH:0 end_of_record diff -Nru python-coverage-6.5.0+dfsg1/tests/test_misc.py python-coverage-7.2.7+dfsg1/tests/test_misc.py --- python-coverage-6.5.0+dfsg1/tests/test_misc.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_misc.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,15 +3,17 @@ """Tests of miscellaneous stuff.""" +from __future__ import annotations + import sys +from unittest import mock import pytest -from coverage import env 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, import_third_party -from coverage.misc import human_sorted, human_sorted_items +from coverage.misc import file_be_gone +from coverage.misc import Hasher, substitute_variables, import_third_party +from coverage.misc import human_sorted, human_sorted_items, stdout_link from tests.coveragetest import CoverageTest @@ -21,7 +23,7 @@ run_in_temp_dir = False - def test_string_hashing(self): + def test_string_hashing(self) -> None: h1 = Hasher() h1.update("Hello, world!") h2 = Hasher() @@ -31,28 +33,28 @@ assert h1.hexdigest() != h2.hexdigest() assert h1.hexdigest() == h3.hexdigest() - def test_bytes_hashing(self): + def test_bytes_hashing(self) -> None: h1 = Hasher() h1.update(b"Hello, world!") h2 = Hasher() h2.update(b"Goodbye!") assert h1.hexdigest() != h2.hexdigest() - def test_unicode_hashing(self): + def test_unicode_hashing(self) -> None: h1 = Hasher() h1.update("Hello, world! \N{SNOWMAN}") h2 = Hasher() h2.update("Goodbye!") assert h1.hexdigest() != h2.hexdigest() - def test_dict_hashing(self): + def test_dict_hashing(self) -> None: h1 = Hasher() h1.update({'a': 17, 'b': 23}) h2 = Hasher() h2.update({'b': 23, 'a': 17}) assert h1.hexdigest() == h2.hexdigest() - def test_dict_collision(self): + def test_dict_collision(self) -> None: h1 = Hasher() h1.update({'a': 17, 'b': {'c': 1, 'd': 2}}) h2 = Hasher() @@ -63,73 +65,23 @@ class RemoveFileTest(CoverageTest): """Tests of misc.file_be_gone.""" - def test_remove_nonexistent_file(self): + def test_remove_nonexistent_file(self) -> None: # It's OK to try to remove a file that doesn't exist. file_be_gone("not_here.txt") - def test_remove_actual_file(self): + def test_remove_actual_file(self) -> None: # It really does remove a file that does exist. self.make_file("here.txt", "We are here, we are here, we are here!") file_be_gone("here.txt") self.assert_doesnt_exist("here.txt") - def test_actual_errors(self): + def test_actual_errors(self) -> None: # Errors can still happen. # ". is a directory" on Unix, or "Access denied" on Windows with pytest.raises(OSError): file_be_gone(".") -@pytest.mark.skipif(not env.USE_CONTRACTS, reason="Contracts are disabled, can't test them") -class ContractTest(CoverageTest): - """Tests of our contract decorators.""" - - run_in_temp_dir = False - - def test_bytes(self): - @contract(text='bytes|None') - def need_bytes(text=None): - return text - - assert need_bytes(b"Hey") == b"Hey" - assert need_bytes() is None - with pytest.raises(Exception): - need_bytes("Oops") - - def test_unicode(self): - @contract(text='unicode|None') - def need_unicode(text=None): - return text - - assert need_unicode("Hey") == "Hey" - assert need_unicode() is None - with pytest.raises(Exception): - need_unicode(b"Oops") - - def test_one_of(self): - @one_of("a, b, c") - def give_me_one(a=None, b=None, c=None): - return (a, b, c) - - assert give_me_one(a=17) == (17, None, None) - assert give_me_one(b=set()) == (None, set(), None) - assert give_me_one(c=17) == (None, None, 17) - with pytest.raises(AssertionError): - give_me_one(a=17, b=set()) - with pytest.raises(AssertionError): - give_me_one() - - def test_dummy_decorator_with_args(self): - @dummy_decorator_with_args("anything", this=17, that="is fine") - def undecorated(a=None, b=None): - return (a, b) - - assert undecorated() == (None, None) - assert undecorated(17) == (17, None) - assert undecorated(b=23) == (None, 23) - assert undecorated(b=42, a=3) == (3, 42) - - VARS = { 'FOO': 'fooey', 'BAR': 'xyzzy', @@ -147,13 +99,13 @@ ("Defaulted: ${WUT-missing}!", "Defaulted: missing!"), ("Defaulted empty: ${WUT-}!", "Defaulted empty: !"), ]) -def test_substitute_variables(before, after): +def test_substitute_variables(before: str, after: str) -> None: assert substitute_variables(before, VARS) == after @pytest.mark.parametrize("text", [ "Strict: ${NOTHING?} is an error", ]) -def test_substitute_variables_errors(text): +def test_substitute_variables_errors(text: str) -> None: with pytest.raises(CoverageException) as exc_info: substitute_variables(text, VARS) assert text in str(exc_info.value) @@ -165,11 +117,12 @@ run_in_temp_dir = False - def test_success(self): + def test_success(self) -> None: # Make sure we don't have pytest in sys.modules before we start. del sys.modules["pytest"] # Import pytest - mod = import_third_party("pytest") + mod, has = import_third_party("pytest") + assert has # Yes, it's really pytest: assert mod.__name__ == "pytest" print(dir(mod)) @@ -177,9 +130,9 @@ # But it's not in sys.modules: assert "pytest" not in sys.modules - def test_failure(self): - mod = import_third_party("xyzzy") - assert mod is None + def test_failure(self) -> None: + _, has = import_third_party("xyzzy") + assert not has assert "xyzzy" not in sys.modules @@ -190,14 +143,32 @@ ] @pytest.mark.parametrize("words, ordered", HUMAN_DATA) -def test_human_sorted(words, ordered): +def test_human_sorted(words: str, ordered: str) -> None: assert " ".join(human_sorted(words.split())) == ordered @pytest.mark.parametrize("words, ordered", HUMAN_DATA) -def test_human_sorted_items(words, ordered): +def test_human_sorted_items(words: str, ordered: str) -> None: keys = words.split() items = [(k, 1) for k in keys] + [(k, 2) for k in keys] okeys = ordered.split() oitems = [(k, v) for k in okeys for v in [1, 2]] assert human_sorted_items(items) == oitems assert human_sorted_items(items, reverse=True) == oitems[::-1] + + +def test_stdout_link_tty() -> None: + with mock.patch.object(sys.stdout, "isatty", lambda:True): + link = stdout_link("some text", "some url") + assert link == "\033]8;;some url\asome text\033]8;;\a" + + +def test_stdout_link_not_tty() -> None: + # Without mocking isatty, it reports False in a pytest suite. + assert stdout_link("some text", "some url") == "some text" + + +def test_stdout_link_with_fake_stdout() -> None: + # If stdout is another object, we should still be ok. + with mock.patch.object(sys, "stdout", object()): + link = stdout_link("some text", "some url") + assert link == "some text" diff -Nru python-coverage-6.5.0+dfsg1/tests/test_mixins.py python-coverage-7.2.7+dfsg1/tests/test_mixins.py --- python-coverage-6.5.0+dfsg1/tests/test_mixins.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_mixins.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Tests of code in tests/mixins.py""" +from __future__ import annotations + import pytest from coverage.misc import import_local_file @@ -13,12 +15,12 @@ class TempDirMixinTest(TempDirMixin): """Test the methods in TempDirMixin.""" - def file_text(self, fname): + def file_text(self, fname: str) -> str: """Return the text read from a file.""" with open(fname, "rb") as f: return f.read().decode('ascii') - def test_make_file(self): + def test_make_file(self) -> None: # A simple file. self.make_file("fooey.boo", "Hello there") assert self.file_text("fooey.boo") == "Hello there" @@ -38,7 +40,7 @@ """) assert self.file_text("dedented.txt") == "Hello\nBye\n" - def test_make_file_newline(self): + def test_make_file_newline(self) -> None: self.make_file("unix.txt", "Hello\n") assert self.file_text("unix.txt") == "Hello\n" self.make_file("dos.txt", "Hello\n", newline="\r\n") @@ -46,13 +48,13 @@ self.make_file("mac.txt", "Hello\n", newline="\r") assert self.file_text("mac.txt") == "Hello\r" - def test_make_file_non_ascii(self): + def test_make_file_non_ascii(self) -> None: self.make_file("unicode.txt", "tablo: «ταБℓσ»") with open("unicode.txt", "rb") as f: text = f.read() assert text == b"tablo: \xc2\xab\xcf\x84\xce\xb1\xd0\x91\xe2\x84\x93\xcf\x83\xc2\xbb" - def test_make_bytes_file(self): + def test_make_bytes_file(self) -> None: self.make_file("binary.dat", bytes=b"\x99\x33\x66hello\0") with open("binary.dat", "rb") as f: data = f.read() @@ -63,12 +65,12 @@ """Tests of SysPathModulesMixin.""" @pytest.mark.parametrize("val", [17, 42]) - def test_module_independence(self, val): + def test_module_independence(self, val: int) -> None: self.make_file("xyzzy.py", f"A = {val}") import xyzzy # pylint: disable=import-error assert xyzzy.A == val - def test_cleanup_and_reimport(self): + def test_cleanup_and_reimport(self) -> None: self.make_file("xyzzy.py", "A = 17") xyzzy = import_local_file("xyzzy") assert xyzzy.A == 17 diff -Nru python-coverage-6.5.0+dfsg1/tests/test_numbits.py python-coverage-7.2.7+dfsg1/tests/test_numbits.py --- python-coverage-6.5.0+dfsg1/tests/test_numbits.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_numbits.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,9 +3,13 @@ """Tests for coverage.numbits""" +from __future__ import annotations + import json import sqlite3 +from typing import Iterable, Set + from hypothesis import example, given, settings from hypothesis.strategies import sets, integers @@ -29,7 +33,7 @@ default_settings = settings(default_settings, deadline=None) -def good_numbits(numbits): +def good_numbits(numbits: bytes) -> None: """Assert that numbits is good.""" # It shouldn't end with a zero byte, that should have been trimmed off. assert (not numbits) or (numbits[-1] != 0) @@ -42,7 +46,7 @@ @given(line_number_sets) @settings(default_settings) - def test_conversion(self, nums): + def test_conversion(self, nums: Iterable[int]) -> None: numbits = nums_to_numbits(nums) good_numbits(numbits) nums2 = numbits_to_nums(numbits) @@ -50,7 +54,7 @@ @given(line_number_sets, line_number_sets) @settings(default_settings) - def test_union(self, nums1, nums2): + def test_union(self, nums1: Set[int], nums2: Set[int]) -> None: nb1 = nums_to_numbits(nums1) good_numbits(nb1) nb2 = nums_to_numbits(nums2) @@ -62,7 +66,7 @@ @given(line_number_sets, line_number_sets) @settings(default_settings) - def test_intersection(self, nums1, nums2): + def test_intersection(self, nums1: Set[int], nums2: Set[int]) -> None: nb1 = nums_to_numbits(nums1) good_numbits(nb1) nb2 = nums_to_numbits(nums2) @@ -74,7 +78,7 @@ @given(line_number_sets, line_number_sets) @settings(default_settings) - def test_any_intersection(self, nums1, nums2): + def test_any_intersection(self, nums1: Set[int], nums2: Set[int]) -> None: nb1 = nums_to_numbits(nums1) good_numbits(nb1) nb2 = nums_to_numbits(nums2) @@ -86,7 +90,7 @@ @given(line_numbers, line_number_sets) @settings(default_settings) @example(152, {144}) - def test_num_in_numbits(self, num, nums): + def test_num_in_numbits(self, num: int, nums: Iterable[int]) -> None: numbits = nums_to_numbits(nums) good_numbits(numbits) is_in = num_in_numbits(num, numbits) @@ -98,7 +102,7 @@ run_in_temp_dir = False - def setUp(self): + def setUp(self) -> None: super().setUp() conn = sqlite3.connect(":memory:") register_sqlite_functions(conn) @@ -113,7 +117,7 @@ ) self.addCleanup(self.cursor.close) - def test_numbits_union(self): + def test_numbits_union(self) -> None: res = self.cursor.execute( "select numbits_union(" + "(select numbits from data where id = 7)," + @@ -127,7 +131,7 @@ answer = numbits_to_nums(list(res)[0][0]) assert expected == answer - def test_numbits_intersection(self): + def test_numbits_intersection(self) -> None: res = self.cursor.execute( "select numbits_intersection(" + "(select numbits from data where id = 7)," + @@ -137,7 +141,7 @@ answer = numbits_to_nums(list(res)[0][0]) assert [63] == answer - def test_numbits_any_intersection(self): + def test_numbits_any_intersection(self) -> None: res = self.cursor.execute( "select numbits_any_intersection(?, ?)", (nums_to_numbits([1, 2, 3]), nums_to_numbits([3, 4, 5])) @@ -152,11 +156,11 @@ answer = [any_inter for (any_inter,) in res] assert [0] == answer - def test_num_in_numbits(self): + def test_num_in_numbits(self) -> None: res = self.cursor.execute("select id, num_in_numbits(12, numbits) from data order by id") answer = [is_in for (id, is_in) in res] assert [1, 1, 1, 1, 0, 1, 0, 0, 0, 0] == answer - def test_numbits_to_nums(self): + def test_numbits_to_nums(self) -> None: res = self.cursor.execute("select numbits_to_nums(?)", [nums_to_numbits([1, 2, 3])]) assert [1, 2, 3] == json.loads(res.fetchone()[0]) diff -Nru python-coverage-6.5.0+dfsg1/tests/test_oddball.py python-coverage-7.2.7+dfsg1/tests/test_oddball.py --- python-coverage-6.5.0+dfsg1/tests/test_oddball.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_oddball.py 2023-05-29 19:46:30.000000000 +0000 @@ -12,6 +12,7 @@ import coverage from coverage import env +from coverage.data import sorted_lines from coverage.files import abs_file from coverage.misc import import_local_file @@ -23,7 +24,7 @@ class ThreadingTest(CoverageTest): """Tests of the threading support.""" - def test_threading(self): + def test_threading(self) -> None: self.check_coverage("""\ import threading @@ -43,7 +44,7 @@ """, [1, 3, 4, 6, 7, 9, 10, 12, 13, 14, 15], "10") - def test_thread_run(self): + def test_thread_run(self) -> None: self.check_coverage("""\ import threading @@ -66,7 +67,7 @@ class RecursionTest(CoverageTest): """Check what happens when recursive code gets near limits.""" - def test_short_recursion(self): + def test_short_recursion(self) -> None: # We can definitely get close to 500 stack frames. self.check_coverage("""\ def recur(n): @@ -80,7 +81,7 @@ """, [1, 2, 3, 5, 7, 8], "") - def test_long_recursion(self): + def test_long_recursion(self) -> None: # We can't finish a very deep recursion, but we don't crash. with pytest.raises(RuntimeError): with swallow_warnings("Trace function changed, data is likely wrong: None"): @@ -96,7 +97,7 @@ [1, 2, 3, 5, 7], "" ) - def test_long_recursion_recovery(self): + def test_long_recursion_recovery(self) -> None: # Test the core of bug 93: https://github.com/nedbat/coveragepy/issues/93 # When recovering from a stack overflow, the Python trace function is # disabled, but the C trace function is not. So if we're using a @@ -123,6 +124,7 @@ with swallow_warnings("Trace function changed, data is likely wrong: None"): self.start_import_stop(cov, "recur") + assert cov._collector is not None pytrace = (cov._collector.tracer_name() == "PyTracer") expected_missing = [3] if pytrace: # pragma: no metacov @@ -138,7 +140,7 @@ assert re.fullmatch( r"Trace function changed, data is likely wrong: None != " + r"<bound method PyTracer._trace of " + - "<PyTracer at 0x[0-9a-fA-F]+: 5 lines in 1 files>>", + "<PyTracer at 0x[0-9a-fA-F]+: 5 data points in 1 files>>", cov._warnings[0], ) else: @@ -154,10 +156,9 @@ It may still fail occasionally, especially on PyPy. """ - @flaky - @pytest.mark.skipif(env.JYTHON, reason="Don't bother on Jython") + @flaky # type: ignore[misc] @pytest.mark.skipif(not env.C_TRACER, reason="Only the C tracer has refcounting issues") - def test_for_leaks(self): + def test_for_leaks(self) -> None: # Our original bad memory leak only happened on line numbers > 255, so # make a code object with more lines than that. Ugly string mumbo # jumbo to get 300 blank lines at the beginning.. @@ -204,11 +205,12 @@ """Test that we properly manage the None refcount.""" @pytest.mark.skipif(not env.C_TRACER, reason="Only the C tracer has refcounting issues") - def test_dropping_none(self): # pragma: not covered + def test_dropping_none(self) -> None: # pragma: not covered # TODO: Mark this so it will only be run sometimes. pytest.skip("This is too expensive for now (30s)") # Start and stop coverage thousands of times to flush out bad # reference counting, maybe. + _ = "this is just here to put a type comment on" # type: ignore[unreachable] self.make_file("the_code.py", """\ import random def f(): @@ -235,11 +237,10 @@ assert "Fatal" not in out -@pytest.mark.skipif(env.JYTHON, reason="Pyexpat isn't a problem on Jython") class PyexpatTest(CoverageTest): """Pyexpat screws up tracing. Make sure we've counter-defended properly.""" - def test_pyexpat(self): + def test_pyexpat(self) -> None: # pyexpat calls the trace function explicitly (inexplicably), and does # it wrong for exceptions. Parsing a DOCTYPE for some reason throws # an exception internally, and triggers its wrong behavior. This test @@ -291,7 +292,7 @@ in the trace function. """ - def test_exception(self): + def test_exception(self) -> None: # Python 2.3's trace function doesn't get called with "return" if the # scope is exiting due to an exception. This confounds our trace # function which relies on scope announcements to track which files to @@ -369,8 +370,8 @@ for callnames, lines_expected in runs: # Make the list of functions we'll call for this test. - callnames = callnames.split() - calls = [getattr(sys.modules[cn], cn) for cn in callnames] + callnames_list = callnames.split() + calls = [getattr(sys.modules[cn], cn) for cn in callnames_list] cov = coverage.Coverage() cov.start() @@ -383,17 +384,9 @@ # The file names are absolute, so keep just the base. clean_lines = {} data = cov.get_data() - for callname in callnames: + for callname in callnames_list: filename = callname + ".py" - lines = data.lines(abs_file(filename)) - clean_lines[filename] = sorted(lines) - - if env.JYTHON: # pragma: only jython - # Jython doesn't report on try or except lines, so take those - # out of the expected lines. - invisible = [202, 206, 302, 304] - for lines in lines_expected.values(): - lines[:] = [l for l in lines if l not in invisible] + clean_lines[filename] = sorted_lines(data, abs_file(filename)) assert clean_lines == lines_expected @@ -401,7 +394,7 @@ class DoctestTest(CoverageTest): """Tests invoked with doctest should measure properly.""" - def test_doctest(self): + def test_doctest(self) -> None: # Doctests used to be traced, with their line numbers credited to the # file they were in. Below, one of the doctests has four lines (1-4), # which would incorrectly claim that lines 1-4 of the file were @@ -436,13 +429,13 @@ self.start_import_stop(cov, "the_doctest") data = cov.get_data() assert len(data.measured_files()) == 1 - lines = data.lines(data.measured_files().pop()) + lines = sorted_lines(data, data.measured_files().pop()) assert lines == [1, 3, 18, 19, 21, 23, 24] class GettraceTest(CoverageTest): """Tests that we work properly with `sys.gettrace()`.""" - def test_round_trip_in_untraced_function(self): + def test_round_trip_in_untraced_function(self) -> None: # https://github.com/nedbat/coveragepy/issues/575 self.make_file("main.py", """import sample""") self.make_file("sample.py", """\ @@ -475,7 +468,7 @@ assert statements == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] assert missing == [] - def test_setting_new_trace_function(self): + def test_setting_new_trace_function(self) -> None: # https://github.com/nedbat/coveragepy/issues/436 self.check_coverage('''\ import os.path @@ -493,7 +486,7 @@ t = sys.gettrace() assert t is tracer, t - def test_unsets_trace(): + def test_unsets_trace() -> None: begin() collect() @@ -507,6 +500,7 @@ missing="5-7, 13-14", ) + assert self.last_module_name is not None out = self.stdout().replace(self.last_module_name, "coverage_test") expected = ( "call: coverage_test.py @ 12\n" + @@ -518,7 +512,7 @@ @pytest.mark.expensive @pytest.mark.skipif(env.METACOV, reason="Can't set trace functions during meta-coverage") - def test_atexit_gettrace(self): + def test_atexit_gettrace(self) -> None: # This is not a test of coverage at all, but of our understanding # of this edge-case behavior in various Pythons. @@ -550,7 +544,7 @@ class ExecTest(CoverageTest): """Tests of exec.""" - def test_correct_filename(self): + def test_correct_filename(self) -> None: # https://github.com/nedbat/coveragepy/issues/380 # Bug was that exec'd files would have their lines attributed to the # calling file. Make two files, both with ~30 lines, but no lines in @@ -579,7 +573,7 @@ assert statements == [31] assert missing == [] - def test_unencodable_filename(self): + def test_unencodable_filename(self) -> None: # https://github.com/nedbat/coveragepy/issues/891 self.make_file("bug891.py", r"""exec(compile("pass", "\udcff.py", "exec"))""") cov = coverage.Coverage() @@ -596,7 +590,7 @@ https://github.com/nedbat/coveragepy/issues/416 """ - def test_os_path_exists(self): + def test_os_path_exists(self) -> None: # To see if this test still detects the problem, change isolate_module # in misc.py to simply return its argument. It should fail with a # StopIteration error. diff -Nru python-coverage-6.5.0+dfsg1/tests/test_parser.py python-coverage-7.2.7+dfsg1/tests/test_parser.py --- python-coverage-6.5.0+dfsg1/tests/test_parser.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_parser.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,18 +3,23 @@ """Tests for coverage.py's code parsing.""" +from __future__ import annotations + +import ast import os.path import textwrap import warnings +from typing import List + import pytest from coverage import env from coverage.exceptions import NotPython -from coverage.parser import ast_dump, ast_parse, PythonParser +from coverage.parser import ast_dump, PythonParser from tests.coveragetest import CoverageTest, TESTS_DIR -from tests.helpers import arcz_to_arcs, re_lines, xfail_pypy_3749 +from tests.helpers import arcz_to_arcs, re_lines, xfail_pypy38 class PythonParserTest(CoverageTest): @@ -22,14 +27,14 @@ run_in_temp_dir = False - def parse_source(self, text): + def parse_source(self, text: str) -> PythonParser: """Parse `text` as source, and return the `PythonParser` used.""" text = textwrap.dedent(text) parser = PythonParser(text=text, exclude="nocover") parser.parse_source() return parser - def test_exit_counts(self): + def test_exit_counts(self) -> None: parser = self.parse_source("""\ # check some basic branch counting class Foo: @@ -46,7 +51,7 @@ 2:1, 3:1, 4:2, 5:1, 7:1, 9:1, 10:1 } - def test_generator_exit_counts(self): + def test_generator_exit_counts(self) -> None: # https://github.com/nedbat/coveragepy/issues/324 parser = self.parse_source("""\ def gen(input): @@ -62,7 +67,7 @@ 5:1, # list -> exit } - def test_try_except(self): + def test_try_except(self) -> None: parser = self.parse_source("""\ try: a = 2 @@ -78,7 +83,7 @@ 1: 1, 2:1, 3:2, 4:1, 5:2, 6:1, 7:1, 8:1, 9:1 } - def test_excluded_classes(self): + def test_excluded_classes(self) -> None: parser = self.parse_source("""\ class Foo: def __init__(self): @@ -92,7 +97,7 @@ 1:0, 2:1, 3:1 } - def test_missing_branch_to_excluded_code(self): + def test_missing_branch_to_excluded_code(self) -> None: parser = self.parse_source("""\ if fooey: a = 2 @@ -120,10 +125,10 @@ """) assert parser.exit_counts() == { 1:1, 2:1, 3:1, 6:1 } - def test_indentation_error(self): + def test_indentation_error(self) -> None: msg = ( "Couldn't parse '<code>' as Python source: " + - "'unindent does not match any outer indentation level' at line 3" + "'unindent does not match any outer indentation level.*' at line 3" ) with pytest.raises(NotPython, match=msg): _ = self.parse_source("""\ @@ -132,15 +137,21 @@ 1 """) - def test_token_error(self): - msg = "Couldn't parse '<code>' as Python source: 'EOF in multi-line string' at line 1" + def test_token_error(self) -> None: + submsgs = [ + r"EOF in multi-line string", # before 3.12.0b1 + r"unterminated triple-quoted string literal .detected at line 1.", # after 3.12.0b1 + ] + msg = ( + r"Couldn't parse '<code>' as Python source: '" + + r"(" + "|".join(submsgs) + ")" + + r"' at line 1" + ) with pytest.raises(NotPython, match=msg): - _ = self.parse_source("""\ - ''' - """) + _ = self.parse_source("'''") - @xfail_pypy_3749 - def test_decorator_pragmas(self): + @xfail_pypy38 + def test_decorator_pragmas(self) -> None: parser = self.parse_source("""\ # 1 @@ -175,8 +186,8 @@ assert parser.raw_statements == raw_statements assert parser.statements == {8} - @xfail_pypy_3749 - def test_decorator_pragmas_with_colons(self): + @xfail_pypy38 + def test_decorator_pragmas_with_colons(self) -> None: # A colon in a decorator expression would confuse the parser, # ending the exclusion of the decorated function. parser = self.parse_source("""\ @@ -196,7 +207,7 @@ assert parser.raw_statements == raw_statements assert parser.statements == set() - def test_class_decorator_pragmas(self): + def test_class_decorator_pragmas(self) -> None: parser = self.parse_source("""\ class Foo(object): def __init__(self): @@ -210,8 +221,7 @@ assert parser.raw_statements == {1, 2, 3, 5, 6, 7, 8} assert parser.statements == {1, 2, 3} - @xfail_pypy_3749 - def test_empty_decorated_function(self): + def test_empty_decorated_function(self) -> None: parser = self.parse_source("""\ def decorator(func): return func @@ -247,10 +257,13 @@ assert expected_arcs == parser.arcs() assert expected_exits == parser.exit_counts() - def test_fuzzed_double_parse(self): + def test_fuzzed_double_parse(self) -> None: # https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=50381 # The second parse used to raise `TypeError: 'NoneType' object is not iterable` - msg = "EOF in multi-line statement" + msg = ( + r"(EOF in multi-line statement)" # before 3.12.0b1 + + r"|(unmatched ']')" # after 3.12.0b1 + ) with pytest.raises(NotPython, match=msg): self.parse_source("]") with pytest.raises(NotPython, match=msg): @@ -262,13 +275,13 @@ run_in_temp_dir = False - def parse_text(self, source): + def parse_text(self, source: str) -> PythonParser: """Parse Python source, and return the parser object.""" parser = PythonParser(text=textwrap.dedent(source)) parser.parse_source() return parser - def test_missing_arc_description(self): + def test_missing_arc_description(self) -> None: # This code is never run, so the actual values don't matter. parser = self.parse_text("""\ if x: @@ -304,7 +317,7 @@ ) assert expected == parser.missing_arc_description(11, 13) - def test_missing_arc_descriptions_for_small_callables(self): + def test_missing_arc_descriptions_for_small_callables(self) -> None: parser = self.parse_text("""\ callables = [ lambda: 2, @@ -318,12 +331,13 @@ assert expected == parser.missing_arc_description(2, -2) expected = "line 3 didn't finish the generator expression on line 3" assert expected == parser.missing_arc_description(3, -3) - expected = "line 4 didn't finish the dictionary comprehension on line 4" - assert expected == parser.missing_arc_description(4, -4) - expected = "line 5 didn't finish the set comprehension on line 5" - assert expected == parser.missing_arc_description(5, -5) + if env.PYBEHAVIOR.comprehensions_are_functions: + expected = "line 4 didn't finish the dictionary comprehension on line 4" + assert expected == parser.missing_arc_description(4, -4) + expected = "line 5 didn't finish the set comprehension on line 5" + assert expected == parser.missing_arc_description(5, -5) - def test_missing_arc_descriptions_for_exceptions(self): + def test_missing_arc_descriptions_for_exceptions(self) -> None: parser = self.parse_text("""\ try: pass @@ -343,7 +357,7 @@ ) assert expected == parser.missing_arc_description(5, 6) - def test_missing_arc_descriptions_for_finally(self): + def test_missing_arc_descriptions_for_finally(self) -> None: parser = self.parse_text("""\ def function(): for i in range(2): @@ -417,7 +431,7 @@ ) assert expected == parser.missing_arc_description(18, -1) - def test_missing_arc_descriptions_bug460(self): + def test_missing_arc_descriptions_bug460(self) -> None: parser = self.parse_text("""\ x = 1 d = { @@ -429,7 +443,7 @@ assert parser.missing_arc_description(2, -3) == "line 3 didn't finish the lambda on line 3" @pytest.mark.skipif(not env.PYBEHAVIOR.match_case, reason="Match-case is new in 3.10") - def test_match_case_with_default(self): + def test_match_case_with_default(self) -> None: parser = self.parse_text("""\ for command in ["huh", "go home", "go n"]: match command.split(): @@ -450,7 +464,7 @@ class ParserFileTest(CoverageTest): """Tests for coverage.py's code parsing from files.""" - def parse_file(self, filename): + def parse_file(self, filename: str) -> PythonParser: """Parse `text` as source, and return the `PythonParser` used.""" parser = PythonParser(filename=filename, exclude="nocover") parser.parse_source() @@ -459,7 +473,7 @@ @pytest.mark.parametrize("slug, newline", [ ("unix", "\n"), ("dos", "\r\n"), ("mac", "\r"), ]) - def test_line_endings(self, slug, newline): + def test_line_endings(self, slug: str, newline: str) -> None: text = """\ # check some basic branch counting class Foo: @@ -478,14 +492,14 @@ parser = self.parse_file(fname) assert parser.exit_counts() == counts, f"Wrong for {fname!r}" - def test_encoding(self): + def test_encoding(self) -> None: self.make_file("encoded.py", """\ coverage = "\xe7\xf6v\xear\xe3g\xe9" """) parser = self.parse_file("encoded.py") assert parser.exit_counts() == {1: 1} - def test_missing_line_ending(self): + def test_missing_line_ending(self) -> None: # Test that the set of statements is the same even if a final # multi-line statement has no final newline. # https://github.com/nedbat/coveragepy/issues/293 @@ -514,7 +528,7 @@ assert parser.statements == {1} -def test_ast_dump(): +def test_ast_dump() -> None: # Run the AST_DUMP code to make sure it doesn't fail, with some light # assertions. Use parser.py as the test code since it is the longest file, # and fitting, since it's the AST_DUMP code. @@ -529,9 +543,9 @@ num_lines = len(source.splitlines()) with warnings.catch_warnings(): # stress_phystoken.tok has deprecation warnings, suppress them. - warnings.filterwarnings("ignore", message=r".*invalid escape sequence",) - ast_root = ast_parse(source) - result = [] + warnings.filterwarnings("ignore", message=r".*invalid escape sequence") + ast_root = ast.parse(source) + result: List[str] = [] ast_dump(ast_root, print=result.append) if num_lines < 100: continue diff -Nru python-coverage-6.5.0+dfsg1/tests/test_phystokens.py python-coverage-7.2.7+dfsg1/tests/test_phystokens.py --- python-coverage-6.5.0+dfsg1/tests/test_phystokens.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_phystokens.py 2023-05-29 19:46:30.000000000 +0000 @@ -12,7 +12,6 @@ from coverage import env from coverage.phystokens import source_token_lines, source_encoding -from coverage.phystokens import neuter_encoding_declaration, compile_unicode from coverage.python import get_python_source from tests.coveragetest import CoverageTest, TESTS_DIR @@ -33,7 +32,7 @@ ('ws', ' '), ('num', '2'), ('op', ')')], ] -# Mixed-whitespace program, and its token stream. +# Mixed-white-space program, and its token stream. MIXED_WS = """\ def hello(): a="Hello world!" @@ -59,7 +58,7 @@ run_in_temp_dir = False - def check_tokenization(self, source): + def check_tokenization(self, source: str) -> None: """Tokenize `source`, then put it back together, should be the same.""" tokenized = "" for line in source_token_lines(source): @@ -72,26 +71,26 @@ tokenized = re.sub(r"(?m)[ \t]+$", "", tokenized) assert source == tokenized - def check_file_tokenization(self, fname): + def check_file_tokenization(self, fname: str) -> None: """Use the contents of `fname` for `check_tokenization`.""" self.check_tokenization(get_python_source(fname)) - def test_simple(self): + def test_simple(self) -> None: assert list(source_token_lines(SIMPLE)) == SIMPLE_TOKENS self.check_tokenization(SIMPLE) - def test_missing_final_newline(self): + def test_missing_final_newline(self) -> None: # We can tokenize source that is missing the final newline. assert list(source_token_lines(SIMPLE.rstrip())) == SIMPLE_TOKENS - def test_tab_indentation(self): + def test_tab_indentation(self) -> None: # Mixed tabs and spaces... assert list(source_token_lines(MIXED_WS)) == MIXED_WS_TOKENS - def test_bug_822(self): + def test_bug_822(self) -> None: self.check_tokenization(BUG_822) - def test_tokenize_real_file(self): + def test_tokenize_real_file(self) -> None: # Check the tokenization of a real file (large, btw). real_file = os.path.join(TESTS_DIR, "test_coverage.py") self.check_file_tokenization(real_file) @@ -100,11 +99,11 @@ "stress_phystoken.tok", "stress_phystoken_dos.tok", ]) - def test_stress(self, fname): + def test_stress(self, fname: str) -> None: # Check the tokenization of the stress-test files. # And check that those files haven't been incorrectly "fixed". with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message=r".*invalid escape sequence",) + warnings.filterwarnings("ignore", message=r".*invalid escape sequence") stress = os.path.join(TESTS_DIR, fname) self.check_file_tokenization(stress) @@ -117,7 +116,7 @@ run_in_temp_dir = False - def test_soft_keywords(self): + def test_soft_keywords(self) -> None: source = textwrap.dedent("""\ match re.match(something): case ["what"]: @@ -147,7 +146,7 @@ assert tokens[11][3] == ("nam", "case") -# The default encoding is different in Python 2 and Python 3. +# The default source file encoding. DEF_ENCODING = "utf-8" @@ -169,152 +168,40 @@ run_in_temp_dir = False - def test_detect_source_encoding(self): + def test_detect_source_encoding(self) -> None: for _, source, expected in ENCODING_DECLARATION_SOURCES: assert source_encoding(source) == expected, f"Wrong encoding in {source!r}" - def test_detect_source_encoding_not_in_comment(self): + def test_detect_source_encoding_not_in_comment(self) -> None: # Should not detect anything here source = b'def parse(src, encoding=None):\n pass' assert source_encoding(source) == DEF_ENCODING - def test_dont_detect_source_encoding_on_third_line(self): + def test_dont_detect_source_encoding_on_third_line(self) -> None: # A coding declaration doesn't count on the third line. source = b"\n\n# coding=cp850\n\n" assert source_encoding(source) == DEF_ENCODING - def test_detect_source_encoding_of_empty_file(self): + def test_detect_source_encoding_of_empty_file(self) -> None: # An important edge case. assert source_encoding(b"") == DEF_ENCODING - def test_bom(self): + def test_bom(self) -> None: # A BOM means utf-8. source = b"\xEF\xBB\xBFtext = 'hello'\n" assert source_encoding(source) == 'utf-8-sig' - def test_bom_with_encoding(self): + def test_bom_with_encoding(self) -> None: source = b"\xEF\xBB\xBF# coding: utf-8\ntext = 'hello'\n" assert source_encoding(source) == 'utf-8-sig' - def test_bom_is_wrong(self): + def test_bom_is_wrong(self) -> None: # A BOM with an explicit non-utf8 encoding is an error. source = b"\xEF\xBB\xBF# coding: cp850\n" with pytest.raises(SyntaxError, match="encoding problem: utf-8"): source_encoding(source) - def test_unknown_encoding(self): + def test_unknown_encoding(self) -> None: source = b"# coding: klingon\n" with pytest.raises(SyntaxError, match="unknown encoding: klingon"): source_encoding(source) - - -class NeuterEncodingDeclarationTest(CoverageTest): - """Tests of phystokens.neuter_encoding_declaration().""" - - run_in_temp_dir = False - - def test_neuter_encoding_declaration(self): - for lines_diff_expected, source, _ in ENCODING_DECLARATION_SOURCES: - neutered = neuter_encoding_declaration(source.decode("ascii")) - neutered = neutered.encode("ascii") - - # The neutered source should have the same number of lines. - source_lines = source.splitlines() - neutered_lines = neutered.splitlines() - assert len(source_lines) == len(neutered_lines) - - # Only one of the lines should be different. - lines_different = sum( - int(nline != sline) for nline, sline in zip(neutered_lines, source_lines) - ) - assert lines_diff_expected == lines_different - - # The neutered source will be detected as having no encoding - # declaration. - assert source_encoding(neutered) == DEF_ENCODING, f"Wrong encoding in {neutered!r}" - - def test_two_encoding_declarations(self): - input_src = textwrap.dedent("""\ - # -*- coding: ascii -*- - # -*- coding: utf-8 -*- - # -*- coding: utf-16 -*- - """) - expected_src = textwrap.dedent("""\ - # (deleted declaration) -*- - # (deleted declaration) -*- - # -*- coding: utf-16 -*- - """) - output_src = neuter_encoding_declaration(input_src) - assert expected_src == output_src - - def test_one_encoding_declaration(self): - input_src = textwrap.dedent("""\ - # -*- coding: utf-16 -*- - # Just a comment. - # -*- coding: ascii -*- - """) - expected_src = textwrap.dedent("""\ - # (deleted declaration) -*- - # Just a comment. - # -*- coding: ascii -*- - """) - output_src = neuter_encoding_declaration(input_src) - assert expected_src == output_src - - -class Bug529Test(CoverageTest): - """Test of bug 529""" - - def test_bug_529(self): - # Don't over-neuter coding declarations. This happened with a test - # file which contained code in multi-line strings, all with coding - # declarations. The neutering of the file also changed the multi-line - # strings, which it shouldn't have. - self.make_file("the_test.py", '''\ - # -*- coding: utf-8 -*- - import unittest - class Bug529Test(unittest.TestCase): - def test_two_strings_are_equal(self): - src1 = u"""\\ - # -*- coding: utf-8 -*- - # Just a comment. - """ - src2 = u"""\\ - # -*- coding: utf-8 -*- - # Just a comment. - """ - self.assertEqual(src1, src2) - - if __name__ == "__main__": - unittest.main() - ''') - status, out = self.run_command_status("coverage run the_test.py") - assert status == 0 - assert "OK" in out - # If this test fails, the output will be super-confusing, because it - # has a failing unit test contained within the failing unit test. - - -class CompileUnicodeTest(CoverageTest): - """Tests of compiling Unicode strings.""" - - run_in_temp_dir = False - - def assert_compile_unicode(self, source): - """Assert that `source` will compile properly with `compile_unicode`.""" - source += "a = 42\n" - # This doesn't raise an exception: - code = compile_unicode(source, "<string>", "exec") - globs = {} - exec(code, globs) - assert globs['a'] == 42 - - def test_cp1252(self): - uni = """# coding: cp1252\n# \u201C curly \u201D\n""" - self.assert_compile_unicode(uni) - - def test_double_coding_declaration(self): - # Build this string in a weird way so that actual vim's won't try to - # interpret it... - uni = "# -*- coding:utf-8 -*-\n# v" + "im: fileencoding=utf-8\n" - self.assert_compile_unicode(uni) diff -Nru python-coverage-6.5.0+dfsg1/tests/test_plugins.py python-coverage-7.2.7+dfsg1/tests/test_plugins.py --- python-coverage-6.5.0+dfsg1/tests/test_plugins.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_plugins.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,20 +3,25 @@ """Tests for plugins.""" +from __future__ import annotations + import inspect import io import math import os.path + +from typing import Any, Dict, List, Optional from xml.etree import ElementTree import pytest import coverage -from coverage import env +from coverage import Coverage, env from coverage.control import Plugins -from coverage.data import line_counts +from coverage.data import line_counts, sorted_lines from coverage.exceptions import CoverageWarning, NoSource, PluginError from coverage.misc import import_local_file +from coverage.types import TConfigSectionOut, TLineNo, TPluginConfig import coverage.plugin @@ -24,18 +29,24 @@ from tests.helpers import CheckUniqueFilenames, swallow_warnings -class FakeConfig: +class NullConfig(TPluginConfig): + """A plugin configure thing when we don't really need one.""" + def get_plugin_options(self, plugin: str) -> TConfigSectionOut: + return {} + + +class FakeConfig(TPluginConfig): """A fake config for use in tests.""" - def __init__(self, plugin, options): + def __init__(self, plugin: str, options: Dict[str, Any]) -> None: self.plugin = plugin self.options = options - self.asked_for = [] + self.asked_for: List[str] = [] - def get_plugin_options(self, module): - """Just return the options for `module` if this is the right module.""" - self.asked_for.append(module) - if module == self.plugin: + def get_plugin_options(self, plugin: str) -> TConfigSectionOut: + """Just return the options for `plugin` if this is the right module.""" + self.asked_for.append(plugin) + if plugin == self.plugin: return self.options else: return {} @@ -44,7 +55,7 @@ class LoadPluginsTest(CoverageTest): """Test Plugins.load_plugins directly.""" - def test_implicit_boolean(self): + def test_implicit_boolean(self) -> None: self.make_file("plugin1.py", """\ from coverage import CoveragePlugin @@ -62,7 +73,7 @@ plugins = Plugins.load_plugins(["plugin1"], config) assert plugins - def test_importing_and_configuring(self): + def test_importing_and_configuring(self) -> None: self.make_file("plugin1.py", """\ from coverage import CoveragePlugin @@ -79,11 +90,11 @@ plugins = list(Plugins.load_plugins(["plugin1"], config)) assert len(plugins) == 1 - assert plugins[0].this_is == "me" - assert plugins[0].options == {'a': 'hello'} + assert plugins[0].this_is == "me" # type: ignore + assert plugins[0].options == {'a': 'hello'} # type: ignore assert config.asked_for == ['plugin1'] - def test_importing_and_configuring_more_than_one(self): + def test_importing_and_configuring_more_than_one(self) -> None: self.make_file("plugin1.py", """\ from coverage import CoveragePlugin @@ -110,9 +121,9 @@ plugins = list(Plugins.load_plugins(["plugin1", "plugin2"], config)) assert len(plugins) == 2 - assert plugins[0].this_is == "me" - assert plugins[0].options == {'a': 'hello'} - assert plugins[1].options == {} + assert plugins[0].this_is == "me" # type: ignore + assert plugins[0].options == {'a': 'hello'} # type: ignore + assert plugins[1].options == {} # type: ignore assert config.asked_for == ['plugin1', 'plugin2'] # The order matters... @@ -120,28 +131,28 @@ plugins = list(Plugins.load_plugins(["plugin2", "plugin1"], config)) assert len(plugins) == 2 - assert plugins[0].options == {} - assert plugins[1].this_is == "me" - assert plugins[1].options == {'a': 'second'} + assert plugins[0].options == {} # type: ignore + assert plugins[1].this_is == "me" # type: ignore + assert plugins[1].options == {'a': 'second'} # type: ignore - def test_cant_import(self): + def test_cant_import(self) -> None: with pytest.raises(ImportError, match="No module named '?plugin_not_there'?"): - _ = Plugins.load_plugins(["plugin_not_there"], None) + _ = Plugins.load_plugins(["plugin_not_there"], NullConfig()) - def test_plugin_must_define_coverage_init(self): + def test_plugin_must_define_coverage_init(self) -> None: self.make_file("no_plugin.py", """\ from coverage import CoveragePlugin Nothing = 0 """) msg_pat = "Plugin module 'no_plugin' didn't define a coverage_init function" with pytest.raises(PluginError, match=msg_pat): - list(Plugins.load_plugins(["no_plugin"], None)) + list(Plugins.load_plugins(["no_plugin"], NullConfig())) class PluginTest(CoverageTest): """Test plugins through the Coverage class.""" - def test_plugin_imported(self): + def test_plugin_imported(self) -> None: # Prove that a plugin will be imported. self.make_file("my_plugin.py", """\ from coverage import CoveragePlugin @@ -162,7 +173,7 @@ with open("evidence.out") as f: assert f.read() == "we are here!" - def test_missing_plugin_raises_import_error(self): + def test_missing_plugin_raises_import_error(self) -> None: # Prove that a missing plugin will raise an ImportError. with pytest.raises(ImportError, match="No module named '?does_not_exist_woijwoicweo'?"): cov = coverage.Coverage() @@ -170,7 +181,7 @@ cov.start() cov.stop() - def test_bad_plugin_isnt_hidden(self): + def test_bad_plugin_isnt_hidden(self) -> None: # Prove that a plugin with an error in it will raise the error. self.make_file("plugin_over_zero.py", "1/0") with pytest.raises(ZeroDivisionError): @@ -179,7 +190,7 @@ cov.start() cov.stop() - def test_plugin_sys_info(self): + def test_plugin_sys_info(self) -> None: self.make_file("plugin_sys_info.py", """\ import coverage @@ -213,7 +224,7 @@ ] assert expected_end == out_lines[-len(expected_end):] - def test_plugin_with_no_sys_info(self): + def test_plugin_with_no_sys_info(self) -> None: self.make_file("plugin_no_sys_info.py", """\ import coverage @@ -239,7 +250,7 @@ ] assert expected_end == out_lines[-len(expected_end):] - def test_local_files_are_importable(self): + def test_local_files_are_importable(self) -> None: self.make_file("importing_plugin.py", """\ from coverage import CoveragePlugin import local_module @@ -264,7 +275,7 @@ @pytest.mark.skipif(env.C_TRACER, reason="This test is only about PyTracer.") class PluginWarningOnPyTracerTest(CoverageTest): """Test that we get a controlled exception with plugins on PyTracer.""" - def test_exception_if_plugins_on_pytracer(self): + def test_exception_if_plugins_on_pytracer(self) -> None: self.make_file("simple.py", "a = 1") cov = coverage.Coverage() @@ -285,7 +296,7 @@ class GoodFileTracerTest(FileTracerTest): """Tests of file tracer plugin happy paths.""" - def test_plugin1(self): + def test_plugin1(self) -> None: self.make_file("simple.py", """\ import try_xyz a = 1 @@ -311,7 +322,7 @@ _, statements, _, _ = cov.analysis(zzfile) assert statements == [105, 106, 107, 205, 206, 207] - def make_render_and_caller(self): + def make_render_and_caller(self) -> None: """Make the render.py and caller.py files we need.""" # plugin2 emulates a dynamic tracing plugin: the caller's locals # are examined to determine the source file and line number. @@ -343,21 +354,18 @@ # quux_5.html will be omitted from the results. assert render("quux_5.html", 3) == "[quux_5.html @ 3]" - - # For Python 2, make sure unicode is working. - assert render(u"uni_3.html", 2) == "[uni_3.html @ 2]" """) # will try to read the actual source files, so make some # source files. - def lines(n): + def lines(n: int) -> str: """Make a string with n lines of text.""" return "".join("line %d\n" % i for i in range(n)) self.make_file("bar_4.html", lines(4)) self.make_file("foo_7.html", lines(7)) - def test_plugin2(self): + def test_plugin2(self) -> None: self.make_render_and_caller() cov = coverage.Coverage(omit=["*quux*"]) @@ -382,12 +390,7 @@ assert "quux_5.html" not in line_counts(cov.get_data()) - _, statements, missing, _ = cov.analysis("uni_3.html") - assert statements == [1, 2, 3] - assert missing == [1] - assert "uni_3.html" in line_counts(cov.get_data()) - - def test_plugin2_with_branch(self): + def test_plugin2_with_branch(self) -> None: self.make_render_and_caller() cov = coverage.Coverage(branch=True, omit=["*quux*"]) @@ -408,7 +411,7 @@ assert analysis.missing == {1, 2, 3, 6, 7} - def test_plugin2_with_text_report(self): + def test_plugin2_with_text_report(self) -> None: self.make_render_and_caller() cov = coverage.Coverage(branch=True, omit=["*quux*"]) @@ -430,7 +433,7 @@ assert expected == report assert math.isclose(total, 4 / 11 * 100) - def test_plugin2_with_html_report(self): + def test_plugin2_with_html_report(self) -> None: self.make_render_and_caller() cov = coverage.Coverage(branch=True, omit=["*quux*"]) @@ -445,7 +448,7 @@ self.assert_exists("htmlcov/bar_4_html.html") self.assert_exists("htmlcov/foo_7_html.html") - def test_plugin2_with_xml_report(self): + def test_plugin2_with_xml_report(self) -> None: self.make_render_and_caller() cov = coverage.Coverage(branch=True, omit=["*quux*"]) @@ -476,7 +479,7 @@ 'name': 'foo_7.html', } - def test_defer_to_python(self): + def test_defer_to_python(self) -> None: # A plugin that measures, but then wants built-in python reporting. self.make_file("fairly_odd_plugin.py", """\ # A plugin that claims all the odd lines are executed, and none of @@ -529,7 +532,7 @@ assert expected == report assert total == 50 - def test_find_unexecuted(self): + def test_find_unexecuted(self) -> None: self.make_file("unexecuted_plugin.py", """\ import os import coverage.plugin @@ -585,7 +588,7 @@ class BadFileTracerTest(FileTracerTest): """Test error handling around file tracer plugins.""" - def run_plugin(self, module_name): + def run_plugin(self, module_name: str) -> Coverage: """Run a plugin with the given module_name. Uses a few fixed Python files. @@ -617,7 +620,14 @@ cov.save() # pytest-cov does a save after stop, so we'll do it too. return cov - def run_bad_plugin(self, module_name, plugin_name, our_error=True, excmsg=None, excmsgs=None): + def run_bad_plugin( + self, + module_name: str, + plugin_name: str, + our_error: bool = True, + excmsg: Optional[str] = None, + excmsgs: Optional[List[str]] = None, + ) -> None: """Run a file, and see that the plugin failed. `module_name` and `plugin_name` is the module and name of the plugin to @@ -639,7 +649,7 @@ self.run_plugin(module_name) stderr = self.stderr() - stderr += "".join(w.message.args[0] for w in warns) + stderr += "".join(str(w.message) for w in warns) if our_error: # The exception we're causing should only appear once. assert stderr.count("# Oh noes!") == 1 @@ -650,9 +660,9 @@ # or: # Disabling plug-in '...' due to an exception: print([str(w) for w in warns.list]) - warns = [w for w in warns.list if issubclass(w.category, CoverageWarning)] - assert len(warns) == 1 - warnmsg = warns[0].message.args[0] + warnings = [w for w in warns.list if issubclass(w.category, CoverageWarning)] + assert len(warnings) == 1 + warnmsg = str(warnings[0].message) assert f"Disabling plug-in '{module_name}.{plugin_name}' due to " in warnmsg if excmsg: @@ -661,7 +671,7 @@ found_exc = any(em in stderr for em in excmsgs) # pragma: part covered assert found_exc, f"expected one of {excmsgs} in stderr" - def test_file_tracer_has_no_file_tracer_method(self): + def test_file_tracer_has_no_file_tracer_method(self) -> None: self.make_file("bad_plugin.py", """\ class Plugin(object): pass @@ -671,7 +681,7 @@ """) self.run_bad_plugin("bad_plugin", "Plugin", our_error=False) - def test_file_tracer_has_inherited_sourcefilename_method(self): + def test_file_tracer_has_inherited_sourcefilename_method(self) -> None: self.make_file("bad_plugin.py", """\ import coverage class Plugin(coverage.CoveragePlugin): @@ -690,7 +700,7 @@ excmsg="Class 'bad_plugin.FileTracer' needs to implement source_filename()", ) - def test_plugin_has_inherited_filereporter_method(self): + def test_plugin_has_inherited_filereporter_method(self) -> None: self.make_file("bad_plugin.py", """\ import coverage class Plugin(coverage.CoveragePlugin): @@ -710,7 +720,7 @@ with pytest.raises(NotImplementedError, match=expected_msg): cov.report() - def test_file_tracer_fails(self): + def test_file_tracer_fails(self) -> None: self.make_file("bad_plugin.py", """\ import coverage.plugin class Plugin(coverage.plugin.CoveragePlugin): @@ -722,7 +732,7 @@ """) self.run_bad_plugin("bad_plugin", "Plugin") - def test_file_tracer_fails_eventually(self): + def test_file_tracer_fails_eventually(self) -> None: # Django coverage plugin can report on a few files and then fail. # https://github.com/nedbat/coveragepy/issues/1011 self.make_file("bad_plugin.py", """\ @@ -753,7 +763,7 @@ """) self.run_bad_plugin("bad_plugin", "Plugin") - def test_file_tracer_returns_wrong(self): + def test_file_tracer_returns_wrong(self) -> None: self.make_file("bad_plugin.py", """\ import coverage.plugin class Plugin(coverage.plugin.CoveragePlugin): @@ -767,7 +777,7 @@ "bad_plugin", "Plugin", our_error=False, excmsg="'float' object has no attribute", ) - def test_has_dynamic_source_filename_fails(self): + def test_has_dynamic_source_filename_fails(self) -> None: self.make_file("bad_plugin.py", """\ import coverage.plugin class Plugin(coverage.plugin.CoveragePlugin): @@ -783,7 +793,7 @@ """) self.run_bad_plugin("bad_plugin", "Plugin") - def test_source_filename_fails(self): + def test_source_filename_fails(self) -> None: self.make_file("bad_plugin.py", """\ import coverage.plugin class Plugin(coverage.plugin.CoveragePlugin): @@ -799,7 +809,7 @@ """) self.run_bad_plugin("bad_plugin", "Plugin") - def test_source_filename_returns_wrong(self): + def test_source_filename_returns_wrong(self) -> None: self.make_file("bad_plugin.py", """\ import coverage.plugin class Plugin(coverage.plugin.CoveragePlugin): @@ -823,7 +833,7 @@ ], ) - def test_dynamic_source_filename_fails(self): + def test_dynamic_source_filename_fails(self) -> None: self.make_file("bad_plugin.py", """\ import coverage.plugin class Plugin(coverage.plugin.CoveragePlugin): @@ -842,7 +852,7 @@ """) self.run_bad_plugin("bad_plugin", "Plugin") - def test_line_number_range_raises_error(self): + def test_line_number_range_raises_error(self) -> None: self.make_file("bad_plugin.py", """\ import coverage.plugin class Plugin(coverage.plugin.CoveragePlugin): @@ -864,7 +874,7 @@ "bad_plugin", "Plugin", our_error=False, excmsg="borked!", ) - def test_line_number_range_returns_non_tuple(self): + def test_line_number_range_returns_non_tuple(self) -> None: self.make_file("bad_plugin.py", """\ import coverage.plugin class Plugin(coverage.plugin.CoveragePlugin): @@ -886,7 +896,7 @@ "bad_plugin", "Plugin", our_error=False, excmsg="line_number_range must return 2-tuple", ) - def test_line_number_range_returns_triple(self): + def test_line_number_range_returns_triple(self) -> None: self.make_file("bad_plugin.py", """\ import coverage.plugin class Plugin(coverage.plugin.CoveragePlugin): @@ -908,7 +918,7 @@ "bad_plugin", "Plugin", our_error=False, excmsg="line_number_range must return 2-tuple", ) - def test_line_number_range_returns_pair_of_strings(self): + def test_line_number_range_returns_pair_of_strings(self) -> None: self.make_file("bad_plugin.py", """\ import coverage.plugin class Plugin(coverage.plugin.CoveragePlugin): @@ -940,12 +950,13 @@ run_in_temp_dir = False - def test_configurer_plugin(self): + def test_configurer_plugin(self) -> None: cov = coverage.Coverage() cov.set_option("run:plugins", ["tests.plugin_config"]) cov.start() cov.stop() # pragma: nested excluded = cov.get_option("report:exclude_lines") + assert isinstance(excluded, list) assert "pragma: custom" in excluded assert "pragma: or whatever" in excluded @@ -953,7 +964,7 @@ class DynamicContextPluginTest(CoverageTest): """Tests of plugins that implement `dynamic_context`.""" - def make_plugin_capitalized_testnames(self, filename): + def make_plugin_capitalized_testnames(self, filename: str) -> None: """Create a dynamic context plugin that capitalizes the part after 'test_'.""" self.make_file(filename, """\ from coverage import CoveragePlugin @@ -970,7 +981,7 @@ reg.add_dynamic_context(Plugin()) """) - def make_plugin_track_render(self, filename): + def make_plugin_track_render(self, filename: str) -> None: """Make a dynamic context plugin that tracks 'render_' functions.""" self.make_file(filename, """\ from coverage import CoveragePlugin @@ -986,7 +997,7 @@ reg.add_dynamic_context(Plugin()) """) - def make_test_files(self): + def make_test_files(self) -> None: """Make some files to use while testing dynamic context plugins.""" self.make_file("rendering.py", """\ def html_tag(tag, content): @@ -1005,7 +1016,7 @@ self.make_file("testsuite.py", """\ import rendering - def test_html_tag(): + def test_html_tag() -> None: assert rendering.html_tag('b', 'hello') == '<b>hello</b>' def doctest_html_tag(): @@ -1013,7 +1024,7 @@ rendering.html_tag('i', 'text') == '<i>text</i>' '''.strip()) - def test_renderers(): + def test_renderers() -> None: assert rendering.render_paragraph('hello') == '<p>hello</p>' assert rendering.render_bold('wide') == '<b>wide</b>' assert rendering.render_span('world') == '<span>world</span>' @@ -1025,7 +1036,7 @@ return html """) - def run_all_functions(self, cov, suite_name): # pragma: nested + def run_all_functions(self, cov: Coverage, suite_name: str) -> None: # pragma: nested """Run all functions in `suite_name` under coverage.""" cov.start() suite = import_local_file(suite_name) @@ -1038,7 +1049,7 @@ finally: cov.stop() - def test_plugin_standalone(self): + def test_plugin_standalone(self) -> None: self.make_plugin_capitalized_testnames('plugin_tests.py') self.make_test_files() @@ -1055,13 +1066,13 @@ expected = ['', 'doctest:HTML_TAG', 'test:HTML_TAG', 'test:RENDERERS'] assert expected == sorted(data.measured_contexts()) data.set_query_context("doctest:HTML_TAG") - assert [2] == data.lines(filenames['rendering.py']) + assert [2] == sorted_lines(data, filenames['rendering.py']) data.set_query_context("test:HTML_TAG") - assert [2] == data.lines(filenames['rendering.py']) + assert [2] == sorted_lines(data, filenames['rendering.py']) data.set_query_context("test:RENDERERS") - assert [2, 5, 8, 11] == sorted(data.lines(filenames['rendering.py'])) + assert [2, 5, 8, 11] == sorted_lines(data, filenames['rendering.py']) - def test_static_context(self): + def test_static_context(self) -> None: self.make_plugin_capitalized_testnames('plugin_tests.py') self.make_test_files() @@ -1082,7 +1093,7 @@ ] assert expected == sorted(data.measured_contexts()) - def test_plugin_with_test_function(self): + def test_plugin_with_test_function(self) -> None: self.make_plugin_capitalized_testnames('plugin_tests.py') self.make_test_files() @@ -1107,15 +1118,15 @@ ] assert expected == sorted(data.measured_contexts()) - def assert_context_lines(context, lines): + def assert_context_lines(context: str, lines: List[TLineNo]) -> None: data.set_query_context(context) - assert lines == sorted(data.lines(filenames['rendering.py'])) + assert lines == sorted_lines(data, filenames['rendering.py']) assert_context_lines("doctest:HTML_TAG", [2]) assert_context_lines("testsuite.test_html_tag", [2]) assert_context_lines("testsuite.test_renderers", [2, 5, 8, 11]) - def test_multiple_plugins(self): + def test_multiple_plugins(self) -> None: self.make_plugin_capitalized_testnames('plugin_tests.py') self.make_plugin_track_render('plugin_renderers.py') self.make_test_files() @@ -1145,9 +1156,9 @@ ] assert expected == sorted(data.measured_contexts()) - def assert_context_lines(context, lines): + def assert_context_lines(context: str, lines: List[TLineNo]) -> None: data.set_query_context(context) - assert lines == sorted(data.lines(filenames['rendering.py'])) + assert lines == sorted_lines(data, filenames['rendering.py']) assert_context_lines("test:HTML_TAG", [2]) assert_context_lines("test:RENDERERS", [2, 5, 8, 11]) diff -Nru python-coverage-6.5.0+dfsg1/tests/test_process.py python-coverage-7.2.7+dfsg1/tests/test_process.py --- python-coverage-6.5.0+dfsg1/tests/test_process.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_process.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,14 +3,19 @@ """Tests for process behavior of coverage.py.""" +from __future__ import annotations + import glob import os import os.path +import platform import re import stat import sys import textwrap +from typing import Any + import pytest import coverage @@ -25,7 +30,7 @@ class ProcessTest(CoverageTest): """Tests of the per-process behavior of coverage.py.""" - def test_save_on_exit(self): + def test_save_on_exit(self) -> None: self.make_file("mycode.py", """\ h = "Hello" w = "world" @@ -35,7 +40,7 @@ self.run_command("coverage run mycode.py") self.assert_exists(".coverage") - def test_tests_dir_is_importable(self): + def test_tests_dir_is_importable(self) -> None: # Checks that we can import modules from the tests directory at all! self.make_file("mycode.py", """\ import covmod1 @@ -49,7 +54,7 @@ self.assert_exists(".coverage") assert out == 'done\n' - def test_coverage_run_envvar_is_in_coveragerun(self): + def test_coverage_run_envvar_is_in_coveragerun(self) -> None: # Test that we are setting COVERAGE_RUN when we run. self.make_file("envornot.py", """\ import os @@ -64,7 +69,7 @@ out = self.run_command("coverage run envornot.py") assert out == "true\n" - def make_b_or_c_py(self): + def make_b_or_c_py(self) -> None: """Create b_or_c.py, used in a few of these tests.""" # "b_or_c.py b" will run 6 lines. # "b_or_c.py c" will run 7 lines. @@ -81,7 +86,7 @@ print('done') """) - def test_append_data(self): + def test_append_data(self) -> None: self.make_b_or_c_py() out = self.run_command("coverage run b_or_c.py b") @@ -100,7 +105,7 @@ data.read() assert line_counts(data)['b_or_c.py'] == 8 - def test_append_data_with_different_file(self): + def test_append_data_with_different_file(self) -> None: self.make_b_or_c_py() self.make_file(".coveragerc", """\ @@ -124,7 +129,7 @@ data.read() assert line_counts(data)['b_or_c.py'] == 8 - def test_append_can_create_a_data_file(self): + def test_append_can_create_a_data_file(self) -> None: self.make_b_or_c_py() out = self.run_command("coverage run --append b_or_c.py b") @@ -138,7 +143,7 @@ data.read() assert line_counts(data)['b_or_c.py'] == 6 - def test_combine_with_rc(self): + def test_combine_with_rc(self) -> None: self.make_b_or_c_py() self.make_file(".coveragerc", """\ @@ -182,7 +187,7 @@ TOTAL 8 0 100% """) - def test_combine_with_aliases(self): + def test_combine_with_aliases(self) -> None: self.make_file("d1/x.py", """\ a = 1 b = 2 @@ -217,6 +222,8 @@ self.assert_file_count(".coverage.*", 2) + self.make_file("src/x.py", "") + self.run_command("coverage combine") self.assert_exists(".coverage") @@ -234,7 +241,7 @@ assert expected == actual assert list(summary.values())[0] == 6 - def test_erase_parallel(self): + def test_erase_parallel(self) -> None: self.make_file(".coveragerc", """\ [run] data_file = data.dat @@ -251,7 +258,7 @@ self.assert_doesnt_exist("data.dat.gooey") self.assert_exists(".coverage") - def test_missing_source_file(self): + def test_missing_source_file(self) -> None: # Check what happens if the source is missing when reporting happens. self.make_file("fleeting.py", """\ s = 'goodbye, cruel world!' @@ -276,14 +283,14 @@ assert "Traceback" not in out assert status == 1 - def test_running_missing_file(self): + def test_running_missing_file(self) -> None: status, out = self.run_command_status("coverage run xyzzy.py") assert re.search("No file to run: .*xyzzy.py", out) assert "raceback" not in out assert "rror" not in out assert status == 1 - def test_code_throws(self): + def test_code_throws(self) -> None: self.make_file("throw.py", """\ class MyException(Exception): pass @@ -313,7 +320,7 @@ assert 'raise MyException("hey!")' in out assert status == 1 - def test_code_exits(self): + def test_code_exits(self) -> None: self.make_file("exit.py", """\ import sys def f1(): @@ -335,7 +342,7 @@ assert status == status2 assert status == 17 - def test_code_exits_no_arg(self): + def test_code_exits_no_arg(self) -> None: self.make_file("exit_none.py", """\ import sys def f1(): @@ -352,7 +359,7 @@ assert status == 0 @pytest.mark.skipif(not hasattr(os, "fork"), reason="Can't test os.fork, it doesn't exist.") - def test_fork(self): + def test_fork(self) -> None: self.make_file("fork.py", """\ import os @@ -395,7 +402,7 @@ data.read() assert line_counts(data)['fork.py'] == 9 - def test_warnings_during_reporting(self): + def test_warnings_during_reporting(self) -> None: # While fixing issue #224, the warnings were being printed far too # often. Make sure they're not any more. self.make_file("hello.py", """\ @@ -416,7 +423,7 @@ out = self.run_command("coverage html") assert out.count("Module xyzzy was never imported.") == 0 - def test_warns_if_never_run(self): + def test_warns_if_never_run(self) -> None: # Note: the name of the function can't have "warning" in it, or the # absolute path of the file will have "warning" in it, and an assertion # will fail. @@ -435,7 +442,7 @@ assert "Exception" not in out @pytest.mark.skipif(env.METACOV, reason="Can't test tracers changing during metacoverage") - def test_warnings_trace_function_changed_with_threads(self): + def test_warnings_trace_function_changed_with_threads(self) -> None: # https://github.com/nedbat/coveragepy/issues/164 self.make_file("bug164.py", """\ @@ -455,7 +462,7 @@ assert "Hello\n" in out assert "warning" not in out - def test_warning_trace_function_changed(self): + def test_warning_trace_function_changed(self) -> None: self.make_file("settrace.py", """\ import sys print("Hello") @@ -471,7 +478,7 @@ # When meta-coverage testing, this test doesn't work, because it finds # coverage.py's own trace function. @pytest.mark.skipif(env.METACOV, reason="Can't test timid during coverage measurement.") - def test_timid(self): + def test_timid(self) -> None: # Test that the --timid command line argument properly swaps the tracer # function for a simpler one. # @@ -525,7 +532,7 @@ timid_out = self.run_command("coverage run --timid showtrace.py") assert timid_out == "PyTracer\n" - def test_warn_preimported(self): + def test_warn_preimported(self) -> None: self.make_file("hello.py", """\ import goodbye import coverage @@ -552,7 +559,7 @@ @pytest.mark.expensive @pytest.mark.skipif(not env.C_TRACER, reason="fullcoverage only works with the C tracer.") @pytest.mark.skipif(env.METACOV, reason="Can't test fullcoverage when measuring ourselves") - def test_fullcoverage(self): + def test_fullcoverage(self) -> None: # fullcoverage is a trick to get stdlib modules measured from # the very beginning of the process. Here we import os and # then check how many lines are measured. @@ -576,9 +583,7 @@ # Pypy passes locally, but fails in CI? Perhaps the version of macOS is # significant? https://foss.heptapod.net/pypy/pypy/-/issues/3074 @pytest.mark.skipif(env.PYPY, reason="PyPy is unreliable with this test") - # Jython as of 2.7.1rc3 won't compile a filename that isn't utf-8. - @pytest.mark.skipif(env.JYTHON, reason="Jython can't handle this test") - def test_lang_c(self): + def test_lang_c(self) -> None: # LANG=C forces getfilesystemencoding on Linux to 'ascii', which causes # failures with non-ascii file names. We don't want to make a real file # with strange characters, though, because that gets the test runners @@ -595,7 +600,7 @@ out = self.run_command("coverage run weird_file.py") assert out == "1\n2\n" - def test_deprecation_warnings(self): + def test_deprecation_warnings(self) -> None: # Test that coverage doesn't trigger deprecation warnings. # https://github.com/nedbat/coveragepy/issues/305 self.make_file("allok.py", """\ @@ -612,7 +617,7 @@ out = self.run_command("python allok.py") assert out == "No warnings!\n" - def test_run_twice(self): + def test_run_twice(self) -> None: # https://github.com/nedbat/coveragepy/issues/353 self.make_file("foo.py", """\ def foo(): @@ -643,7 +648,7 @@ ) assert expected == out - def test_module_name(self): + def test_module_name(self) -> None: # https://github.com/nedbat/coveragepy/issues/478 # Make sure help doesn't show a silly command name when run as a # module, like it used to: @@ -658,7 +663,7 @@ class EnvironmentTest(CoverageTest): """Tests using try_execfile.py to test the execution environment.""" - def assert_tryexecfile_output(self, expected, actual): + def assert_tryexecfile_output(self, expected: str, actual: str) -> None: """Assert that the output we got is a successful run of try_execfile.py. `expected` and `actual` must be the same, modulo a few slight known @@ -667,35 +672,29 @@ """ # First, is this even credible try_execfile.py output? assert '"DATA": "xyzzy"' in actual - - if env.JYTHON: # pragma: only jython - # Argv0 is different for Jython, remove that from the comparison. - expected = re_lines_text(r'\s+"argv0":', expected, match=False) - actual = re_lines_text(r'\s+"argv0":', actual, match=False) - assert actual == expected - def test_coverage_run_is_like_python(self): + def test_coverage_run_is_like_python(self) -> None: with open(TRY_EXECFILE) as f: self.make_file("run_me.py", f.read()) expected = self.run_command("python run_me.py") actual = self.run_command("coverage run run_me.py") self.assert_tryexecfile_output(expected, actual) - def test_coverage_run_far_away_is_like_python(self): + def test_coverage_run_far_away_is_like_python(self) -> None: with open(TRY_EXECFILE) as f: self.make_file("sub/overthere/prog.py", f.read()) expected = self.run_command("python sub/overthere/prog.py") actual = self.run_command("coverage run sub/overthere/prog.py") self.assert_tryexecfile_output(expected, actual) - def test_coverage_run_dashm_is_like_python_dashm(self): + def test_coverage_run_dashm_is_like_python_dashm(self) -> None: # These -m commands assume the coverage tree is on the path. expected = self.run_command("python -m process_test.try_execfile") actual = self.run_command("coverage run -m process_test.try_execfile") self.assert_tryexecfile_output(expected, actual) - def test_coverage_run_dir_is_like_python_dir(self): + def test_coverage_run_dir_is_like_python_dir(self) -> None: with open(TRY_EXECFILE) as f: self.make_file("with_main/__main__.py", f.read()) @@ -703,7 +702,7 @@ actual = self.run_command("coverage run with_main") self.assert_tryexecfile_output(expected, actual) - def test_coverage_run_dashm_dir_no_init_is_like_python(self): + def test_coverage_run_dashm_dir_no_init_is_like_python(self) -> None: with open(TRY_EXECFILE) as f: self.make_file("with_main/__main__.py", f.read()) @@ -711,7 +710,7 @@ actual = self.run_command("coverage run -m with_main") self.assert_tryexecfile_output(expected, actual) - def test_coverage_run_dashm_dir_with_init_is_like_python(self): + def test_coverage_run_dashm_dir_with_init_is_like_python(self) -> None: with open(TRY_EXECFILE) as f: self.make_file("with_main/__main__.py", f.read()) self.make_file("with_main/__init__.py", "") @@ -720,7 +719,7 @@ actual = self.run_command("coverage run -m with_main") self.assert_tryexecfile_output(expected, actual) - def test_coverage_run_dashm_equal_to_doubledashsource(self): + def test_coverage_run_dashm_equal_to_doubledashsource(self) -> None: """regression test for #328 When imported by -m, a module's __name__ is __main__, but we need the @@ -733,7 +732,7 @@ ) self.assert_tryexecfile_output(expected, actual) - def test_coverage_run_dashm_superset_of_doubledashsource(self): + def test_coverage_run_dashm_superset_of_doubledashsource(self) -> None: """Edge case: --source foo -m foo.bar""" # Ugh: without this config file, we'll get a warning about # CoverageWarning: Module process_test was previously imported, @@ -757,7 +756,7 @@ assert st == 0 assert self.line_count(out) == 6, out - def test_coverage_run_script_imports_doubledashsource(self): + def test_coverage_run_script_imports_doubledashsource(self) -> None: # This file imports try_execfile, which compiles it to .pyc, so the # first run will have __file__ == "try_execfile.py" and the second will # have __file__ == "try_execfile.pyc", which throws off the comparison. @@ -776,7 +775,7 @@ assert st == 0 assert self.line_count(out) == 6, out - def test_coverage_run_dashm_is_like_python_dashm_off_path(self): + def test_coverage_run_dashm_is_like_python_dashm_off_path(self) -> None: # https://github.com/nedbat/coveragepy/issues/242 self.make_file("sub/__init__.py", "") with open(TRY_EXECFILE) as f: @@ -786,7 +785,7 @@ actual = self.run_command("coverage run -m sub.run_me") self.assert_tryexecfile_output(expected, actual) - def test_coverage_run_dashm_is_like_python_dashm_with__main__207(self): + def test_coverage_run_dashm_is_like_python_dashm_with__main__207(self) -> None: # https://github.com/nedbat/coveragepy/issues/207 self.make_file("package/__init__.py", "print('init')") self.make_file("package/__main__.py", "print('main')") @@ -794,7 +793,7 @@ actual = self.run_command("coverage run -m package") assert expected == actual - def test_coverage_zip_is_like_python(self): + def test_coverage_zip_is_like_python(self) -> None: # Test running coverage from a zip file itself. Some environments # (windows?) zip up the coverage main to be used as the coverage # command. @@ -805,7 +804,7 @@ actual = self.run_command(f"python {cov_main} run run_me.py") self.assert_tryexecfile_output(expected, actual) - def test_coverage_custom_script(self): + def test_coverage_custom_script(self) -> None: # https://github.com/nedbat/coveragepy/issues/678 # If sys.path[0] isn't the Python default, then coverage.py won't # fiddle with it. @@ -839,7 +838,13 @@ assert "hello-xyzzy" in out @pytest.mark.skipif(env.WINDOWS, reason="Windows can't make symlinks") - def test_bug_862(self): + @pytest.mark.skipif( + platform.python_version().endswith("+"), + reason="setuptools barfs on dev versions: https://github.com/pypa/packaging/issues/678" + # https://github.com/nedbat/coveragepy/issues/1556 + # TODO: get rid of pkg_resources + ) + def test_bug_862(self) -> None: # This simulates how pyenv and pyenv-virtualenv end up creating the # coverage executable. self.make_file("elsewhere/bin/fake-coverage", """\ @@ -854,7 +859,7 @@ out = self.run_command("somewhere/bin/fake-coverage run bar.py") assert "inside foo\n" == out - def test_bug_909(self): + def test_bug_909(self) -> None: # https://github.com/nedbat/coveragepy/issues/909 # The __init__ files were being imported before measurement started, # so the line in __init__.py was being marked as missed, and there were @@ -888,7 +893,7 @@ # TODO: do we need these as process tests if we have test_execfile.py:RunFileTest? - def test_excepthook(self): + def test_excepthook(self) -> None: self.make_file("excepthook.py", """\ import sys @@ -904,10 +909,8 @@ """) cov_st, cov_out = self.run_command_status("coverage run excepthook.py") py_st, py_out = self.run_command_status("python excepthook.py") - if not env.JYTHON: - assert cov_st == py_st - assert cov_st == 1 - + assert cov_st == py_st + assert cov_st == 1 assert "in excepthook" in py_out assert cov_out == py_out @@ -920,7 +923,7 @@ @pytest.mark.skipif(not env.CPYTHON, reason="non-CPython handles excepthook exits differently, punt for now." ) - def test_excepthook_exit(self): + def test_excepthook_exit(self) -> None: self.make_file("excepthook_exit.py", """\ import sys @@ -941,7 +944,7 @@ assert cov_out == py_out @pytest.mark.skipif(env.PYPY, reason="PyPy handles excepthook throws differently.") - def test_excepthook_throw(self): + def test_excepthook_throw(self) -> None: self.make_file("excepthook_throw.py", """\ import sys @@ -958,34 +961,31 @@ """) cov_st, cov_out = self.run_command_status("coverage run excepthook_throw.py") py_st, py_out = self.run_command_status("python excepthook_throw.py") - if not env.JYTHON: - assert cov_st == py_st - assert cov_st == 1 - + assert cov_st == py_st + assert cov_st == 1 assert "in excepthook" in py_out assert cov_out == py_out -@pytest.mark.skipif(env.JYTHON, reason="Coverage command names don't work on Jython") class AliasedCommandTest(CoverageTest): """Tests of the version-specific command aliases.""" run_in_temp_dir = False - def test_major_version_works(self): + def test_major_version_works(self) -> None: # "coverage3" works on py3 cmd = "coverage%d" % sys.version_info[0] out = self.run_command(cmd) assert "Code coverage for Python" in out - def test_wrong_alias_doesnt_work(self): + def test_wrong_alias_doesnt_work(self) -> None: # "coverage2" doesn't work on py3 assert sys.version_info[0] in [2, 3] # Let us know when Python 4 is out... badcmd = "coverage%d" % (5 - sys.version_info[0]) out = self.run_command(badcmd) assert "Code coverage for Python" not in out - def test_specific_alias_works(self): + def test_specific_alias_works(self) -> None: # "coverage-3.9" works on py3.9 cmd = "coverage-%d.%d" % sys.version_info[:2] out = self.run_command(cmd) @@ -996,7 +996,7 @@ "coverage%d" % sys.version_info[0], "coverage-%d.%d" % sys.version_info[:2], ]) - def test_aliases_used_in_messages(self, cmd): + def test_aliases_used_in_messages(self, cmd: str) -> None: out = self.run_command(f"{cmd} foobar") assert "Unknown command: 'foobar'" in out assert f"Use '{cmd} help' for help" in out @@ -1007,7 +1007,7 @@ run_in_temp_dir = False - def assert_pydoc_ok(self, name, thing): + def assert_pydoc_ok(self, name: str, thing: Any) -> None: """Check that pydoc of `name` finds the docstring from `thing`.""" # Run pydoc. out = self.run_command("python -m pydoc " + name) @@ -1019,17 +1019,17 @@ for line in thing.__doc__.splitlines(): assert line.strip() in out - def test_pydoc_coverage(self): + def test_pydoc_coverage(self) -> None: self.assert_pydoc_ok("coverage", coverage) - def test_pydoc_coverage_coverage(self): + def test_pydoc_coverage_coverage(self) -> None: self.assert_pydoc_ok("coverage.Coverage", coverage.Coverage) class FailUnderTest(CoverageTest): """Tests of the --fail-under switch.""" - def setUp(self): + def setUp(self) -> None: super().setUp() self.make_file("forty_two_plus.py", """\ # I have 42.857% (3/7) coverage! @@ -1043,25 +1043,25 @@ """) self.make_data_file(lines={abs_file("forty_two_plus.py"): [2, 3, 4]}) - def test_report_43_is_ok(self): + def test_report_43_is_ok(self) -> None: st, out = self.run_command_status("coverage report --fail-under=43") assert st == 0 assert self.last_line_squeezed(out) == "TOTAL 7 4 43%" - def test_report_43_is_not_ok(self): + def test_report_43_is_not_ok(self) -> None: st, out = self.run_command_status("coverage report --fail-under=44") assert st == 2 expected = "Coverage failure: total of 43 is less than fail-under=44" assert expected == self.last_line_squeezed(out) - def test_report_42p86_is_not_ok(self): + def test_report_42p86_is_not_ok(self) -> None: self.make_file(".coveragerc", "[report]\nprecision = 2") st, out = self.run_command_status("coverage report --fail-under=42.88") assert st == 2 expected = "Coverage failure: total of 42.86 is less than fail-under=42.88" assert expected == self.last_line_squeezed(out) - def test_report_99p9_is_not_ok(self): + def test_report_99p9_is_not_ok(self) -> None: # A file with 99.9% coverage: self.make_file("ninety_nine_plus.py", "a = 1\n" + @@ -1078,7 +1078,7 @@ class FailUnderNoFilesTest(CoverageTest): """Test that nothing to report results in an error exit status.""" - def test_report(self): + def test_report(self) -> None: self.make_file(".coveragerc", "[report]\nfail_under = 99\n") st, out = self.run_command_status("coverage report") assert 'No data to report.' in out @@ -1087,13 +1087,14 @@ class FailUnderEmptyFilesTest(CoverageTest): """Test that empty files produce the proper fail_under exit status.""" - def test_report(self): + def test_report(self) -> None: self.make_file(".coveragerc", "[report]\nfail_under = 99\n") self.make_file("empty.py", "") st, _ = self.run_command_status("coverage run empty.py") assert st == 0 st, _ = self.run_command_status("coverage report") - assert st == 2 + # An empty file is marked as 100% covered, so this is ok. + assert st == 0 @pytest.mark.skipif(env.WINDOWS, reason="Windows can't delete the directory in use.") @@ -1111,12 +1112,12 @@ print(sys.argv[1]) """ - def test_removing_directory(self): + def test_removing_directory(self) -> None: self.make_file("bug806.py", self.BUG_806) out = self.run_command("coverage run bug806.py noerror") assert out == "noerror\n" - def test_removing_directory_with_error(self): + def test_removing_directory_with_error(self) -> None: self.make_file("bug806.py", self.BUG_806) out = self.run_command("coverage run bug806.py") path = python_reported_file('bug806.py') @@ -1135,7 +1136,7 @@ class ProcessStartupTest(CoverageTest): """Test that we can measure coverage in sub-processes.""" - def setUp(self): + def setUp(self) -> None: super().setUp() # Main will run sub.py @@ -1151,7 +1152,7 @@ f.close() """) - def test_subprocess_with_pth_files(self): + def test_subprocess_with_pth_files(self) -> None: # An existing data file should not be read when a subprocess gets # measured automatically. Create the data file here with bogus data in # it. @@ -1175,7 +1176,7 @@ data.read() assert line_counts(data)['sub.py'] == 3 - def test_subprocess_with_pth_files_and_parallel(self): + def test_subprocess_with_pth_files_and_parallel(self) -> None: # https://github.com/nedbat/coveragepy/issues/492 self.make_file("coverage.ini", """\ [run] @@ -1222,7 +1223,7 @@ @pytest.mark.parametrize("dashm", ["-m", ""]) @pytest.mark.parametrize("package", ["pkg", ""]) @pytest.mark.parametrize("source", ["main", "sub"]) - def test_pth_and_source_work_together(self, dashm, package, source): + def test_pth_and_source_work_together(self, dashm: str, package: str, source: str) -> None: """Run the test for a particular combination of factors. The arguments are all strings: @@ -1237,14 +1238,14 @@ ``--source`` argument. """ - def fullname(modname): + def fullname(modname: str) -> str: """What is the full module name for `modname` for this test?""" if package and dashm: return '.'.join((package, modname)) else: return modname - def path(basename): + def path(basename: str) -> str: """Where should `basename` be created for this test?""" return os.path.join(package, basename) @@ -1258,7 +1259,6 @@ self.make_file(path("__init__.py"), "") # sub.py will write a few lines. self.make_file(path("sub.py"), """\ - # Avoid 'with' so Jython can play along. f = open("out.txt", "w") f.write("Hello, world!") f.close() diff -Nru python-coverage-6.5.0+dfsg1/tests/test_python.py python-coverage-7.2.7+dfsg1/tests/test_python.py --- python-coverage-6.5.0+dfsg1/tests/test_python.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_python.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,9 @@ """Tests of coverage/python.py""" +from __future__ import annotations + +import pathlib import sys import pytest @@ -23,13 +26,14 @@ "encoding", ["utf-8", "gb2312", "hebrew", "shift_jis", "cp1252"], ) - def test_get_encoded_zip_files(self, encoding): + def test_get_encoded_zip_files(self, encoding: str) -> None: # See igor.py, do_zipmods, for the text of these files. zip_file = "tests/zipmods.zip" sys.path.append(zip_file) # So we can import the files. filename = zip_file + "/encoded_" + encoding + ".py" filename = os_sep(filename) zip_data = get_zip_bytes(filename) + assert zip_data is not None zip_text = zip_data.decode(encoding) assert 'All OK' in zip_text # Run the code to see that we really got it encoded properly. @@ -37,9 +41,8 @@ assert mod.encoding == encoding -def test_source_for_file(tmpdir): - path = tmpdir.join("a.py") - src = str(path) +def test_source_for_file(tmp_path: pathlib.Path) -> None: + src = str(tmp_path / "a.py") assert source_for_file(src) == src assert source_for_file(src + 'c') == src assert source_for_file(src + 'o') == src @@ -48,18 +51,15 @@ @pytest.mark.skipif(not env.WINDOWS, reason="not windows") -def test_source_for_file_windows(tmpdir): - path = tmpdir.join("a.py") - src = str(path) +def test_source_for_file_windows(tmp_path: pathlib.Path) -> None: + a_py = tmp_path / "a.py" + src = str(a_py) # On windows if a pyw exists, it is an acceptable source - path_windows = tmpdir.ensure("a.pyw") + path_windows = tmp_path / "a.pyw" + path_windows.write_text("") assert str(path_windows) == source_for_file(src + 'c') # If both pyw and py exist, py is preferred - path.ensure(file=True) + a_py.write_text("") assert source_for_file(src + 'c') == src - - -def test_source_for_file_jython(): - assert source_for_file("a$py.class") == "a.py" diff -Nru python-coverage-6.5.0+dfsg1/tests/test_report_common.py python-coverage-7.2.7+dfsg1/tests/test_report_common.py --- python-coverage-6.5.0+dfsg1/tests/test_report_common.py 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_report_common.py 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,283 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Tests of behavior common to all reporting.""" + +from __future__ import annotations + +import textwrap + +import coverage +from coverage import env +from coverage.files import abs_file + +from tests.coveragetest import CoverageTest +from tests.goldtest import contains, doesnt_contain +from tests.helpers import arcz_to_arcs, os_sep + + +class ReportMapsPathsTest(CoverageTest): + """Check that reporting implicitly maps paths.""" + + def make_files(self, data: str, settings: bool = False) -> None: + """Create the test files we need for line coverage.""" + src = """\ + if VER == 1: + print("line 2") + if VER == 2: + print("line 4") + if VER == 3: + print("line 6") + """ + self.make_file("src/program.py", src) + self.make_file("ver1/program.py", src) + self.make_file("ver2/program.py", src) + + if data == "line": + self.make_data_file( + lines={ + abs_file("ver1/program.py"): [1, 2, 3, 5], + abs_file("ver2/program.py"): [1, 3, 4, 5], + } + ) + else: + self.make_data_file( + arcs={ + abs_file("ver1/program.py"): arcz_to_arcs(".1 12 23 35 5."), + abs_file("ver2/program.py"): arcz_to_arcs(".1 13 34 45 5."), + } + ) + + if settings: + self.make_file(".coveragerc", """\ + [paths] + source = + src + ver1 + ver2 + """) + + def test_map_paths_during_line_report_without_setting(self) -> None: + self.make_files(data="line") + cov = coverage.Coverage() + cov.load() + cov.report(show_missing=True) + expected = textwrap.dedent(os_sep("""\ + Name Stmts Miss Cover Missing + ----------------------------------------------- + ver1/program.py 6 2 67% 4, 6 + ver2/program.py 6 2 67% 2, 6 + ----------------------------------------------- + TOTAL 12 4 67% + """)) + assert expected == self.stdout() + + def test_map_paths_during_line_report(self) -> None: + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.report(show_missing=True) + expected = textwrap.dedent(os_sep("""\ + Name Stmts Miss Cover Missing + ---------------------------------------------- + src/program.py 6 1 83% 6 + ---------------------------------------------- + TOTAL 6 1 83% + """)) + assert expected == self.stdout() + + def test_map_paths_during_branch_report_without_setting(self) -> None: + self.make_files(data="arcs") + cov = coverage.Coverage(branch=True) + cov.load() + cov.report(show_missing=True) + expected = textwrap.dedent(os_sep("""\ + Name Stmts Miss Branch BrPart Cover Missing + ------------------------------------------------------------- + ver1/program.py 6 2 6 3 58% 1->3, 4, 6 + ver2/program.py 6 2 6 3 58% 2, 3->5, 6 + ------------------------------------------------------------- + TOTAL 12 4 12 6 58% + """)) + assert expected == self.stdout() + + def test_map_paths_during_branch_report(self) -> None: + self.make_files(data="arcs", settings=True) + cov = coverage.Coverage(branch=True) + cov.load() + cov.report(show_missing=True) + expected = textwrap.dedent(os_sep("""\ + Name Stmts Miss Branch BrPart Cover Missing + ------------------------------------------------------------ + src/program.py 6 1 6 1 83% 6 + ------------------------------------------------------------ + TOTAL 6 1 6 1 83% + """)) + assert expected == self.stdout() + + def test_map_paths_during_annotate(self) -> None: + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.annotate() + self.assert_exists(os_sep("src/program.py,cover")) + self.assert_doesnt_exist(os_sep("ver1/program.py,cover")) + self.assert_doesnt_exist(os_sep("ver2/program.py,cover")) + + def test_map_paths_during_html_report(self) -> None: + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.html_report() + contains("htmlcov/index.html", os_sep("src/program.py")) + doesnt_contain("htmlcov/index.html", os_sep("ver1/program.py"), os_sep("ver2/program.py")) + + def test_map_paths_during_xml_report(self) -> None: + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.xml_report() + contains("coverage.xml", "src/program.py") + doesnt_contain("coverage.xml", "ver1/program.py", "ver2/program.py") + + def test_map_paths_during_json_report(self) -> None: + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.json_report() + def os_sepj(s: str) -> str: + return os_sep(s).replace("\\", r"\\") + contains("coverage.json", os_sepj("src/program.py")) + doesnt_contain("coverage.json", os_sepj("ver1/program.py"), os_sepj("ver2/program.py")) + + def test_map_paths_during_lcov_report(self) -> None: + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.lcov_report() + contains("coverage.lcov", os_sep("src/program.py")) + doesnt_contain("coverage.lcov", os_sep("ver1/program.py"), os_sep("ver2/program.py")) + + +class ReportWithJinjaTest(CoverageTest): + """Tests of Jinja-like behavior. + + Jinja2 compiles a template into Python code, and then runs the Python code + to render the template. But during rendering, it uses the template name + (for example, "template.j2") as the file name, not the Python code file + name. Then during reporting, we will try to parse template.j2 as Python + code. + + If the file can be parsed, it's included in the report (as a Python file!). + If it can't be parsed, then it's not included in the report. + + These tests confirm that code doesn't raise an exception (as reported in + #1553), and that the current (incorrect) behavior remains stable. Ideally, + good.j2 wouldn't be listed at all, since we can't report on it accurately. + + See https://github.com/nedbat/coveragepy/issues/1553 for more detail, and + https://github.com/nedbat/coveragepy/issues/1623 for an issue about this + behavior. + + """ + + def make_files(self) -> None: + """Create test files: two Jinja templates, and data from rendering them.""" + # A Jinja2 file that is syntactically acceptable Python (though it wont run). + self.make_file("good.j2", """\ + {{ data }} + line2 + line3 + """) + # A Jinja2 file that is a Python syntax error. + self.make_file("bad.j2", """\ + This is data: {{ data }}. + line 2 + line 3 + """) + self.make_data_file( + lines={ + abs_file("good.j2"): [1, 3, 5, 7, 9], + abs_file("bad.j2"): [1, 3, 5, 7, 9], + } + ) + + def test_report(self) -> None: + self.make_files() + cov = coverage.Coverage() + cov.load() + cov.report(show_missing=True) + expected = textwrap.dedent("""\ + Name Stmts Miss Cover Missing + --------------------------------------- + good.j2 3 1 67% 2 + --------------------------------------- + TOTAL 3 1 67% + """) + assert expected == self.stdout() + + def test_html(self) -> None: + self.make_files() + cov = coverage.Coverage() + cov.load() + cov.html_report() + contains("htmlcov/index.html", """\ + <tbody> + <tr class="file"> + <td class="name left"><a href="good_j2.html">good.j2</a></td> + <td>3</td> + <td>1</td> + <td>0</td> + <td class="right" data-ratio="2 3">67%</td> + </tr> + </tbody>""" + ) + doesnt_contain("htmlcov/index.html", "bad.j2") + + def test_xml(self) -> None: + self.make_files() + cov = coverage.Coverage() + cov.load() + cov.xml_report() + contains("coverage.xml", 'filename="good.j2"') + if env.PYVERSION >= (3, 8): # Py3.7 puts attributes in the other order. + contains("coverage.xml", + '<line number="1" hits="1"/>', + '<line number="2" hits="0"/>', + '<line number="3" hits="1"/>', + ) + doesnt_contain("coverage.xml", 'filename="bad.j2"') + if env.PYVERSION >= (3, 8): # Py3.7 puts attributes in the other order. + doesnt_contain("coverage.xml", '<line number="4"',) + + def test_json(self) -> None: + self.make_files() + cov = coverage.Coverage() + cov.load() + cov.json_report() + contains("coverage.json", + # Notice the .json report claims lines in good.j2 executed that + # don't even exist in good.j2... + '"files": {"good.j2": {"executed_lines": [1, 3, 5, 7, 9], ' + + '"summary": {"covered_lines": 2, "num_statements": 3', + ) + doesnt_contain("coverage.json", "bad.j2") + + def test_lcov(self) -> None: + self.make_files() + cov = coverage.Coverage() + cov.load() + cov.lcov_report() + with open("coverage.lcov") as lcov: + actual = lcov.read() + expected = textwrap.dedent("""\ + TN: + SF:good.j2 + DA:1,1,FHs1rDakj9p/NAzMCu3Kgw + DA:3,1,DGOyp8LEgI+3CcdFYw9uKQ + DA:2,0,5iUbzxp9w7peeTPjJbvmBQ + LF:3 + LH:2 + end_of_record + """) + assert expected == actual diff -Nru python-coverage-6.5.0+dfsg1/tests/test_report_core.py python-coverage-7.2.7+dfsg1/tests/test_report_core.py --- python-coverage-6.5.0+dfsg1/tests/test_report_core.py 1970-01-01 00:00:00.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_report_core.py 2023-05-29 19:46:30.000000000 +0000 @@ -0,0 +1,68 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Tests for helpers in report.py""" + +from __future__ import annotations + +from typing import IO, Iterable, List, Optional, Type + +import pytest + +from coverage.exceptions import CoverageException +from coverage.report_core import render_report +from coverage.types import TMorf + +from tests.coveragetest import CoverageTest + + +class FakeReporter: + """A fake implementation of a one-file reporter.""" + + report_type = "fake report file" + + def __init__(self, output: str = "", error: Optional[Type[Exception]] = None) -> None: + self.output = output + self.error = error + self.morfs: Optional[Iterable[TMorf]] = None + + def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float: + """Fake.""" + self.morfs = morfs + outfile.write(self.output) + if self.error: + raise self.error("You asked for it!") + return 17.25 + + +class RenderReportTest(CoverageTest): + """Tests of render_report.""" + + def test_stdout(self) -> None: + fake = FakeReporter(output="Hello!\n") + msgs: List[str] = [] + res = render_report("-", fake, [pytest, "coverage"], msgs.append) + assert res == 17.25 + assert fake.morfs == [pytest, "coverage"] + assert self.stdout() == "Hello!\n" + assert not msgs + + def test_file(self) -> None: + fake = FakeReporter(output="Gréètings!\n") + msgs: List[str] = [] + res = render_report("output.txt", fake, [], msgs.append) + assert res == 17.25 + assert self.stdout() == "" + with open("output.txt", "rb") as f: + assert f.read().rstrip() == b"Gr\xc3\xa9\xc3\xa8tings!" + assert msgs == ["Wrote fake report file to output.txt"] + + @pytest.mark.parametrize("error", [CoverageException, ZeroDivisionError]) + def test_exception(self, error: Type[Exception]) -> None: + fake = FakeReporter(error=error) + msgs: List[str] = [] + with pytest.raises(error, match="You asked for it!"): + render_report("output.txt", fake, [], msgs.append) + assert self.stdout() == "" + self.assert_doesnt_exist("output.txt") + assert not msgs diff -Nru python-coverage-6.5.0+dfsg1/tests/test_report.py python-coverage-7.2.7+dfsg1/tests/test_report.py --- python-coverage-6.5.0+dfsg1/tests/test_report.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_report.py 2023-05-29 19:46:30.000000000 +0000 @@ -1,58 +1,1081 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt -"""Tests for helpers in report.py""" +"""Test text-based summary reporting for coverage.py""" + +from __future__ import annotations + +import glob +import io +import math +import os +import os.path +import py_compile +import re + +from typing import Tuple import pytest -from coverage.exceptions import CoverageException -from coverage.report import render_report -from tests.coveragetest import CoverageTest - - -class FakeReporter: - """A fake implementation of a one-file reporter.""" - - report_type = "fake report file" - - def __init__(self, output="", error=False): - self.output = output - self.error = error - self.morfs = None - - def report(self, morfs, outfile): - """Fake.""" - self.morfs = morfs - outfile.write(self.output) - if self.error: - raise CoverageException("You asked for it!") - - -class RenderReportTest(CoverageTest): - """Tests of render_report.""" - - def test_stdout(self): - fake = FakeReporter(output="Hello!\n") - msgs = [] - render_report("-", fake, [pytest, "coverage"], msgs.append) - assert fake.morfs == [pytest, "coverage"] - assert self.stdout() == "Hello!\n" - assert not msgs - - def test_file(self): - fake = FakeReporter(output="Gréètings!\n") - msgs = [] - render_report("output.txt", fake, [], msgs.append) +import coverage +from coverage import env +from coverage.control import Coverage +from coverage.data import CoverageData +from coverage.exceptions import ConfigError, NoDataError, NotPython +from coverage.files import abs_file +from coverage.report import SummaryReporter +from coverage.types import TConfigValueIn + +from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin +from tests.helpers import assert_coverage_warnings + + +class SummaryTest(UsingModulesMixin, CoverageTest): + """Tests of the text summary reporting for coverage.py.""" + + def make_mycode(self) -> None: + """Make the mycode.py file when needed.""" + self.make_file("mycode.py", """\ + import covmod1 + import covmodzip1 + a = 1 + print('done') + """) + + def test_report(self) -> None: + self.make_mycode() + cov = coverage.Coverage() + self.start_import_stop(cov, "mycode") + assert self.stdout() == 'done\n' + report = self.get_report(cov) + + # Name Stmts Miss Cover + # ------------------------------------------------------------------ + # c:/ned/coverage/tests/modules/covmod1.py 2 0 100% + # c:/ned/coverage/tests/zipmods.zip/covmodzip1.py 2 0 100% + # mycode.py 4 0 100% + # ------------------------------------------------------------------ + # TOTAL 8 0 100% + + assert "/coverage/__init__/" not in report + assert "/tests/modules/covmod1.py " in report + assert "/tests/zipmods.zip/covmodzip1.py " in report + assert "mycode.py " in report + assert self.last_line_squeezed(report) == "TOTAL 8 0 100%" + + def test_report_just_one(self) -> None: + # Try reporting just one module + self.make_mycode() + cov = coverage.Coverage() + self.start_import_stop(cov, "mycode") + report = self.get_report(cov, morfs=["mycode.py"]) + + # Name Stmts Miss Cover + # ------------------------------- + # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% + assert self.line_count(report) == 5 + assert "/coverage/" not in report + assert "/tests/modules/covmod1.py " not in report + assert "/tests/zipmods.zip/covmodzip1.py " not in report + assert "mycode.py " in report + assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" + + def test_report_wildcard(self) -> None: + # Try reporting using wildcards to get the modules. + self.make_mycode() + # Wildcard is handled by shell or cmdline.py, so use real commands + self.run_command("coverage run mycode.py") + report = self.report_from_command("coverage report my*.py") + + # Name Stmts Miss Cover + # ------------------------------- + # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% + + assert self.line_count(report) == 5 + assert "/coverage/" not in report + assert "/tests/modules/covmod1.py " not in report + assert "/tests/zipmods.zip/covmodzip1.py " not in report + assert "mycode.py " in report + assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" + + def test_report_omitting(self) -> None: + # Try reporting while omitting some modules + self.make_mycode() + cov = coverage.Coverage() + self.start_import_stop(cov, "mycode") + report = self.get_report(cov, omit=[f"{TESTS_DIR}/*", "*/site-packages/*"]) + + # Name Stmts Miss Cover + # ------------------------------- + # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% + + assert self.line_count(report) == 5 + assert "/coverage/" not in report + assert "/tests/modules/covmod1.py " not in report + assert "/tests/zipmods.zip/covmodzip1.py " not in report + assert "mycode.py " in report + assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" + + def test_report_including(self) -> None: + # Try reporting while including some modules + self.make_mycode() + cov = coverage.Coverage() + self.start_import_stop(cov, "mycode") + report = self.get_report(cov, include=["mycode*"]) + + # Name Stmts Miss Cover + # ------------------------------- + # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% + + assert self.line_count(report) == 5 + assert "/coverage/" not in report + assert "/tests/modules/covmod1.py " not in report + assert "/tests/zipmods.zip/covmodzip1.py " not in report + assert "mycode.py " in report + assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" + + def test_report_include_relative_files_and_path(self) -> None: + """ + Test that when relative_files is True and a relative path to a module + is included, coverage is reported for the module. + + Ref: https://github.com/nedbat/coveragepy/issues/1604 + """ + self.make_mycode() + self.make_file(".coveragerc", """\ + [run] + relative_files = true + """) + self.make_file("submodule/mycode.py", "import mycode") + + cov = coverage.Coverage() + self.start_import_stop(cov, "submodule/mycode") + report = self.get_report(cov, include="submodule/mycode.py") + + # Name Stmts Miss Cover + # --------------------------------------- + # submodule/mycode.py 1 0 100% + # --------------------------------------- + # TOTAL 1 0 100% + + assert "submodule/mycode.py " in report + assert self.last_line_squeezed(report) == "TOTAL 1 0 100%" + + def test_report_include_relative_files_and_wildcard_path(self) -> None: + self.make_mycode() + self.make_file(".coveragerc", """\ + [run] + relative_files = true + """) + self.make_file("submodule/mycode.py", "import nested.submodule.mycode") + self.make_file("nested/submodule/mycode.py", "import mycode") + + cov = coverage.Coverage() + self.start_import_stop(cov, "submodule/mycode") + report = self.get_report(cov, include="*/submodule/mycode.py") + + # Name Stmts Miss Cover + # ------------------------------------------------- + # nested/submodule/mycode.py 1 0 100% + # submodule/mycode.py 1 0 100% + # ------------------------------------------------- + # TOTAL 2 0 100% + + reported_files = [line.split()[0] for line in report.splitlines()[2:4]] + assert reported_files == [ + "nested/submodule/mycode.py", + "submodule/mycode.py", + ] + + def test_omit_files_here(self) -> None: + # https://github.com/nedbat/coveragepy/issues/1407 + self.make_file("foo.py", "") + self.make_file("bar/bar.py", "") + self.make_file("tests/test_baz.py", """\ + def test_foo(): + assert True + test_foo() + """) + self.run_command("coverage run --source=. --omit='./*.py' -m tests.test_baz") + report = self.report_from_command("coverage report") + + # Name Stmts Miss Cover + # --------------------------------------- + # tests/test_baz.py 3 0 100% + # --------------------------------------- + # TOTAL 3 0 100% + + assert self.line_count(report) == 5 + assert "foo" not in report + assert "bar" not in report + assert "tests/test_baz.py" in report + assert self.last_line_squeezed(report) == "TOTAL 3 0 100%" + + def test_run_source_vs_report_include(self) -> None: + # https://github.com/nedbat/coveragepy/issues/621 + self.make_file(".coveragerc", """\ + [run] + source = . + + [report] + include = mod/*,tests/* + """) + # It should be OK to use that configuration. + cov = coverage.Coverage() + with self.assert_warnings(cov, []): + cov.start() + cov.stop() # pragma: nested + + def test_run_omit_vs_report_omit(self) -> None: + # https://github.com/nedbat/coveragepy/issues/622 + # report:omit shouldn't clobber run:omit. + self.make_mycode() + self.make_file(".coveragerc", """\ + [run] + omit = */covmodzip1.py + + [report] + omit = */covmod1.py + """) + self.run_command("coverage run mycode.py") + + # Read the data written, to see that the right files have been omitted from running. + covdata = CoverageData() + covdata.read() + files = [os.path.basename(p) for p in covdata.measured_files()] + assert "covmod1.py" in files + assert "covmodzip1.py" not in files + + def test_report_branches(self) -> None: + self.make_file("mybranch.py", """\ + def branch(x): + if x: + print("x") + return x + branch(1) + """) + cov = coverage.Coverage(source=["."], branch=True) + self.start_import_stop(cov, "mybranch") + assert self.stdout() == 'x\n' + report = self.get_report(cov) + + # Name Stmts Miss Branch BrPart Cover + # ----------------------------------------------- + # mybranch.py 5 0 2 1 86% + # ----------------------------------------------- + # TOTAL 5 0 2 1 86% + assert self.line_count(report) == 5 + assert "mybranch.py " in report + assert self.last_line_squeezed(report) == "TOTAL 5 0 2 1 86%" + + def test_report_show_missing(self) -> None: + self.make_file("mymissing.py", """\ + def missing(x, y): + if x: + print("x") + return x + if y: + print("y") + try: + print("z") + 1/0 + print("Never!") + except ZeroDivisionError: + pass + return x + missing(0, 1) + """) + cov = coverage.Coverage(source=["."]) + self.start_import_stop(cov, "mymissing") + assert self.stdout() == 'y\nz\n' + report = self.get_report(cov, show_missing=True) + + # Name Stmts Miss Cover Missing + # -------------------------------------------- + # mymissing.py 14 3 79% 3-4, 10 + # -------------------------------------------- + # TOTAL 14 3 79% + + assert self.line_count(report) == 5 + squeezed = self.squeezed_lines(report) + assert squeezed[2] == "mymissing.py 14 3 79% 3-4, 10" + assert squeezed[4] == "TOTAL 14 3 79%" + + def test_report_show_missing_branches(self) -> None: + self.make_file("mybranch.py", """\ + def branch(x, y): + if x: + print("x") + if y: + print("y") + branch(1, 1) + """) + cov = coverage.Coverage(branch=True) + self.start_import_stop(cov, "mybranch") + assert self.stdout() == 'x\ny\n' + + def test_report_show_missing_branches_and_lines(self) -> None: + self.make_file("main.py", """\ + import mybranch + """) + self.make_file("mybranch.py", """\ + def branch(x, y, z): + if x: + print("x") + if y: + print("y") + if z: + if x and y: + print("z") + return x + branch(1, 1, 0) + """) + cov = coverage.Coverage(branch=True) + self.start_import_stop(cov, "main") + assert self.stdout() == 'x\ny\n' + + def test_report_skip_covered_no_branches(self) -> None: + self.make_file("main.py", """ + import not_covered + + def normal(): + print("z") + normal() + """) + self.make_file("not_covered.py", """ + def not_covered(): + print("n") + """) + # --fail-under is handled by cmdline.py, use real commands. + out = self.run_command("coverage run main.py") + assert out == "z\n" + report = self.report_from_command("coverage report --skip-covered --fail-under=70") + + # Name Stmts Miss Cover + # ------------------------------------ + # not_covered.py 2 1 50% + # ------------------------------------ + # TOTAL 6 1 83% + # + # 1 file skipped due to complete coverage. + + assert self.line_count(report) == 7, report + squeezed = self.squeezed_lines(report) + assert squeezed[2] == "not_covered.py 2 1 50%" + assert squeezed[4] == "TOTAL 6 1 83%" + assert squeezed[6] == "1 file skipped due to complete coverage." + assert self.last_command_status == 0 + + def test_report_skip_covered_branches(self) -> None: + self.make_file("main.py", """ + import not_covered, covered + + def normal(z): + if z: + print("z") + normal(True) + normal(False) + """) + self.make_file("not_covered.py", """ + def not_covered(n): + if n: + print("n") + not_covered(True) + """) + self.make_file("covered.py", """ + def foo(): + pass + foo() + """) + cov = coverage.Coverage(branch=True) + self.start_import_stop(cov, "main") + assert self.stdout() == "n\nz\n" + report = self.get_report(cov, skip_covered=True) + + # Name Stmts Miss Branch BrPart Cover + # -------------------------------------------------- + # not_covered.py 4 0 2 1 83% + # -------------------------------------------------- + # TOTAL 13 0 4 1 94% + # + # 2 files skipped due to complete coverage. + + assert self.line_count(report) == 7, report + squeezed = self.squeezed_lines(report) + assert squeezed[2] == "not_covered.py 4 0 2 1 83%" + assert squeezed[4] == "TOTAL 13 0 4 1 94%" + assert squeezed[6] == "2 files skipped due to complete coverage." + + def test_report_skip_covered_branches_with_totals(self) -> None: + self.make_file("main.py", """ + import not_covered + import also_not_run + + def normal(z): + if z: + print("z") + normal(True) + normal(False) + """) + self.make_file("not_covered.py", """ + def not_covered(n): + if n: + print("n") + not_covered(True) + """) + self.make_file("also_not_run.py", """ + def does_not_appear_in_this_film(ni): + print("Ni!") + """) + cov = coverage.Coverage(branch=True) + self.start_import_stop(cov, "main") + assert self.stdout() == "n\nz\n" + report = self.get_report(cov, skip_covered=True) + + # Name Stmts Miss Branch BrPart Cover + # -------------------------------------------------- + # also_not_run.py 2 1 0 0 50% + # not_covered.py 4 0 2 1 83% + # -------------------------------------------------- + # TOTAL 13 1 4 1 88% + # + # 1 file skipped due to complete coverage. + + assert self.line_count(report) == 8, report + squeezed = self.squeezed_lines(report) + assert squeezed[2] == "also_not_run.py 2 1 0 0 50%" + assert squeezed[3] == "not_covered.py 4 0 2 1 83%" + assert squeezed[5] == "TOTAL 13 1 4 1 88%" + assert squeezed[7] == "1 file skipped due to complete coverage." + + def test_report_skip_covered_all_files_covered(self) -> None: + self.make_file("main.py", """ + def foo(): + pass + foo() + """) + cov = coverage.Coverage(source=["."], branch=True) + self.start_import_stop(cov, "main") + assert self.stdout() == "" + report = self.get_report(cov, skip_covered=True) + + # Name Stmts Miss Branch BrPart Cover + # ----------------------------------------- + # TOTAL 3 0 0 0 100% + # + # 1 file skipped due to complete coverage. + + assert self.line_count(report) == 5, report + squeezed = self.squeezed_lines(report) + assert squeezed[4] == "1 file skipped due to complete coverage." + + report = self.get_report(cov, squeeze=False, skip_covered=True, output_format="markdown") + + # | Name | Stmts | Miss | Branch | BrPart | Cover | + # |---------- | -------: | -------: | -------: | -------: | -------: | + # | **TOTAL** | **3** | **0** | **0** | **0** | **100%** | + # + # 1 file skipped due to complete coverage. + + assert self.line_count(report) == 5, report + assert report.split("\n")[0] == ( + '| Name | Stmts | Miss | Branch | BrPart | Cover |' + ) + assert report.split("\n")[1] == ( + '|---------- | -------: | -------: | -------: | -------: | -------: |' + ) + assert report.split("\n")[2] == ( + '| **TOTAL** | **3** | **0** | **0** | **0** | **100%** |' + ) + squeezed = self.squeezed_lines(report) + assert squeezed[4] == "1 file skipped due to complete coverage." + + total = self.get_report(cov, output_format="total", skip_covered=True) + assert total == "100\n" + + def test_report_skip_covered_longfilename(self) -> None: + self.make_file("long_______________filename.py", """ + def foo(): + pass + foo() + """) + cov = coverage.Coverage(source=["."], branch=True) + self.start_import_stop(cov, "long_______________filename") assert self.stdout() == "" - with open("output.txt", "rb") as f: - assert f.read().rstrip() == b"Gr\xc3\xa9\xc3\xa8tings!" - assert msgs == ["Wrote fake report file to output.txt"] - - def test_exception(self): - fake = FakeReporter(error=True) - msgs = [] - with pytest.raises(CoverageException, match="You asked for it!"): - render_report("output.txt", fake, [], msgs.append) + report = self.get_report(cov, squeeze=False, skip_covered=True) + + # Name Stmts Miss Branch BrPart Cover + # ----------------------------------------- + # TOTAL 3 0 0 0 100% + # + # 1 file skipped due to complete coverage. + + assert self.line_count(report) == 5, report + lines = self.report_lines(report) + assert lines[0] == "Name Stmts Miss Branch BrPart Cover" + squeezed = self.squeezed_lines(report) + assert squeezed[4] == "1 file skipped due to complete coverage." + + def test_report_skip_covered_no_data(self) -> None: + cov = coverage.Coverage() + cov.load() + with pytest.raises(NoDataError, match="No data to report."): + self.get_report(cov, skip_covered=True) + self.assert_doesnt_exist(".coverage") + + def test_report_skip_empty(self) -> None: + self.make_file("main.py", """ + import submodule + + def normal(): + print("z") + normal() + """) + self.make_file("submodule/__init__.py", "") + cov = coverage.Coverage() + self.start_import_stop(cov, "main") + assert self.stdout() == "z\n" + report = self.get_report(cov, skip_empty=True) + + # Name Stmts Miss Cover + # ------------------------------------ + # main.py 4 0 100% + # ------------------------------------ + # TOTAL 4 0 100% + # + # 1 empty file skipped. + + assert self.line_count(report) == 7, report + squeezed = self.squeezed_lines(report) + assert squeezed[2] == "main.py 4 0 100%" + assert squeezed[4] == "TOTAL 4 0 100%" + assert squeezed[6] == "1 empty file skipped." + + def test_report_skip_empty_no_data(self) -> None: + self.make_file("__init__.py", "") + cov = coverage.Coverage() + self.start_import_stop(cov, "__init__") assert self.stdout() == "" - self.assert_doesnt_exist("output.txt") - assert not msgs + report = self.get_report(cov, skip_empty=True) + + # Name Stmts Miss Cover + # ------------------------------------ + # TOTAL 0 0 100% + # + # 1 empty file skipped. + + assert self.line_count(report) == 5, report + assert report.split("\n")[2] == "TOTAL 0 0 100%" + assert report.split("\n")[4] == "1 empty file skipped." + + def test_report_precision(self) -> None: + self.make_file(".coveragerc", """\ + [report] + precision = 3 + omit = */site-packages/* + """) + self.make_file("main.py", """ + import not_covered, covered + + def normal(z): + if z: + print("z") + normal(True) + normal(False) + """) + self.make_file("not_covered.py", """ + def not_covered(n): + if n: + print("n") + not_covered(True) + """) + self.make_file("covered.py", """ + def foo(): + pass + foo() + """) + cov = coverage.Coverage(branch=True) + self.start_import_stop(cov, "main") + assert self.stdout() == "n\nz\n" + report = self.get_report(cov, squeeze=False) + + # Name Stmts Miss Branch BrPart Cover + # ------------------------------------------------------ + # covered.py 3 0 0 0 100.000% + # main.py 6 0 2 0 100.000% + # not_covered.py 4 0 2 1 83.333% + # ------------------------------------------------------ + # TOTAL 13 0 4 1 94.118% + + assert self.line_count(report) == 7, report + squeezed = self.squeezed_lines(report) + assert squeezed[2] == "covered.py 3 0 0 0 100.000%" + assert squeezed[4] == "not_covered.py 4 0 2 1 83.333%" + assert squeezed[6] == "TOTAL 13 0 4 1 94.118%" + + def test_report_precision_all_zero(self) -> None: + self.make_file("not_covered.py", """ + def not_covered(n): + if n: + print("n") + """) + self.make_file("empty.py", "") + cov = coverage.Coverage(source=["."]) + self.start_import_stop(cov, "empty") + report = self.get_report(cov, precision=6, squeeze=False) + + # Name Stmts Miss Cover + # ----------------------------------------- + # empty.py 0 0 100.000000% + # not_covered.py 3 3 0.000000% + # ----------------------------------------- + # TOTAL 3 3 0.000000% + + assert self.line_count(report) == 6, report + assert "empty.py 0 0 100.000000%" in report + assert "not_covered.py 3 3 0.000000%" in report + assert "TOTAL 3 3 0.000000%" in report + + def test_dotpy_not_python(self) -> None: + # We run a .py file, and when reporting, we can't parse it as Python. + # We should get an error message in the report. + + self.make_data_file(lines={"mycode.py": [1]}) + self.make_file("mycode.py", "This isn't python at all!") + cov = coverage.Coverage() + cov.load() + msg = r"Couldn't parse '.*[/\\]mycode.py' as Python source: '.*' at line 1" + with pytest.raises(NotPython, match=msg): + self.get_report(cov, morfs=["mycode.py"]) + + def test_accented_directory(self) -> None: + # Make a file with a non-ascii character in the directory name. + self.make_file("\xe2/accented.py", "print('accented')") + self.make_data_file(lines={abs_file("\xe2/accented.py"): [1]}) + report_expected = ( + "Name Stmts Miss Cover\n" + + "-----------------------------------\n" + + "\xe2/accented.py 1 0 100%\n" + + "-----------------------------------\n" + + "TOTAL 1 0 100%\n" + ) + cov = coverage.Coverage() + cov.load() + output = self.get_report(cov, squeeze=False) + assert output == report_expected + + def test_accenteddotpy_not_python(self) -> None: + # We run a .py file with a non-ascii name, and when reporting, we can't + # parse it as Python. We should get an error message in the report. + + self.make_data_file(lines={"accented\xe2.py": [1]}) + self.make_file("accented\xe2.py", "This isn't python at all!") + cov = coverage.Coverage() + cov.load() + msg = r"Couldn't parse '.*[/\\]accented\xe2.py' as Python source: '.*' at line 1" + with pytest.raises(NotPython, match=msg): + self.get_report(cov, morfs=["accented\xe2.py"]) + + def test_dotpy_not_python_ignored(self) -> None: + # We run a .py file, and when reporting, we can't parse it as Python, + # but we've said to ignore errors, so there's no error reported, + # though we still get a warning. + self.make_file("mycode.py", "This isn't python at all!") + self.make_data_file(lines={"mycode.py": [1]}) + cov = coverage.Coverage() + cov.load() + with pytest.raises(NoDataError, match="No data to report."): + with pytest.warns(Warning) as warns: + self.get_report(cov, morfs=["mycode.py"], ignore_errors=True) + assert_coverage_warnings( + warns, + re.compile(r"Couldn't parse Python file '.*[/\\]mycode.py' \(couldnt-parse\)"), + ) + + def test_dothtml_not_python(self) -> None: + # We run a .html file, and when reporting, we can't parse it as + # Python. Since it wasn't .py, no error is reported. + + # Pretend to run an html file. + self.make_file("mycode.html", "<h1>This isn't python at all!</h1>") + self.make_data_file(lines={"mycode.html": [1]}) + cov = coverage.Coverage() + cov.load() + with pytest.raises(NoDataError, match="No data to report."): + self.get_report(cov, morfs=["mycode.html"]) + + def test_report_no_extension(self) -> None: + self.make_file("xxx", """\ + # This is a python file though it doesn't look like it, like a main script. + a = b = c = d = 0 + a = 3 + b = 4 + if not b: + c = 6 + d = 7 + print(f"xxx: {a} {b} {c} {d}") + """) + self.make_data_file(lines={abs_file("xxx"): [2, 3, 4, 5, 7, 8]}) + cov = coverage.Coverage() + cov.load() + report = self.get_report(cov) + assert self.last_line_squeezed(report) == "TOTAL 7 1 86%" + + def test_report_with_chdir(self) -> None: + self.make_file("chdir.py", """\ + import os + print("Line One") + os.chdir("subdir") + print("Line Two") + print(open("something").read()) + """) + self.make_file("subdir/something", "hello") + out = self.run_command("coverage run --source=. chdir.py") + assert out == "Line One\nLine Two\nhello\n" + report = self.report_from_command("coverage report") + assert self.last_line_squeezed(report) == "TOTAL 5 0 100%" + report = self.report_from_command("coverage report --format=markdown") + assert self.last_line_squeezed(report) == "| **TOTAL** | **5** | **0** | **100%** |" + + def test_bug_156_file_not_run_should_be_zero(self) -> None: + # https://github.com/nedbat/coveragepy/issues/156 + self.make_file("mybranch.py", """\ + def branch(x): + if x: + print("x") + return x + branch(1) + """) + self.make_file("main.py", """\ + print("y") + """) + cov = coverage.Coverage(branch=True, source=["."]) + self.start_import_stop(cov, "main") + report = self.get_report(cov).splitlines() + assert "mybranch.py 5 5 2 0 0%" in report + + def run_TheCode_and_report_it(self) -> str: + """A helper for the next few tests.""" + cov = coverage.Coverage() + self.start_import_stop(cov, "TheCode") + return self.get_report(cov) + + def test_bug_203_mixed_case_listed_twice_with_rc(self) -> None: + self.make_file("TheCode.py", "a = 1\n") + self.make_file(".coveragerc", "[run]\nsource = .\n") + + report = self.run_TheCode_and_report_it() + assert "TheCode" in report + assert "thecode" not in report + + def test_bug_203_mixed_case_listed_twice(self) -> None: + self.make_file("TheCode.py", "a = 1\n") + + report = self.run_TheCode_and_report_it() + + assert "TheCode" in report + assert "thecode" not in report + + @pytest.mark.skipif(not env.WINDOWS, reason=".pyw files are only on Windows.") + def test_pyw_files(self) -> None: + # https://github.com/nedbat/coveragepy/issues/261 + self.make_file("start.pyw", """\ + import mod + print("In start.pyw") + """) + self.make_file("mod.pyw", """\ + print("In mod.pyw") + """) + cov = coverage.Coverage() + # start_import_stop can't import the .pyw file, so use the long form. + cov.start() + import start # pragma: nested # pylint: disable=import-error, unused-import + cov.stop() # pragma: nested + + report = self.get_report(cov) + assert "NoSource" not in report + report_lines = report.splitlines() + assert "start.pyw 2 0 100%" in report_lines + assert "mod.pyw 1 0 100%" in report_lines + + def test_tracing_pyc_file(self) -> None: + # Create two Python files. + self.make_file("mod.py", "a = 1\n") + self.make_file("main.py", "import mod\n") + + # Make one into a .pyc. + py_compile.compile("mod.py") + + # Run the program. + cov = coverage.Coverage() + self.start_import_stop(cov, "main") + + report_lines = self.get_report(cov).splitlines() + assert "mod.py 1 0 100%" in report_lines + report = self.get_report(cov, squeeze=False, output_format="markdown") + assert report.split("\n")[3] == "| mod.py | 1 | 0 | 100% |" + assert report.split("\n")[4] == "| **TOTAL** | **2** | **0** | **100%** |" + + def test_missing_py_file_during_run(self) -> None: + # Create two Python files. + self.make_file("mod.py", "a = 1\n") + self.make_file("main.py", "import mod\n") + + # Make one into a .pyc, and remove the .py. + py_compile.compile("mod.py") + os.remove("mod.py") + + # Python 3 puts the .pyc files in a __pycache__ directory, and will + # not import from there without source. It will import a .pyc from + # the source location though. + pycs = glob.glob("__pycache__/mod.*.pyc") + assert len(pycs) == 1 + os.rename(pycs[0], "mod.pyc") + + # Run the program. + cov = coverage.Coverage() + self.start_import_stop(cov, "main") + + # Put back the missing Python file. + self.make_file("mod.py", "a = 1\n") + report = self.get_report(cov).splitlines() + assert "mod.py 1 0 100%" in report + + def test_empty_files(self) -> None: + # Shows that empty files like __init__.py are listed as having zero + # statements, not one statement. + cov = coverage.Coverage(branch=True) + cov.start() + import usepkgs # pragma: nested # pylint: disable=import-error, unused-import + cov.stop() # pragma: nested + report = self.get_report(cov) + assert "tests/modules/pkg1/__init__.py 1 0 0 0 100%" in report + assert "tests/modules/pkg2/__init__.py 0 0 0 0 100%" in report + report = self.get_report(cov, squeeze=False, output_format="markdown") + # get_report() escapes backslash so we expect forward slash escaped + # underscore + assert "tests/modules/pkg1//_/_init/_/_.py " in report + assert "| 1 | 0 | 0 | 0 | 100% |" in report + assert "tests/modules/pkg2//_/_init/_/_.py " in report + assert "| 0 | 0 | 0 | 0 | 100% |" in report + + def test_markdown_with_missing(self) -> None: + self.make_file("mymissing.py", """\ + def missing(x, y): + if x: + print("x") + return x + if y: + print("y") + try: + print("z") + 1/0 + print("Never!") + except ZeroDivisionError: + pass + return x + missing(0, 1) + """) + cov = coverage.Coverage(source=["."]) + self.start_import_stop(cov, "mymissing") + assert self.stdout() == 'y\nz\n' + report = self.get_report(cov, squeeze=False, output_format="markdown", show_missing=True) + + # | Name | Stmts | Miss | Cover | Missing | + # |------------- | -------: | -------: | ------: | --------: | + # | mymissing.py | 14 | 3 | 79% | 3-4, 10 | + # | **TOTAL** | **14** | **3** | **79%** | | + assert self.line_count(report) == 4 + report_lines = report.split("\n") + assert report_lines[2] == "| mymissing.py | 14 | 3 | 79% | 3-4, 10 |" + assert report_lines[3] == "| **TOTAL** | **14** | **3** | **79%** | |" + + assert self.get_report(cov, output_format="total") == "79\n" + assert self.get_report(cov, output_format="total", precision=2) == "78.57\n" + assert self.get_report(cov, output_format="total", precision=4) == "78.5714\n" + + def test_bug_1524(self) -> None: + self.make_file("bug1524.py", """\ + class Mine: + @property + def thing(self) -> int: + return 17 + + print(Mine().thing) + """) + cov = coverage.Coverage() + self.start_import_stop(cov, "bug1524") + assert self.stdout() == "17\n" + report = self.get_report(cov) + report_lines = report.splitlines() + assert report_lines[2] == "bug1524.py 5 0 100%" + + +class ReportingReturnValueTest(CoverageTest): + """Tests of reporting functions returning values.""" + + def run_coverage(self) -> Coverage: + """Run coverage on doit.py and return the coverage object.""" + self.make_file("doit.py", """\ + a = 1 + b = 2 + c = 3 + d = 4 + if a > 10: + f = 6 + g = 7 + """) + + cov = coverage.Coverage() + self.start_import_stop(cov, "doit") + return cov + + def test_report(self) -> None: + cov = self.run_coverage() + val = cov.report(include="*/doit.py") + assert math.isclose(val, 6 / 7 * 100) + + def test_html(self) -> None: + cov = self.run_coverage() + val = cov.html_report(include="*/doit.py") + assert math.isclose(val, 6 / 7 * 100) + + def test_xml(self) -> None: + cov = self.run_coverage() + val = cov.xml_report(include="*/doit.py") + assert math.isclose(val, 6 / 7 * 100) + + +class SummaryReporterConfigurationTest(CoverageTest): + """Tests of SummaryReporter.""" + + def make_rigged_file(self, filename: str, stmts: int, miss: int) -> None: + """Create a file that will have specific results. + + `stmts` and `miss` are ints, the number of statements, and + missed statements that should result. + """ + run = stmts - miss - 1 + dont_run = miss + source = "" + source += "a = 1\n" * run + source += "if a == 99:\n" + source += " a = 2\n" * dont_run + self.make_file(filename, source) + + def get_summary_text(self, *options: Tuple[str, TConfigValueIn]) -> str: + """Get text output from the SummaryReporter. + + The arguments are tuples: (name, value) for Coverage.set_option. + """ + self.make_rigged_file("file1.py", 339, 155) + self.make_rigged_file("file2.py", 13, 3) + self.make_rigged_file("file10.py", 234, 228) + self.make_file("doit.py", "import file1, file2, file10") + + cov = Coverage(source=["."], omit=["doit.py"]) + self.start_import_stop(cov, "doit") + for name, value in options: + cov.set_option(name, value) + printer = SummaryReporter(cov) + destination = io.StringIO() + printer.report([], destination) + return destination.getvalue() + + def test_test_data(self) -> None: + # We use our own test files as test data. Check that our assumptions + # about them are still valid. We want the three columns of numbers to + # sort in three different orders. + report = self.get_summary_text() + # Name Stmts Miss Cover + # ------------------------------ + # file1.py 339 155 54% + # file2.py 13 3 77% + # file10.py 234 228 3% + # ------------------------------ + # TOTAL 586 386 34% + lines = report.splitlines()[2:-2] + assert len(lines) == 3 + nums = [list(map(int, l.replace('%', '').split()[1:])) for l in lines] + # [ + # [339, 155, 54], + # [ 13, 3, 77], + # [234, 228, 3] + # ] + assert nums[1][0] < nums[2][0] < nums[0][0] + assert nums[1][1] < nums[0][1] < nums[2][1] + assert nums[2][2] < nums[0][2] < nums[1][2] + + def test_defaults(self) -> None: + """Run the report with no configuration options.""" + report = self.get_summary_text() + assert 'Missing' not in report + assert 'Branch' not in report + + def test_print_missing(self) -> None: + """Run the report printing the missing lines.""" + report = self.get_summary_text(('report:show_missing', True)) + assert 'Missing' in report + assert 'Branch' not in report + + def assert_ordering(self, text: str, *words: str) -> None: + """Assert that the `words` appear in order in `text`.""" + indexes = list(map(text.find, words)) + assert -1 not in indexes + msg = f"The words {words!r} don't appear in order in {text!r}" + assert indexes == sorted(indexes), msg + + def test_default_sort_report(self) -> None: + # Sort the text report by the default (Name) column. + report = self.get_summary_text() + self.assert_ordering(report, "file1.py", "file2.py", "file10.py") + + def test_sort_report_by_name(self) -> None: + # Sort the text report explicitly by the Name column. + report = self.get_summary_text(('report:sort', 'Name')) + self.assert_ordering(report, "file1.py", "file2.py", "file10.py") + + def test_sort_report_by_stmts(self) -> None: + # Sort the text report by the Stmts column. + report = self.get_summary_text(('report:sort', 'Stmts')) + self.assert_ordering(report, "file2.py", "file10.py", "file1.py") + + def test_sort_report_by_missing(self) -> None: + # Sort the text report by the Missing column. + report = self.get_summary_text(('report:sort', 'Miss')) + self.assert_ordering(report, "file2.py", "file1.py", "file10.py") + + def test_sort_report_by_cover(self) -> None: + # Sort the text report by the Cover column. + report = self.get_summary_text(('report:sort', 'Cover')) + self.assert_ordering(report, "file10.py", "file1.py", "file2.py") + + def test_sort_report_by_cover_plus(self) -> None: + # Sort the text report by the Cover column, including the explicit + sign. + report = self.get_summary_text(('report:sort', '+Cover')) + self.assert_ordering(report, "file10.py", "file1.py", "file2.py") + + def test_sort_report_by_cover_reversed(self) -> None: + # Sort the text report by the Cover column reversed. + report = self.get_summary_text(('report:sort', '-Cover')) + self.assert_ordering(report, "file2.py", "file1.py", "file10.py") + + def test_sort_report_by_invalid_option(self) -> None: + # Sort the text report by a nonsense column. + msg = "Invalid sorting option: 'Xyzzy'" + with pytest.raises(ConfigError, match=msg): + self.get_summary_text(('report:sort', 'Xyzzy')) + + def test_report_with_invalid_format(self) -> None: + # Ask for an invalid format. + msg = "Unknown report format choice: 'xyzzy'" + with pytest.raises(ConfigError, match=msg): + self.get_summary_text(('report:format', 'xyzzy')) diff -Nru python-coverage-6.5.0+dfsg1/tests/test_results.py python-coverage-7.2.7+dfsg1/tests/test_results.py --- python-coverage-6.5.0+dfsg1/tests/test_results.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_results.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,12 +3,17 @@ """Tests for coverage.py's results analysis.""" +from __future__ import annotations + import math +from typing import Dict, Iterable, List, Tuple, cast + import pytest from coverage.exceptions import ConfigError from coverage.results import format_lines, Numbers, should_fail_under +from coverage.types import TLineNo from tests.coveragetest import CoverageTest @@ -18,14 +23,14 @@ run_in_temp_dir = False - def test_basic(self): + def test_basic(self) -> None: n1 = Numbers(n_files=1, n_statements=200, n_missing=20) assert n1.n_statements == 200 assert n1.n_executed == 180 assert n1.n_missing == 20 assert n1.pc_covered == 90 - def test_addition(self): + def test_addition(self) -> None: n1 = Numbers(n_files=1, n_statements=200, n_missing=20) n2 = Numbers(n_files=1, n_statements=10, n_missing=8) n3 = n1 + n2 @@ -35,10 +40,10 @@ assert n3.n_missing == 28 assert math.isclose(n3.pc_covered, 86.666666666) - def test_sum(self): + def test_sum(self) -> None: n1 = Numbers(n_files=1, n_statements=200, n_missing=20) n2 = Numbers(n_files=1, n_statements=10, n_missing=8) - n3 = sum([n1, n2]) + n3 = cast(Numbers, sum([n1, n2])) assert n3.n_files == 2 assert n3.n_statements == 210 assert n3.n_executed == 182 @@ -55,7 +60,7 @@ (dict(precision=1, n_files=1, n_statements=10000, n_missing=9999), "0.1"), (dict(precision=1, n_files=1, n_statements=10000, n_missing=10000), "0.0"), ]) - def test_pc_covered_str(self, kwargs, res): + def test_pc_covered_str(self, kwargs: Dict[str, int], res: str) -> None: assert Numbers(**kwargs).pc_covered_str == res @pytest.mark.parametrize("prec, pc, res", [ @@ -64,7 +69,7 @@ (0, 99.995, "99"), (2, 99.99995, "99.99"), ]) - def test_display_covered(self, prec, pc, res): + def test_display_covered(self, prec: int, pc: float, res: str) -> None: assert Numbers(precision=prec).display_covered(pc) == res @pytest.mark.parametrize("prec, width", [ @@ -72,10 +77,10 @@ (1, 5), # 100.0 (4, 8), # 100.0000 ]) - def test_pc_str_width(self, prec, width): + def test_pc_str_width(self, prec: int, width: int) -> None: assert Numbers(precision=prec).pc_str_width() == width - def test_covered_ratio(self): + def test_covered_ratio(self) -> None: n = Numbers(n_files=1, n_statements=200, n_missing=47) assert n.ratio_covered == (153, 200) @@ -111,11 +116,11 @@ (99.999, 100, 2, True), (99.999, 100, 3, True), ]) -def test_should_fail_under(total, fail_under, precision, result): +def test_should_fail_under(total: float, fail_under: float, precision: int, result: bool) -> None: assert should_fail_under(float(total), float(fail_under), precision) == result -def test_should_fail_under_invalid_value(): +def test_should_fail_under_invalid_value() -> None: with pytest.raises(ConfigError, match=r"fail_under=101"): should_fail_under(100.0, 101, 0) @@ -129,7 +134,11 @@ ([1, 2, 3, 4, 5], [], ""), ([1, 2, 3, 4, 5], [4], "4"), ]) -def test_format_lines(statements, lines, result): +def test_format_lines( + statements: Iterable[TLineNo], + lines: Iterable[TLineNo], + result: str, +) -> None: assert format_lines(statements, lines) == result @@ -153,5 +162,10 @@ "1-2, 3->4, 99, 102-104" ), ]) -def test_format_lines_with_arcs(statements, lines, arcs, result): +def test_format_lines_with_arcs( + statements: Iterable[TLineNo], + lines: Iterable[TLineNo], + arcs: Iterable[Tuple[TLineNo, List[TLineNo]]], + result: str, +) -> None: assert format_lines(statements, lines, arcs) == result diff -Nru python-coverage-6.5.0+dfsg1/tests/test_setup.py python-coverage-7.2.7+dfsg1/tests/test_setup.py --- python-coverage-6.5.0+dfsg1/tests/test_setup.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_setup.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,8 +3,12 @@ """Tests of miscellaneous stuff.""" +from __future__ import annotations + import sys +from typing import List, cast + import coverage from tests.coveragetest import CoverageTest @@ -15,12 +19,12 @@ run_in_temp_dir = False - def setUp(self): + def setUp(self) -> None: super().setUp() # Force the most restrictive interpretation. self.set_environ('LC_ALL', 'C') - def test_metadata(self): + def test_metadata(self) -> None: status, output = self.run_command_status( "python setup.py --description --version --url --author" ) @@ -31,19 +35,19 @@ assert "github.com/nedbat/coveragepy" in out[2] assert "Ned Batchelder" in out[3] - def test_more_metadata(self): + def test_more_metadata(self) -> None: # Let's be sure we pick up our own setup.py # CoverageTest restores the original sys.path for us. sys.path.insert(0, '') from setup import setup_args - classifiers = setup_args['classifiers'] + classifiers = cast(List[str], setup_args['classifiers']) assert len(classifiers) > 7 assert classifiers[-1].startswith("Development Status ::") assert "Programming Language :: Python :: %d" % sys.version_info[:1] in classifiers assert "Programming Language :: Python :: %d.%d" % sys.version_info[:2] in classifiers - long_description = setup_args['long_description'].splitlines() + long_description = cast(str, setup_args['long_description']).splitlines() assert len(long_description) > 7 assert long_description[0].strip() != "" assert long_description[-1].strip() != "" diff -Nru python-coverage-6.5.0+dfsg1/tests/test_summary.py python-coverage-7.2.7+dfsg1/tests/test_summary.py --- python-coverage-6.5.0+dfsg1/tests/test_summary.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_summary.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,916 +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 - -"""Test text-based summary reporting for coverage.py""" - -import glob -import io -import math -import os -import os.path -import py_compile -import re - -import pytest - -import coverage -from coverage import env -from coverage.control import Coverage -from coverage.data import CoverageData -from coverage.exceptions import ConfigError, NoDataError, NotPython -from coverage.files import abs_file -from coverage.summary import SummaryReporter - -from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin -from tests.helpers import assert_coverage_warnings - - -class SummaryTest(UsingModulesMixin, CoverageTest): - """Tests of the text summary reporting for coverage.py.""" - - def make_mycode(self): - """Make the mycode.py file when needed.""" - self.make_file("mycode.py", """\ - import covmod1 - import covmodzip1 - a = 1 - print('done') - """) - - def test_report(self): - self.make_mycode() - cov = coverage.Coverage() - self.start_import_stop(cov, "mycode") - assert self.stdout() == 'done\n' - report = self.get_report(cov) - - # Name Stmts Miss Cover - # ------------------------------------------------------------------ - # c:/ned/coverage/tests/modules/covmod1.py 2 0 100% - # c:/ned/coverage/tests/zipmods.zip/covmodzip1.py 2 0 100% - # mycode.py 4 0 100% - # ------------------------------------------------------------------ - # TOTAL 8 0 100% - - assert "/coverage/__init__/" not in report - assert "/tests/modules/covmod1.py " in report - assert "/tests/zipmods.zip/covmodzip1.py " in report - assert "mycode.py " in report - assert self.last_line_squeezed(report) == "TOTAL 8 0 100%" - - def test_report_just_one(self): - # Try reporting just one module - self.make_mycode() - cov = coverage.Coverage() - self.start_import_stop(cov, "mycode") - report = self.get_report(cov, morfs=["mycode.py"]) - - # Name Stmts Miss Cover - # ------------------------------- - # mycode.py 4 0 100% - # ------------------------------- - # TOTAL 4 0 100% - - assert self.line_count(report) == 5 - assert "/coverage/" not in report - assert "/tests/modules/covmod1.py " not in report - assert "/tests/zipmods.zip/covmodzip1.py " not in report - assert "mycode.py " in report - assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" - - def test_report_wildcard(self): - # Try reporting using wildcards to get the modules. - self.make_mycode() - # Wildcard is handled by shell or cmdline.py, so use real commands - self.run_command("coverage run mycode.py") - report = self.report_from_command("coverage report my*.py") - - # Name Stmts Miss Cover - # ------------------------------- - # mycode.py 4 0 100% - # ------------------------------- - # TOTAL 4 0 100% - - assert self.line_count(report) == 5 - assert "/coverage/" not in report - assert "/tests/modules/covmod1.py " not in report - assert "/tests/zipmods.zip/covmodzip1.py " not in report - assert "mycode.py " in report - assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" - - def test_report_omitting(self): - # Try reporting while omitting some modules - self.make_mycode() - cov = coverage.Coverage() - self.start_import_stop(cov, "mycode") - report = self.get_report(cov, omit=[f"{TESTS_DIR}/*", "*/site-packages/*"]) - - # Name Stmts Miss Cover - # ------------------------------- - # mycode.py 4 0 100% - # ------------------------------- - # TOTAL 4 0 100% - - assert self.line_count(report) == 5 - assert "/coverage/" not in report - assert "/tests/modules/covmod1.py " not in report - assert "/tests/zipmods.zip/covmodzip1.py " not in report - assert "mycode.py " in report - assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" - - def test_report_including(self): - # Try reporting while including some modules - self.make_mycode() - cov = coverage.Coverage() - self.start_import_stop(cov, "mycode") - report = self.get_report(cov, include=["mycode*"]) - - # Name Stmts Miss Cover - # ------------------------------- - # mycode.py 4 0 100% - # ------------------------------- - # TOTAL 4 0 100% - - assert self.line_count(report) == 5 - assert "/coverage/" not in report - assert "/tests/modules/covmod1.py " not in report - assert "/tests/zipmods.zip/covmodzip1.py " not in report - assert "mycode.py " in report - assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" - - def test_run_source_vs_report_include(self): - # https://github.com/nedbat/coveragepy/issues/621 - self.make_file(".coveragerc", """\ - [run] - source = . - - [report] - include = mod/*,tests/* - """) - # It should be OK to use that configuration. - cov = coverage.Coverage() - with self.assert_warnings(cov, []): - cov.start() - cov.stop() # pragma: nested - - def test_run_omit_vs_report_omit(self): - # https://github.com/nedbat/coveragepy/issues/622 - # report:omit shouldn't clobber run:omit. - self.make_mycode() - self.make_file(".coveragerc", """\ - [run] - omit = */covmodzip1.py - - [report] - omit = */covmod1.py - """) - self.run_command("coverage run mycode.py") - - # Read the data written, to see that the right files have been omitted from running. - covdata = CoverageData() - covdata.read() - files = [os.path.basename(p) for p in covdata.measured_files()] - assert "covmod1.py" in files - assert "covmodzip1.py" not in files - - def test_report_branches(self): - self.make_file("mybranch.py", """\ - def branch(x): - if x: - print("x") - return x - branch(1) - """) - cov = coverage.Coverage(source=["."], branch=True) - self.start_import_stop(cov, "mybranch") - assert self.stdout() == 'x\n' - report = self.get_report(cov) - - # Name Stmts Miss Branch BrPart Cover - # ----------------------------------------------- - # mybranch.py 5 0 2 1 86% - # ----------------------------------------------- - # TOTAL 5 0 2 1 86% - - assert self.line_count(report) == 5 - assert "mybranch.py " in report - assert self.last_line_squeezed(report) == "TOTAL 5 0 2 1 86%" - - def test_report_show_missing(self): - self.make_file("mymissing.py", """\ - def missing(x, y): - if x: - print("x") - return x - if y: - print("y") - try: - print("z") - 1/0 - print("Never!") - except ZeroDivisionError: - pass - return x - missing(0, 1) - """) - cov = coverage.Coverage(source=["."]) - self.start_import_stop(cov, "mymissing") - assert self.stdout() == 'y\nz\n' - report = self.get_report(cov, show_missing=True) - - # Name Stmts Miss Cover Missing - # -------------------------------------------- - # mymissing.py 14 3 79% 3-4, 10 - # -------------------------------------------- - # TOTAL 14 3 79% 3-4, 10 - - assert self.line_count(report) == 5 - squeezed = self.squeezed_lines(report) - assert squeezed[2] == "mymissing.py 14 3 79% 3-4, 10" - assert squeezed[4] == "TOTAL 14 3 79%" - - def test_report_show_missing_branches(self): - self.make_file("mybranch.py", """\ - def branch(x, y): - if x: - print("x") - if y: - print("y") - branch(1, 1) - """) - cov = coverage.Coverage(branch=True) - self.start_import_stop(cov, "mybranch") - assert self.stdout() == 'x\ny\n' - report = self.get_report(cov, show_missing=True) - - # Name Stmts Miss Branch BrPart Cover Missing - # ---------------------------------------------------------- - # mybranch.py 6 0 4 2 80% 2->4, 4->exit - # ---------------------------------------------------------- - # TOTAL 6 0 4 2 80% - - assert self.line_count(report) == 5 - squeezed = self.squeezed_lines(report) - assert squeezed[2] == "mybranch.py 6 0 4 2 80% 2->4, 4->exit" - assert squeezed[4] == "TOTAL 6 0 4 2 80%" - - def test_report_show_missing_branches_and_lines(self): - self.make_file("main.py", """\ - import mybranch - """) - self.make_file("mybranch.py", """\ - def branch(x, y, z): - if x: - print("x") - if y: - print("y") - if z: - if x and y: - print("z") - return x - branch(1, 1, 0) - """) - cov = coverage.Coverage(branch=True) - self.start_import_stop(cov, "main") - assert self.stdout() == 'x\ny\n' - report_lines = self.get_report(cov, squeeze=False, show_missing=True).splitlines() - - expected = [ - 'Name Stmts Miss Branch BrPart Cover Missing', - '---------------------------------------------------------', - 'main.py 1 0 0 0 100%', - 'mybranch.py 10 2 8 3 61% 2->4, 4->6, 7-8', - '---------------------------------------------------------', - 'TOTAL 11 2 8 3 63%', - ] - assert expected == report_lines - - def test_report_skip_covered_no_branches(self): - self.make_file("main.py", """ - import not_covered - - def normal(): - print("z") - normal() - """) - self.make_file("not_covered.py", """ - def not_covered(): - print("n") - """) - # --fail-under is handled by cmdline.py, use real commands. - out = self.run_command("coverage run main.py") - assert out == "z\n" - report = self.report_from_command("coverage report --skip-covered --fail-under=70") - - # Name Stmts Miss Cover - # ------------------------------------ - # not_covered.py 2 1 50% - # ------------------------------------ - # TOTAL 6 1 83% - # - # 1 file skipped due to complete coverage. - - assert self.line_count(report) == 7, report - squeezed = self.squeezed_lines(report) - assert squeezed[2] == "not_covered.py 2 1 50%" - assert squeezed[4] == "TOTAL 6 1 83%" - assert squeezed[6] == "1 file skipped due to complete coverage." - assert self.last_command_status == 0 - - def test_report_skip_covered_branches(self): - self.make_file("main.py", """ - import not_covered, covered - - def normal(z): - if z: - print("z") - normal(True) - normal(False) - """) - self.make_file("not_covered.py", """ - def not_covered(n): - if n: - print("n") - not_covered(True) - """) - self.make_file("covered.py", """ - def foo(): - pass - foo() - """) - cov = coverage.Coverage(branch=True) - self.start_import_stop(cov, "main") - assert self.stdout() == "n\nz\n" - report = self.get_report(cov, skip_covered=True) - - # Name Stmts Miss Branch BrPart Cover - # -------------------------------------------------- - # not_covered.py 4 0 2 1 83% - # -------------------------------------------------- - # TOTAL 13 0 4 1 94% - # - # 2 files skipped due to complete coverage. - - assert self.line_count(report) == 7, report - squeezed = self.squeezed_lines(report) - assert squeezed[2] == "not_covered.py 4 0 2 1 83%" - assert squeezed[4] == "TOTAL 13 0 4 1 94%" - assert squeezed[6] == "2 files skipped due to complete coverage." - - def test_report_skip_covered_branches_with_totals(self): - self.make_file("main.py", """ - import not_covered - import also_not_run - - def normal(z): - if z: - print("z") - normal(True) - normal(False) - """) - self.make_file("not_covered.py", """ - def not_covered(n): - if n: - print("n") - not_covered(True) - """) - self.make_file("also_not_run.py", """ - def does_not_appear_in_this_film(ni): - print("Ni!") - """) - cov = coverage.Coverage(branch=True) - self.start_import_stop(cov, "main") - assert self.stdout() == "n\nz\n" - report = self.get_report(cov, skip_covered=True) - - # Name Stmts Miss Branch BrPart Cover - # -------------------------------------------------- - # also_not_run.py 2 1 0 0 50% - # not_covered.py 4 0 2 1 83% - # -------------------------------------------------- - # TOTAL 13 1 4 1 88% - # - # 1 file skipped due to complete coverage. - - assert self.line_count(report) == 8, report - squeezed = self.squeezed_lines(report) - assert squeezed[2] == "also_not_run.py 2 1 0 0 50%" - assert squeezed[3] == "not_covered.py 4 0 2 1 83%" - assert squeezed[5] == "TOTAL 13 1 4 1 88%" - assert squeezed[7] == "1 file skipped due to complete coverage." - - def test_report_skip_covered_all_files_covered(self): - self.make_file("main.py", """ - def foo(): - pass - foo() - """) - cov = coverage.Coverage(source=["."], branch=True) - self.start_import_stop(cov, "main") - assert self.stdout() == "" - report = self.get_report(cov, skip_covered=True) - - # Name Stmts Miss Branch BrPart Cover - # ------------------------------------------- - # ----------------------------------------- - # TOTAL 3 0 0 0 100% - # - # 1 file skipped due to complete coverage. - - assert self.line_count(report) == 6, report - squeezed = self.squeezed_lines(report) - assert squeezed[5] == "1 file skipped due to complete coverage." - - def test_report_skip_covered_longfilename(self): - self.make_file("long_______________filename.py", """ - def foo(): - pass - foo() - """) - cov = coverage.Coverage(source=["."], branch=True) - self.start_import_stop(cov, "long_______________filename") - assert self.stdout() == "" - report = self.get_report(cov, squeeze=False, skip_covered=True) - - # Name Stmts Miss Branch BrPart Cover - # ----------------------------------------- - # ----------------------------------------- - # TOTAL 3 0 0 0 100% - # - # 1 file skipped due to complete coverage. - - assert self.line_count(report) == 6, report - lines = self.report_lines(report) - assert lines[0] == "Name Stmts Miss Branch BrPart Cover" - squeezed = self.squeezed_lines(report) - assert squeezed[5] == "1 file skipped due to complete coverage." - - def test_report_skip_covered_no_data(self): - cov = coverage.Coverage() - cov.load() - with pytest.raises(NoDataError, match="No data to report."): - self.get_report(cov, skip_covered=True) - self.assert_doesnt_exist(".coverage") - - def test_report_skip_empty(self): - self.make_file("main.py", """ - import submodule - - def normal(): - print("z") - normal() - """) - self.make_file("submodule/__init__.py", "") - cov = coverage.Coverage() - self.start_import_stop(cov, "main") - assert self.stdout() == "z\n" - report = self.get_report(cov, skip_empty=True) - - # Name Stmts Miss Cover - # ------------------------------------ - # main.py 4 0 100% - # ------------------------------------ - # TOTAL 4 0 100% - # - # 1 empty file skipped. - - assert self.line_count(report) == 7, report - squeezed = self.squeezed_lines(report) - assert squeezed[2] == "main.py 4 0 100%" - assert squeezed[4] == "TOTAL 4 0 100%" - assert squeezed[6] == "1 empty file skipped." - - def test_report_skip_empty_no_data(self): - self.make_file("__init__.py", "") - cov = coverage.Coverage() - self.start_import_stop(cov, "__init__") - assert self.stdout() == "" - report = self.get_report(cov, skip_empty=True) - - # Name Stmts Miss Cover - # ------------------------------------ - # - # 1 empty file skipped. - - assert self.line_count(report) == 6, report - squeezed = self.squeezed_lines(report) - assert squeezed[3] == "TOTAL 0 0 100%" - assert squeezed[5] == "1 empty file skipped." - - def test_report_precision(self): - self.make_file(".coveragerc", """\ - [report] - precision = 3 - omit = */site-packages/* - """) - self.make_file("main.py", """ - import not_covered, covered - - def normal(z): - if z: - print("z") - normal(True) - normal(False) - """) - self.make_file("not_covered.py", """ - def not_covered(n): - if n: - print("n") - not_covered(True) - """) - self.make_file("covered.py", """ - def foo(): - pass - foo() - """) - cov = coverage.Coverage(branch=True) - self.start_import_stop(cov, "main") - assert self.stdout() == "n\nz\n" - report = self.get_report(cov) - - # Name Stmts Miss Branch BrPart Cover - # ------------------------------------------------------ - # covered.py 3 0 0 0 100.000% - # main.py 6 0 2 0 100.000% - # not_covered.py 4 0 2 1 83.333% - # ------------------------------------------------------ - # TOTAL 13 0 4 1 94.118% - - assert self.line_count(report) == 7, report - squeezed = self.squeezed_lines(report) - assert squeezed[2] == "covered.py 3 0 0 0 100.000%" - assert squeezed[4] == "not_covered.py 4 0 2 1 83.333%" - assert squeezed[6] == "TOTAL 13 0 4 1 94.118%" - - def test_dotpy_not_python(self): - # We run a .py file, and when reporting, we can't parse it as Python. - # We should get an error message in the report. - - self.make_data_file(lines={"mycode.py": [1]}) - self.make_file("mycode.py", "This isn't python at all!") - cov = coverage.Coverage() - cov.load() - msg = r"Couldn't parse '.*[/\\]mycode.py' as Python source: '.*' at line 1" - with pytest.raises(NotPython, match=msg): - self.get_report(cov, morfs=["mycode.py"]) - - def test_accented_directory(self): - # Make a file with a non-ascii character in the directory name. - self.make_file("\xe2/accented.py", "print('accented')") - self.make_data_file(lines={abs_file("\xe2/accented.py"): [1]}) - report_expected = ( - "Name Stmts Miss Cover\n" + - "-----------------------------------\n" + - "\xe2/accented.py 1 0 100%\n" + - "-----------------------------------\n" + - "TOTAL 1 0 100%\n" - ) - - cov = coverage.Coverage() - cov.load() - output = self.get_report(cov, squeeze=False) - assert output == report_expected - - @pytest.mark.skipif(env.JYTHON, reason="Jython doesn't like accented file names") - def test_accenteddotpy_not_python(self): - # We run a .py file with a non-ascii name, and when reporting, we can't - # parse it as Python. We should get an error message in the report. - - self.make_data_file(lines={"accented\xe2.py": [1]}) - self.make_file("accented\xe2.py", "This isn't python at all!") - cov = coverage.Coverage() - cov.load() - msg = r"Couldn't parse '.*[/\\]accented\xe2.py' as Python source: '.*' at line 1" - with pytest.raises(NotPython, match=msg): - self.get_report(cov, morfs=["accented\xe2.py"]) - - def test_dotpy_not_python_ignored(self): - # We run a .py file, and when reporting, we can't parse it as Python, - # but we've said to ignore errors, so there's no error reported, - # though we still get a warning. - self.make_file("mycode.py", "This isn't python at all!") - self.make_data_file(lines={"mycode.py": [1]}) - cov = coverage.Coverage() - cov.load() - with pytest.raises(NoDataError, match="No data to report."): - with pytest.warns(Warning) as warns: - self.get_report(cov, morfs=["mycode.py"], ignore_errors=True) - assert_coverage_warnings( - warns, - re.compile(r"Couldn't parse Python file '.*[/\\]mycode.py' \(couldnt-parse\)"), - ) - - def test_dothtml_not_python(self): - # We run a .html file, and when reporting, we can't parse it as - # Python. Since it wasn't .py, no error is reported. - - # Pretend to run an html file. - self.make_file("mycode.html", "<h1>This isn't python at all!</h1>") - self.make_data_file(lines={"mycode.html": [1]}) - cov = coverage.Coverage() - cov.load() - with pytest.raises(NoDataError, match="No data to report."): - self.get_report(cov, morfs=["mycode.html"]) - - def test_report_no_extension(self): - self.make_file("xxx", """\ - # This is a python file though it doesn't look like it, like a main script. - a = b = c = d = 0 - a = 3 - b = 4 - if not b: - c = 6 - d = 7 - print(f"xxx: {a} {b} {c} {d}") - """) - self.make_data_file(lines={abs_file("xxx"): [2, 3, 4, 5, 7, 8]}) - cov = coverage.Coverage() - cov.load() - report = self.get_report(cov) - assert self.last_line_squeezed(report) == "TOTAL 7 1 86%" - - def test_report_with_chdir(self): - self.make_file("chdir.py", """\ - import os - print("Line One") - os.chdir("subdir") - print("Line Two") - print(open("something").read()) - """) - self.make_file("subdir/something", "hello") - out = self.run_command("coverage run --source=. chdir.py") - assert out == "Line One\nLine Two\nhello\n" - report = self.report_from_command("coverage report") - assert self.last_line_squeezed(report) == "TOTAL 5 0 100%" - - def test_bug_156_file_not_run_should_be_zero(self): - # https://github.com/nedbat/coveragepy/issues/156 - self.make_file("mybranch.py", """\ - def branch(x): - if x: - print("x") - return x - branch(1) - """) - self.make_file("main.py", """\ - print("y") - """) - cov = coverage.Coverage(branch=True, source=["."]) - self.start_import_stop(cov, "main") - report = self.get_report(cov).splitlines() - assert "mybranch.py 5 5 2 0 0%" in report - - def run_TheCode_and_report_it(self): - """A helper for the next few tests.""" - cov = coverage.Coverage() - self.start_import_stop(cov, "TheCode") - return self.get_report(cov) - - def test_bug_203_mixed_case_listed_twice_with_rc(self): - self.make_file("TheCode.py", "a = 1\n") - self.make_file(".coveragerc", "[run]\nsource = .\n") - - report = self.run_TheCode_and_report_it() - - assert "TheCode" in report - assert "thecode" not in report - - def test_bug_203_mixed_case_listed_twice(self): - self.make_file("TheCode.py", "a = 1\n") - - report = self.run_TheCode_and_report_it() - - assert "TheCode" in report - assert "thecode" not in report - - @pytest.mark.skipif(not env.WINDOWS, reason=".pyw files are only on Windows.") - def test_pyw_files(self): - # https://github.com/nedbat/coveragepy/issues/261 - self.make_file("start.pyw", """\ - import mod - print("In start.pyw") - """) - self.make_file("mod.pyw", """\ - print("In mod.pyw") - """) - cov = coverage.Coverage() - # start_import_stop can't import the .pyw file, so use the long form. - cov.start() - import start # pragma: nested # pylint: disable=import-error, unused-import - cov.stop() # pragma: nested - - report = self.get_report(cov) - assert "NoSource" not in report - report = report.splitlines() - assert "start.pyw 2 0 100%" in report - assert "mod.pyw 1 0 100%" in report - - def test_tracing_pyc_file(self): - # Create two Python files. - self.make_file("mod.py", "a = 1\n") - self.make_file("main.py", "import mod\n") - - # Make one into a .pyc. - py_compile.compile("mod.py") - - # Run the program. - cov = coverage.Coverage() - self.start_import_stop(cov, "main") - - report = self.get_report(cov).splitlines() - assert "mod.py 1 0 100%" in report - - def test_missing_py_file_during_run(self): - # Create two Python files. - self.make_file("mod.py", "a = 1\n") - self.make_file("main.py", "import mod\n") - - # Make one into a .pyc, and remove the .py. - py_compile.compile("mod.py") - os.remove("mod.py") - - # Python 3 puts the .pyc files in a __pycache__ directory, and will - # not import from there without source. It will import a .pyc from - # the source location though. - if not env.JYTHON: - pycs = glob.glob("__pycache__/mod.*.pyc") - assert len(pycs) == 1 - os.rename(pycs[0], "mod.pyc") - - # Run the program. - cov = coverage.Coverage() - self.start_import_stop(cov, "main") - - # Put back the missing Python file. - self.make_file("mod.py", "a = 1\n") - report = self.get_report(cov).splitlines() - assert "mod.py 1 0 100%" in report - - def test_empty_files(self): - # Shows that empty files like __init__.py are listed as having zero - # statements, not one statement. - cov = coverage.Coverage(branch=True) - cov.start() - import usepkgs # pragma: nested # pylint: disable=import-error, unused-import - cov.stop() # pragma: nested - report = self.get_report(cov) - assert "tests/modules/pkg1/__init__.py 1 0 0 0 100%" in report - assert "tests/modules/pkg2/__init__.py 0 0 0 0 100%" in report - - -class ReportingReturnValueTest(CoverageTest): - """Tests of reporting functions returning values.""" - - def run_coverage(self): - """Run coverage on doit.py and return the coverage object.""" - self.make_file("doit.py", """\ - a = 1 - b = 2 - c = 3 - d = 4 - if a > 10: - f = 6 - g = 7 - """) - - cov = coverage.Coverage() - self.start_import_stop(cov, "doit") - return cov - - def test_report(self): - cov = self.run_coverage() - val = cov.report(include="*/doit.py") - assert math.isclose(val, 6 / 7 * 100) - - def test_html(self): - cov = self.run_coverage() - val = cov.html_report(include="*/doit.py") - assert math.isclose(val, 6 / 7 * 100) - - def test_xml(self): - cov = self.run_coverage() - val = cov.xml_report(include="*/doit.py") - assert math.isclose(val, 6 / 7 * 100) - - -class SummaryReporterConfigurationTest(CoverageTest): - """Tests of SummaryReporter.""" - - def make_rigged_file(self, filename, stmts, miss): - """Create a file that will have specific results. - - `stmts` and `miss` are ints, the number of statements, and - missed statements that should result. - """ - run = stmts - miss - 1 - dont_run = miss - source = "" - source += "a = 1\n" * run - source += "if a == 99:\n" - source += " a = 2\n" * dont_run - self.make_file(filename, source) - - def get_summary_text(self, *options): - """Get text output from the SummaryReporter. - - The arguments are tuples: (name, value) for Coverage.set_option. - """ - self.make_rigged_file("file1.py", 339, 155) - self.make_rigged_file("file2.py", 13, 3) - self.make_rigged_file("file10.py", 234, 228) - self.make_file("doit.py", "import file1, file2, file10") - - cov = Coverage(source=["."], omit=["doit.py"]) - self.start_import_stop(cov, "doit") - for name, value in options: - cov.set_option(name, value) - printer = SummaryReporter(cov) - destination = io.StringIO() - printer.report([], destination) - return destination.getvalue() - - def test_test_data(self): - # We use our own test files as test data. Check that our assumptions - # about them are still valid. We want the three columns of numbers to - # sort in three different orders. - report = self.get_summary_text() - print(report) - # Name Stmts Miss Cover - # ------------------------------ - # file1.py 339 155 54% - # file2.py 13 3 77% - # file10.py 234 228 3% - # ------------------------------ - # TOTAL 586 386 34% - - lines = report.splitlines()[2:-2] - assert len(lines) == 3 - nums = [list(map(int, l.replace('%', '').split()[1:])) for l in lines] - # [ - # [339, 155, 54], - # [ 13, 3, 77], - # [234, 228, 3] - # ] - assert nums[1][0] < nums[2][0] < nums[0][0] - assert nums[1][1] < nums[0][1] < nums[2][1] - assert nums[2][2] < nums[0][2] < nums[1][2] - - def test_defaults(self): - """Run the report with no configuration options.""" - report = self.get_summary_text() - assert 'Missing' not in report - assert 'Branch' not in report - - def test_print_missing(self): - """Run the report printing the missing lines.""" - report = self.get_summary_text(('report:show_missing', True)) - assert 'Missing' in report - assert 'Branch' not in report - - def assert_ordering(self, text, *words): - """Assert that the `words` appear in order in `text`.""" - indexes = list(map(text.find, words)) - assert -1 not in indexes - msg = f"The words {words!r} don't appear in order in {text!r}" - assert indexes == sorted(indexes), msg - - def test_default_sort_report(self): - # Sort the text report by the default (Name) column. - report = self.get_summary_text() - self.assert_ordering(report, "file1.py", "file2.py", "file10.py") - - def test_sort_report_by_name(self): - # Sort the text report explicitly by the Name column. - report = self.get_summary_text(('report:sort', 'Name')) - self.assert_ordering(report, "file1.py", "file2.py", "file10.py") - - def test_sort_report_by_stmts(self): - # Sort the text report by the Stmts column. - report = self.get_summary_text(('report:sort', 'Stmts')) - self.assert_ordering(report, "file2.py", "file10.py", "file1.py") - - def test_sort_report_by_missing(self): - # Sort the text report by the Missing column. - report = self.get_summary_text(('report:sort', 'Miss')) - self.assert_ordering(report, "file2.py", "file1.py", "file10.py") - - def test_sort_report_by_cover(self): - # Sort the text report by the Cover column. - report = self.get_summary_text(('report:sort', 'Cover')) - self.assert_ordering(report, "file10.py", "file1.py", "file2.py") - - def test_sort_report_by_cover_plus(self): - # Sort the text report by the Cover column, including the explicit + sign. - report = self.get_summary_text(('report:sort', '+Cover')) - self.assert_ordering(report, "file10.py", "file1.py", "file2.py") - - def test_sort_report_by_cover_reversed(self): - # Sort the text report by the Cover column reversed. - report = self.get_summary_text(('report:sort', '-Cover')) - self.assert_ordering(report, "file2.py", "file1.py", "file10.py") - - def test_sort_report_by_invalid_option(self): - # Sort the text report by a nonsense column. - msg = "Invalid sorting option: 'Xyzzy'" - with pytest.raises(ConfigError, match=msg): - self.get_summary_text(('report:sort', 'Xyzzy')) diff -Nru python-coverage-6.5.0+dfsg1/tests/test_templite.py python-coverage-7.2.7+dfsg1/tests/test_templite.py --- python-coverage-6.5.0+dfsg1/tests/test_templite.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_templite.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,8 +3,13 @@ """Tests for coverage.templite.""" +from __future__ import annotations + import re +from types import SimpleNamespace +from typing import Any, ContextManager, Dict, List, Optional + import pytest from coverage.templite import Templite, TempliteSyntaxError, TempliteValueError @@ -13,23 +18,18 @@ # pylint: disable=possibly-unused-variable -class AnyOldObject: - """Simple testing object. - - Use keyword arguments in the constructor to set attributes on the object. - - """ - def __init__(self, **attrs): - for n, v in attrs.items(): - setattr(self, n, v) - class TempliteTest(CoverageTest): """Tests for Templite.""" run_in_temp_dir = False - def try_render(self, text, ctx=None, result=None): + def try_render( + self, + text: str, + ctx: Optional[Dict[str, Any]] = None, + result: Optional[str] = None, + ) -> None: """Render `text` through `ctx`, and it had better be `result`. Result defaults to None so we can shorten the calls where we expect @@ -42,30 +42,30 @@ assert result is not None assert actual == result - def assertSynErr(self, msg): + def assertSynErr(self, msg: str) -> ContextManager[None]: """Assert that a `TempliteSyntaxError` will happen. A context manager, and the message should be `msg`. """ pat = "^" + re.escape(msg) + "$" - return pytest.raises(TempliteSyntaxError, match=pat) + return pytest.raises(TempliteSyntaxError, match=pat) # type: ignore - def test_passthrough(self): + def test_passthrough(self) -> None: # Strings without variables are passed through unchanged. assert Templite("Hello").render() == "Hello" assert Templite("Hello, 20% fun time!").render() == "Hello, 20% fun time!" - def test_variables(self): + def test_variables(self) -> None: # Variables use {{var}} syntax. self.try_render("Hello, {{name}}!", {'name':'Ned'}, "Hello, Ned!") - def test_undefined_variables(self): + def test_undefined_variables(self) -> None: # Using undefined names is an error. with pytest.raises(Exception, match="'name'"): self.try_render("Hi, {{name}}!") - def test_pipes(self): + def test_pipes(self) -> None: # Variables can be filtered with pipes. data = { 'name': 'Ned', @@ -77,7 +77,7 @@ # Pipes can be concatenated. self.try_render("Hello, {{name|upper|second}}!", data, "Hello, E!") - def test_reusability(self): + def test_reusability(self) -> None: # A single Templite can be used more than once with different data. globs = { 'upper': lambda x: x.upper(), @@ -88,30 +88,30 @@ assert template.render({'name':'Ned'}) == "This is NED!" assert template.render({'name':'Ben'}) == "This is BEN!" - def test_attribute(self): + def test_attribute(self) -> None: # Variables' attributes can be accessed with dots. - obj = AnyOldObject(a="Ay") + obj = SimpleNamespace(a="Ay") self.try_render("{{obj.a}}", locals(), "Ay") - obj2 = AnyOldObject(obj=obj, b="Bee") + obj2 = SimpleNamespace(obj=obj, b="Bee") self.try_render("{{obj2.obj.a}} {{obj2.b}}", locals(), "Ay Bee") - def test_member_function(self): + def test_member_function(self) -> None: # Variables' member functions can be used, as long as they are nullary. - class WithMemberFns(AnyOldObject): + class WithMemberFns(SimpleNamespace): """A class to try out member function access.""" - def ditto(self): + def ditto(self) -> str: """Return twice the .txt attribute.""" - return self.txt + self.txt + return self.txt + self.txt # type: ignore obj = WithMemberFns(txt="Once") self.try_render("{{obj.ditto}}", locals(), "OnceOnce") - def test_item_access(self): + def test_item_access(self) -> None: # Variables' items can be used. d = {'a':17, 'b':23} self.try_render("{{d.a}} < {{d.b}}", locals(), "17 < 23") - def test_loops(self): + def test_loops(self) -> None: # Loops work like in Django. nums = [1,2,3,4] self.try_render( @@ -120,7 +120,7 @@ "Look: 1, 2, 3, 4, done." ) # Loop iterables can be filtered. - def rev(l): + def rev(l: List[int]) -> List[int]: """Return the reverse of `l`.""" l = l[:] l.reverse() @@ -132,21 +132,21 @@ "Look: 4, 3, 2, 1, done." ) - def test_empty_loops(self): + def test_empty_loops(self) -> None: self.try_render( "Empty: {% for n in nums %}{{n}}, {% endfor %}done.", {'nums':[]}, "Empty: done." ) - def test_multiline_loops(self): + def test_multiline_loops(self) -> None: self.try_render( "Look: \n{% for n in nums %}\n{{n}}, \n{% endfor %}done.", {'nums':[1,2,3]}, "Look: \n\n1, \n\n2, \n\n3, \ndone." ) - def test_multiple_loops(self): + def test_multiple_loops(self) -> None: self.try_render( "{% for n in nums %}{{n}}{% endfor %} and " + "{% for n in nums %}{{n}}{% endfor %}", @@ -154,7 +154,7 @@ "123 and 123" ) - def test_comments(self): + def test_comments(self) -> None: # Single-line comments work: self.try_render( "Hello, {# Name goes here: #}{{name}}!", @@ -166,7 +166,7 @@ {'name':'Ned'}, "Hello, Ned!" ) - def test_if(self): + def test_if(self) -> None: self.try_render( "Hi, {% if ned %}NED{% endif %}{% if ben %}BEN{% endif %}!", {'ned': 1, 'ben': 0}, @@ -193,10 +193,10 @@ "Hi, NEDBEN!" ) - def test_complex_if(self): - class Complex(AnyOldObject): + def test_complex_if(self) -> None: + class Complex(SimpleNamespace): """A class to try out complex data access.""" - def getit(self): + def getit(self): # type: ignore """Return it.""" return self.it obj = Complex(it={'x':"Hello", 'y': 0}) @@ -210,7 +210,7 @@ "@XS!" ) - def test_loop_if(self): + def test_loop_if(self) -> None: self.try_render( "@{% for n in nums %}{% if n %}Z{% endif %}{{n}}{% endfor %}!", {'nums': [0,1,2]}, @@ -227,7 +227,7 @@ "X!" ) - def test_nested_loops(self): + def test_nested_loops(self) -> None: self.try_render( "@" + "{% for n in nums %}" + @@ -238,7 +238,7 @@ "@a0b0c0a1b1c1a2b2c2!" ) - def test_whitespace_handling(self): + def test_whitespace_handling(self) -> None: self.try_render( "@{% for n in nums %}\n" + " {% for a in abc %}{{a}}{{n}}{% endfor %}\n" + @@ -268,7 +268,7 @@ ) self.try_render(" hello ", {}, " hello ") - def test_eat_whitespace(self): + def test_eat_whitespace(self) -> None: self.try_render( "Hey!\n" + "{% joined %}\n" + @@ -286,14 +286,14 @@ "Hey!\n@XYa0XYb0XYc0XYa1XYb1XYc1XYa2XYb2XYc2!\n" ) - def test_non_ascii(self): + def test_non_ascii(self) -> None: self.try_render( "{{where}} ollǝɥ", { 'where': 'ǝɹǝɥʇ' }, "ǝɹǝɥʇ ollǝɥ" ) - def test_exception_during_evaluation(self): + def test_exception_during_evaluation(self) -> None: # TypeError: Couldn't evaluate {{ foo.bar.baz }}: regex = "^Couldn't evaluate None.bar$" with pytest.raises(TempliteValueError, match=regex): @@ -301,7 +301,7 @@ "Hey {{foo.bar.baz}} there", {'foo': None}, "Hey ??? there" ) - def test_bad_names(self): + def test_bad_names(self) -> None: with self.assertSynErr("Not a valid name: 'var%&!@'"): self.try_render("Wat: {{ var%&!@ }}") with self.assertSynErr("Not a valid name: 'filter%&!@'"): @@ -309,17 +309,17 @@ with self.assertSynErr("Not a valid name: '@'"): self.try_render("Wat: {% for @ in x %}{% endfor %}") - def test_bogus_tag_syntax(self): + def test_bogus_tag_syntax(self) -> None: with self.assertSynErr("Don't understand tag: 'bogus'"): self.try_render("Huh: {% bogus %}!!{% endbogus %}??") - def test_malformed_if(self): + def test_malformed_if(self) -> None: with self.assertSynErr("Don't understand if: '{% if %}'"): self.try_render("Buh? {% if %}hi!{% endif %}") with self.assertSynErr("Don't understand if: '{% if this or that %}'"): self.try_render("Buh? {% if this or that %}hi!{% endif %}") - def test_malformed_for(self): + def test_malformed_for(self) -> None: with self.assertSynErr("Don't understand for: '{% for %}'"): self.try_render("Weird: {% for %}loop{% endfor %}") with self.assertSynErr("Don't understand for: '{% for x from y %}'"): @@ -327,7 +327,7 @@ with self.assertSynErr("Don't understand for: '{% for x, y in z %}'"): self.try_render("Weird: {% for x, y in z %}loop{% endfor %}") - def test_bad_nesting(self): + def test_bad_nesting(self) -> None: with self.assertSynErr("Unmatched action tag: 'if'"): self.try_render("{% if x %}X") with self.assertSynErr("Mismatched end tag: 'for'"): @@ -335,7 +335,7 @@ with self.assertSynErr("Too many ends: '{% endif %}'"): self.try_render("{% if x %}{% endif %}{% endif %}") - def test_malformed_end(self): + def test_malformed_end(self) -> None: with self.assertSynErr("Don't understand end: '{% end if %}'"): self.try_render("{% if x %}X{% end if %}") with self.assertSynErr("Don't understand end: '{% endif now %}'"): diff -Nru python-coverage-6.5.0+dfsg1/tests/test_testing.py python-coverage-7.2.7+dfsg1/tests/test_testing.py --- python-coverage-6.5.0+dfsg1/tests/test_testing.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_testing.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,33 +3,37 @@ """Tests that our test infrastructure is really working!""" +from __future__ import annotations + import datetime import os import re import sys import warnings +from typing import List, Tuple + import pytest import coverage -from coverage import tomlconfig from coverage.exceptions import CoverageWarning from coverage.files import actual_path +from coverage.types import TArc from tests.coveragetest import CoverageTest from tests.helpers import ( arcs_to_arcz_repr, arcz_to_arcs, assert_count_equal, assert_coverage_warnings, - CheckUniqueFilenames, re_lines, re_lines_text, re_line, without_module, + CheckUniqueFilenames, re_lines, re_lines_text, re_line, ) -def test_xdist_sys_path_nuttiness_is_fixed(): +def test_xdist_sys_path_nuttiness_is_fixed() -> None: # See conftest.py:fix_xdist_sys_path assert sys.path[1] != '' assert os.environ.get('PYTHONPATH') is None -def test_assert_count_equal(): +def test_assert_count_equal() -> None: assert_count_equal(set(), set()) assert_count_equal({"a": 1, "b": 2}, ["b", "a"]) with pytest.raises(AssertionError): @@ -41,7 +45,7 @@ class CoverageTestTest(CoverageTest): """Test the methods in `CoverageTest`.""" - def test_file_exists(self): + def test_file_exists(self) -> None: self.make_file("whoville.txt", "We are here!") self.assert_exists("whoville.txt") self.assert_doesnt_exist("shadow.txt") @@ -52,7 +56,7 @@ with pytest.raises(AssertionError, match=msg): self.assert_exists("shadow.txt") - def test_file_count(self): + def test_file_count(self) -> None: self.make_file("abcde.txt", "abcde") self.make_file("axczz.txt", "axczz") self.make_file("afile.txt", "afile") @@ -83,8 +87,8 @@ with pytest.raises(AssertionError, match=msg): self.assert_file_count("*.q", 10) - def test_assert_recent_datetime(self): - def now_delta(seconds): + def test_assert_recent_datetime(self) -> None: + def now_delta(seconds: int) -> datetime.datetime: """Make a datetime `seconds` seconds from now.""" return datetime.datetime.now() + datetime.timedelta(seconds=seconds) @@ -104,7 +108,7 @@ with pytest.raises(AssertionError): self.assert_recent_datetime(now_delta(1), seconds=120) - def test_assert_warnings(self): + def test_assert_warnings(self) -> None: cov = coverage.Coverage() # Make a warning, it should catch it properly. @@ -153,7 +157,7 @@ with self.assert_warnings(cov, ["Hello there!"]): raise ZeroDivisionError("oops") - def test_assert_no_warnings(self): + def test_assert_no_warnings(self) -> None: cov = coverage.Coverage() # Happy path: no warnings. @@ -166,7 +170,7 @@ with self.assert_warnings(cov, []): cov._warn("Watch out!") - def test_sub_python_is_this_python(self): + def test_sub_python_is_this_python(self) -> None: # Try it with a Python command. self.set_environ('COV_FOOBAR', 'XYZZY') self.make_file("showme.py", """\ @@ -175,10 +179,10 @@ print(os.__file__) print(os.environ['COV_FOOBAR']) """) - out = self.run_command("python showme.py").splitlines() - assert actual_path(out[0]) == actual_path(sys.executable) - assert out[1] == os.__file__ - assert out[2] == 'XYZZY' + out_lines = self.run_command("python showme.py").splitlines() + assert actual_path(out_lines[0]) == actual_path(sys.executable) + assert out_lines[1] == os.__file__ + assert out_lines[2] == 'XYZZY' # Try it with a "coverage debug sys" command. out = self.run_command("coverage debug sys") @@ -192,7 +196,7 @@ _, _, environ = environ.rpartition(":") assert environ.strip() == "COV_FOOBAR = XYZZY" - def test_run_command_stdout_stderr(self): + def test_run_command_stdout_stderr(self) -> None: # run_command should give us both stdout and stderr. self.make_file("outputs.py", """\ import sys @@ -203,7 +207,7 @@ assert "StdOut\n" in out assert "StdErr\n" in out - def test_stdout(self): + def test_stdout(self) -> None: # stdout is captured. print("This is stdout") print("Line 2") @@ -220,14 +224,19 @@ class Stub: """A stand-in for the class we're checking.""" - def __init__(self, x): + def __init__(self, x: int) -> None: self.x = x - def method(self, filename, a=17, b="hello"): + def method( + self, + filename: str, + a: int = 17, + b: str = "hello", + ) -> Tuple[int, str, int, str]: """The method we'll wrap, with args to be sure args work.""" return (self.x, filename, a, b) - def test_detect_duplicate(self): + def test_detect_duplicate(self) -> None: stub = self.Stub(23) CheckUniqueFilenames.hook(stub, "method") @@ -260,7 +269,7 @@ ARCZ_MISSING = "3-2 78 8B" ARCZ_UNPREDICTED = "79" - def test_check_coverage_possible(self): + def test_check_coverage_possible(self) -> None: msg = r"(?s)Possible arcs differ: .*- \(6, 3\).*\+ \(6, 7\)" with pytest.raises(AssertionError, match=msg): self.check_coverage( @@ -270,7 +279,7 @@ arcz_unpredicted=self.ARCZ_UNPREDICTED, ) - def test_check_coverage_missing(self): + def test_check_coverage_missing(self) -> None: msg = r"(?s)Missing arcs differ: .*- \(3, 8\).*\+ \(7, 8\)" with pytest.raises(AssertionError, match=msg): self.check_coverage( @@ -280,7 +289,7 @@ arcz_unpredicted=self.ARCZ_UNPREDICTED, ) - def test_check_coverage_unpredicted(self): + def test_check_coverage_unpredicted(self) -> None: msg = r"(?s)Unpredicted arcs differ: .*- \(3, 9\).*\+ \(7, 9\)" with pytest.raises(AssertionError, match=msg): self.check_coverage( @@ -301,7 +310,7 @@ ("[13]", "line1\nline2\nline3\n", "line1\nline3\n"), ("X", "line1\nline2\nline3\n", ""), ]) - def test_re_lines(self, pat, text, result): + def test_re_lines(self, pat: str, text: str, result: str) -> None: assert re_lines_text(pat, text) == result assert re_lines(pat, text) == result.splitlines() @@ -310,26 +319,26 @@ ("[13]", "line1\nline2\nline3\n", "line2\n"), ("X", "line1\nline2\nline3\n", "line1\nline2\nline3\n"), ]) - def test_re_lines_inverted(self, pat, text, result): + def test_re_lines_inverted(self, pat: str, text: str, result: str) -> None: assert re_lines_text(pat, text, match=False) == result assert re_lines(pat, text, match=False) == result.splitlines() @pytest.mark.parametrize("pat, text, result", [ ("2", "line1\nline2\nline3\n", "line2"), ]) - def test_re_line(self, pat, text, result): + def test_re_line(self, pat: str, text: str, result: str) -> None: assert re_line(pat, text) == result @pytest.mark.parametrize("pat, text", [ ("line", "line1\nline2\nline3\n"), # too many matches ("X", "line1\nline2\nline3\n"), # no matches ]) - def test_re_line_bad(self, pat, text): + def test_re_line_bad(self, pat: str, text: str) -> None: with pytest.raises(AssertionError): re_line(pat, text) -def _same_python_executable(e1, e2): +def _same_python_executable(e1: str, e2: str) -> bool: """Determine if `e1` and `e2` refer to the same Python executable. Either path could include symbolic links. The two paths might not refer @@ -356,16 +365,6 @@ return False # pragma: only failure -def test_without_module(): - toml1 = tomlconfig.tomllib - with without_module(tomlconfig, 'tomllib'): - toml2 = tomlconfig.tomllib - toml3 = tomlconfig.tomllib - - assert toml1 is toml3 is not None - assert toml2 is None - - class ArczTest(CoverageTest): """Tests of arcz/arcs helpers.""" @@ -376,7 +375,7 @@ ("-11 12 2-5", [(-1, 1), (1, 2), (2, -5)]), ("-QA CB IT Z-A", [(-26, 10), (12, 11), (18, 29), (35, -10)]), ]) - def test_arcz_to_arcs(self, arcz, arcs): + def test_arcz_to_arcs(self, arcz: str, arcs: List[TArc]) -> None: assert arcz_to_arcs(arcz) == arcs @pytest.mark.parametrize("arcs, arcz_repr", [ @@ -393,45 +392,45 @@ ) ), ]) - def test_arcs_to_arcz_repr(self, arcs, arcz_repr): + def test_arcs_to_arcz_repr(self, arcs: List[TArc], arcz_repr: str) -> None: assert arcs_to_arcz_repr(arcs) == arcz_repr class AssertCoverageWarningsTest(CoverageTest): """Tests of assert_coverage_warnings""" - def test_one_warning(self): + def test_one_warning(self) -> None: with pytest.warns(Warning) as warns: warnings.warn("Hello there", category=CoverageWarning) assert_coverage_warnings(warns, "Hello there") - def test_many_warnings(self): + def test_many_warnings(self) -> None: with pytest.warns(Warning) as warns: warnings.warn("The first", category=CoverageWarning) warnings.warn("The second", category=CoverageWarning) warnings.warn("The third", category=CoverageWarning) assert_coverage_warnings(warns, "The first", "The second", "The third") - def test_wrong_type(self): + def test_wrong_type(self) -> None: with pytest.warns(Warning) as warns: warnings.warn("Not ours", category=Warning) with pytest.raises(AssertionError): assert_coverage_warnings(warns, "Not ours") - def test_wrong_message(self): + def test_wrong_message(self) -> None: with pytest.warns(Warning) as warns: warnings.warn("Goodbye", category=CoverageWarning) with pytest.raises(AssertionError): assert_coverage_warnings(warns, "Hello there") - def test_wrong_number_too_many(self): + def test_wrong_number_too_many(self) -> None: with pytest.warns(Warning) as warns: warnings.warn("The first", category=CoverageWarning) warnings.warn("The second", category=CoverageWarning) with pytest.raises(AssertionError): assert_coverage_warnings(warns, "The first", "The second", "The third") - def test_wrong_number_too_few(self): + def test_wrong_number_too_few(self) -> None: with pytest.warns(Warning) as warns: warnings.warn("The first", category=CoverageWarning) warnings.warn("The second", category=CoverageWarning) @@ -439,12 +438,12 @@ with pytest.raises(AssertionError): assert_coverage_warnings(warns, "The first", "The second") - def test_regex_matches(self): + def test_regex_matches(self) -> None: with pytest.warns(Warning) as warns: warnings.warn("The first", category=CoverageWarning) assert_coverage_warnings(warns, re.compile("f?rst")) - def test_regex_doesnt_match(self): + def test_regex_doesnt_match(self) -> None: with pytest.warns(Warning) as warns: warnings.warn("The first", category=CoverageWarning) with pytest.raises(AssertionError): diff -Nru python-coverage-6.5.0+dfsg1/tests/test_venv.py python-coverage-7.2.7+dfsg1/tests/test_venv.py --- python-coverage-6.5.0+dfsg1/tests/test_venv.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_venv.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,10 +3,14 @@ """Tests about understanding how third-party code is installed.""" +from __future__ import annotations + import os import os.path import shutil -import sys + +from pathlib import Path +from typing import Iterator, cast import pytest @@ -17,7 +21,7 @@ from tests.helpers import re_lines, run_command -def run_in_venv(cmd): +def run_in_venv(cmd: str) -> str: r"""Run `cmd` in the virtualenv at `venv`. The first word of the command will be adjusted to run it from the @@ -38,7 +42,7 @@ @pytest.fixture(scope="session", name="venv_world") -def venv_world_fixture(tmp_path_factory): +def venv_world_fixture(tmp_path_factory: pytest.TempPathFactory) -> Path: """Create a virtualenv with a few test packages for VirtualenvTest to use. Returns the directory containing the "venv" virtualenv. @@ -112,8 +116,12 @@ __path__ = extend_path(__path__, __name__) """) make_file("bug888/app/testcov/main.py", """\ - import pkg_resources - for entry_point in pkg_resources.iter_entry_points('plugins'): + try: # pragma: no cover + entry_points = __import__("pkg_resources").iter_entry_points('plugins') + except ImportError: # pragma: no cover + import importlib.metadata + entry_points = importlib.metadata.entry_points(group="plugins") + for entry_point in entry_points: entry_point.load()() """) make_file("bug888/plugin/setup.py", """\ @@ -154,24 +162,18 @@ "coverage", "python -m coverage", ], name="coverage_command") -def coverage_command_fixture(request): +def coverage_command_fixture(request: pytest.FixtureRequest) -> str: """Parametrized fixture to use multiple forms of "coverage" command.""" - return request.param + return cast(str, request.param) -# https://bugs.python.org/issue46028 -@pytest.mark.xfail( - (3, 11, 0, 'alpha', 4, 0) == env.PYVERSION and - not os.path.exists(sys._base_executable), - reason="avoid 3.11 bug: bpo46028" -) class VirtualenvTest(CoverageTest): """Tests of virtualenv considerations.""" expected_stdout = "33\n110\n198\n1.5\n" @pytest.fixture(autouse=True) - def in_venv_world_fixture(self, venv_world): + def in_venv_world_fixture(self, venv_world: Path) -> Iterator[None]: """For running tests inside venv_world, and cleaning up made files.""" with change_dir(venv_world): self.make_file("myproduct.py", """\ @@ -185,7 +187,7 @@ print(sum(colorsys.rgb_to_hls(1, 0, 0))) """) - self.del_environ("COVERAGE_TESTING") # To avoid needing contracts installed. + self.del_environ("COVERAGE_TESTING") # To get realistic behavior self.set_environ("COVERAGE_DEBUG_FILE", "debug_out.txt") self.set_environ("COVERAGE_DEBUG", "trace") @@ -195,13 +197,33 @@ if fname not in {"venv", "another_pkg", "bug888"}: os.remove(fname) - def get_trace_output(self): + def get_trace_output(self) -> str: """Get the debug output of coverage.py""" with open("debug_out.txt") as f: return f.read() - def test_third_party_venv_isnt_measured(self, coverage_command): - out = run_in_venv(coverage_command + " run --source=. myproduct.py") + @pytest.mark.parametrize('install_source_in_venv', [True, False]) + def test_third_party_venv_isnt_measured( + self, coverage_command: str, install_source_in_venv: bool + ) -> None: + if install_source_in_venv: + make_file("setup.py", """\ + import setuptools + setuptools.setup( + name="myproduct", + py_modules = ["myproduct"], + ) + """) + try: + run_in_venv("python -m pip install .") + finally: + shutil.rmtree("build", ignore_errors=True) + shutil.rmtree("myproduct.egg-info", ignore_errors=True) + # Ensure that coverage doesn't run the non-installed module. + os.remove('myproduct.py') + out = run_in_venv(coverage_command + " run --source=.,myproduct -m myproduct") + else: + out = run_in_venv(coverage_command + " run --source=. myproduct.py") # In particular, this warning doesn't appear: # Already imported a file that will be measured: .../coverage/__main__.py assert out == self.expected_stdout @@ -215,7 +237,7 @@ ) assert re_lines(r"^Tracing .*\bmyproduct.py", debug_out) assert re_lines( - r"^Not tracing .*\bcolorsys.py': falls outside the --source spec", + r"^Not tracing .*\bcolorsys.py': (module 'colorsys' |)?falls outside the --source spec", debug_out, ) @@ -225,7 +247,7 @@ assert "coverage" not in out assert "colorsys" not in out - def test_us_in_venv_isnt_measured(self, coverage_command): + def test_us_in_venv_isnt_measured(self, coverage_command: str) -> None: out = run_in_venv(coverage_command + " run --source=third myproduct.py") assert out == self.expected_stdout @@ -252,7 +274,7 @@ assert "coverage" not in out assert "colorsys" not in out - def test_venv_isnt_measured(self, coverage_command): + def test_venv_isnt_measured(self, coverage_command: str) -> None: out = run_in_venv(coverage_command + " run myproduct.py") assert out == self.expected_stdout @@ -268,7 +290,7 @@ assert "colorsys" not in out @pytest.mark.skipif(not env.C_TRACER, reason="Plugins are only supported with the C tracer.") - def test_venv_with_dynamic_plugin(self, coverage_command): + def test_venv_with_dynamic_plugin(self, coverage_command: str) -> None: # https://github.com/nedbat/coveragepy/issues/1150 # Django coverage plugin was incorrectly getting warnings: # "Already imported: ... django/template/blah.py" @@ -284,7 +306,7 @@ # Already imported a file that will be measured: ...third/render.py (already-imported) assert out == "HTML: hello.html@1723\n" - def test_installed_namespace_packages(self, coverage_command): + def test_installed_namespace_packages(self, coverage_command: str) -> None: # https://github.com/nedbat/coveragepy/issues/1231 # When namespace packages were installed, they were considered # third-party packages. Test that isn't still happening. @@ -326,7 +348,7 @@ assert "fifth" in out assert "sixth" in out - def test_bug_888(self, coverage_command): + def test_bug_888(self, coverage_command: str) -> None: out = run_in_venv( coverage_command + " run --source=bug888/app,bug888/plugin bug888/app/testcov/main.py" diff -Nru python-coverage-6.5.0+dfsg1/tests/test_version.py python-coverage-7.2.7+dfsg1/tests/test_version.py --- python-coverage-6.5.0+dfsg1/tests/test_version.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_version.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,6 +3,8 @@ """Tests of version.py.""" +from __future__ import annotations + import coverage from coverage.version import _make_url, _make_version @@ -14,22 +16,28 @@ run_in_temp_dir = False - def test_version_info(self): + def test_version_info(self) -> None: # Make sure we didn't screw up the version_info tuple. assert isinstance(coverage.version_info, tuple) assert [type(d) for d in coverage.version_info] == [int, int, int, str, int] - assert coverage.version_info[3] in ['alpha', 'beta', 'candidate', 'final'] + assert coverage.version_info[3] in {'alpha', 'beta', 'candidate', 'final'} - def test_make_version(self): - assert _make_version(4, 0, 0, 'alpha', 0) == "4.0.0a0" + def test_make_version(self) -> None: + assert _make_version(4, 0, 0, 'alpha') == "4.0.0a0" assert _make_version(4, 0, 0, 'alpha', 1) == "4.0.0a1" - assert _make_version(4, 0, 0, 'final', 0) == "4.0.0" - assert _make_version(4, 1, 0, 'final', 0) == "4.1.0" + assert _make_version(4, 0, 0, 'final') == "4.0.0" + assert _make_version(4, 1, 0) == "4.1.0" assert _make_version(4, 1, 2, 'beta', 3) == "4.1.2b3" - assert _make_version(4, 1, 2, 'final', 0) == "4.1.2" + assert _make_version(4, 1, 2) == "4.1.2" assert _make_version(5, 10, 2, 'candidate', 7) == "5.10.2rc7" + assert _make_version(5, 10, 2, 'candidate', 7, 3) == "5.10.2rc7.dev3" - def test_make_url(self): - assert _make_url(4, 0, 0, 'final', 0) == "https://coverage.readthedocs.io" + def test_make_url(self) -> None: + expected = "https://coverage.readthedocs.io/en/4.1.2" + assert _make_url(4, 1, 2, 'final') == expected expected = "https://coverage.readthedocs.io/en/4.1.2b3" assert _make_url(4, 1, 2, 'beta', 3) == expected + expected = "https://coverage.readthedocs.io/en/4.1.2b3.dev17" + assert _make_url(4, 1, 2, 'beta', 3, 17) == expected + expected = "https://coverage.readthedocs.io/en/4.1.2.dev17" + assert _make_url(4, 1, 2, 'final', 0, 17) == expected diff -Nru python-coverage-6.5.0+dfsg1/tests/test_xml.py python-coverage-7.2.7+dfsg1/tests/test_xml.py --- python-coverage-6.5.0+dfsg1/tests/test_xml.py 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tests/test_xml.py 2023-05-29 19:46:30.000000000 +0000 @@ -3,14 +3,19 @@ """Tests for XML reports from coverage.py.""" +from __future__ import annotations + import os import os.path import re + +from typing import Any, Dict, Iterator, Tuple, Union from xml.etree import ElementTree import pytest import coverage +from coverage import Coverage, env from coverage.exceptions import NoDataError from coverage.files import abs_file from coverage.misc import import_local_file @@ -23,7 +28,7 @@ class XmlTestHelpers(CoverageTest): """Methods to use from XML tests.""" - def run_doit(self): + def run_doit(self) -> Coverage: """Construct a simple sub-package.""" self.make_file("sub/__init__.py") self.make_file("sub/doit.py", "print('doit!')") @@ -32,7 +37,7 @@ self.start_import_stop(cov, "main") return cov - def make_tree(self, width, depth, curdir="."): + def make_tree(self, width: int, depth: int, curdir: str = ".") -> None: """Make a tree of packages. Makes `width` directories, named d0 .. d{width-1}. Each directory has @@ -44,7 +49,7 @@ if depth == 0: return - def here(p): + def here(p: str) -> str: """A path for `p` in our currently interesting directory.""" return os.path.join(curdir, p) @@ -57,7 +62,11 @@ filename = here(f"f{i}.py") self.make_file(filename, f"# {filename}\n") - def assert_source(self, xmldom, src): + def assert_source( + self, + xmldom: Union[ElementTree.Element, ElementTree.ElementTree], + src: str, + ) -> None: """Assert that the XML has a <source> element with `src`.""" src = abs_file(src) elts = xmldom.findall(".//sources/source") @@ -69,7 +78,7 @@ run_in_temp_dir = False - def test_assert_source(self): + def test_assert_source(self) -> None: dom = ElementTree.fromstring("""\ <doc> <src>foo</src> @@ -94,24 +103,24 @@ class XmlReportTest(XmlTestHelpers, CoverageTest): """Tests of the XML reports from coverage.py.""" - def make_mycode_data(self): + def make_mycode_data(self) -> None: """Pretend that we ran mycode.py, so we can report on it.""" self.make_file("mycode.py", "print('hello')\n") self.make_data_file(lines={abs_file("mycode.py"): [1]}) - def run_xml_report(self, **kwargs): + def run_xml_report(self, **kwargs: Any) -> None: """Run xml_report()""" cov = coverage.Coverage() cov.load() cov.xml_report(**kwargs) - def test_default_file_placement(self): + def test_default_file_placement(self) -> None: self.make_mycode_data() self.run_xml_report() self.assert_exists("coverage.xml") assert self.stdout() == "" - def test_argument_affects_xml_placement(self): + def test_argument_affects_xml_placement(self) -> None: self.make_mycode_data() cov = coverage.Coverage(messages=True) cov.load() @@ -120,28 +129,28 @@ self.assert_doesnt_exist("coverage.xml") self.assert_exists("put_it_there.xml") - def test_output_directory_does_not_exist(self): + def test_output_directory_does_not_exist(self) -> None: self.make_mycode_data() self.run_xml_report(outfile="nonexistent/put_it_there.xml") self.assert_doesnt_exist("coverage.xml") self.assert_doesnt_exist("put_it_there.xml") self.assert_exists("nonexistent/put_it_there.xml") - def test_config_affects_xml_placement(self): + def test_config_affects_xml_placement(self) -> None: self.make_mycode_data() self.make_file(".coveragerc", "[xml]\noutput = xml.out\n") self.run_xml_report() self.assert_doesnt_exist("coverage.xml") self.assert_exists("xml.out") - def test_no_data(self): + def test_no_data(self) -> None: # https://github.com/nedbat/coveragepy/issues/210 with pytest.raises(NoDataError, match="No data to report."): self.run_xml_report() self.assert_doesnt_exist("coverage.xml") self.assert_doesnt_exist(".coverage") - def test_no_source(self): + def test_no_source(self) -> None: # Written while investigating a bug, might as well keep it. # https://github.com/nedbat/coveragepy/issues/208 self.make_file("innocuous.py", "a = 4") @@ -156,7 +165,7 @@ ) self.assert_exists("coverage.xml") - def test_filename_format_showing_everything(self): + def test_filename_format_showing_everything(self) -> None: cov = self.run_doit() cov.xml_report() dom = ElementTree.parse("coverage.xml") @@ -164,7 +173,7 @@ assert len(elts) == 1 assert elts[0].get('filename') == "sub/doit.py" - def test_filename_format_including_filename(self): + def test_filename_format_including_filename(self) -> None: cov = self.run_doit() cov.xml_report(["sub/doit.py"]) dom = ElementTree.parse("coverage.xml") @@ -172,7 +181,7 @@ assert len(elts) == 1 assert elts[0].get('filename') == "sub/doit.py" - def test_filename_format_including_module(self): + def test_filename_format_including_module(self) -> None: cov = self.run_doit() import sub.doit # pylint: disable=import-error cov.xml_report([sub.doit]) @@ -181,7 +190,7 @@ assert len(elts) == 1 assert elts[0].get('filename') == "sub/doit.py" - def test_reporting_on_nothing(self): + def test_reporting_on_nothing(self) -> None: # Used to raise a zero division error: # https://github.com/nedbat/coveragepy/issues/250 self.make_file("empty.py", "") @@ -194,7 +203,7 @@ assert elts[0].get('filename') == "empty.py" assert elts[0].get('line-rate') == '1' - def test_empty_file_is_100_not_0(self): + def test_empty_file_is_100_not_0(self) -> None: # https://github.com/nedbat/coveragepy/issues/345 cov = self.run_doit() cov.xml_report() @@ -203,14 +212,14 @@ assert len(elts) == 1 assert elts[0].get('line-rate') == '1' - def test_empty_file_is_skipped(self): + def test_empty_file_is_skipped(self) -> None: cov = self.run_doit() cov.xml_report(skip_empty=True) dom = ElementTree.parse("coverage.xml") elts = dom.findall(".//class[@name='__init__.py']") assert len(elts) == 0 - def test_curdir_source(self): + def test_curdir_source(self) -> None: # With no source= option, the XML report should explain that the source # is in the current directory. cov = self.run_doit() @@ -220,7 +229,7 @@ sources = dom.findall(".//source") assert len(sources) == 1 - def test_deep_source(self): + def test_deep_source(self) -> None: # When using source=, the XML report needs to mention those directories # in the <source> elements. # https://github.com/nedbat/coveragepy/issues/439 @@ -264,7 +273,7 @@ 'name': 'bar.py', } - def test_nonascii_directory(self): + def test_nonascii_directory(self) -> None: # https://github.com/nedbat/coveragepy/issues/573 self.make_file("테스트/program.py", "a = 1") with change_dir("테스트"): @@ -272,7 +281,7 @@ self.start_import_stop(cov, "program") cov.xml_report() - def test_accented_dot_py(self): + def test_accented_dot_py(self) -> None: # Make a file with a non-ascii character in the filename. self.make_file("h\xe2t.py", "print('accented')") self.make_data_file(lines={abs_file("h\xe2t.py"): [1]}) @@ -285,7 +294,7 @@ assert ' filename="h\xe2t.py"'.encode() in xml assert ' name="h\xe2t.py"'.encode() in xml - def test_accented_directory(self): + def test_accented_directory(self) -> None: # Make a file with a non-ascii character in the directory name. self.make_file("\xe2/accented.py", "print('accented')") self.make_data_file(lines={abs_file("\xe2/accented.py"): [1]}) @@ -309,8 +318,41 @@ "name": "â", } + def test_no_duplicate_packages(self) -> None: + self.make_file( + "namespace/package/__init__.py", + "from . import sample; from . import test; from .subpackage import test" + ) + self.make_file("namespace/package/sample.py", "print('package.sample')") + self.make_file("namespace/package/test.py", "print('package.test')") + self.make_file("namespace/package/subpackage/test.py", "print('package.subpackage.test')") + + # no source path passed to coverage! + # problem occurs when they are dynamically generated during xml report + cov = coverage.Coverage() -def unbackslash(v): + cov.start() + import_local_file("foo", "namespace/package/__init__.py") # pragma: nested + cov.stop() # pragma: nested + + cov.xml_report() + + dom = ElementTree.parse("coverage.xml") + + # only two packages should be present + packages = dom.findall(".//package") + assert len(packages) == 2 + + # one of them is namespace.package + named_package = dom.findall(".//package[@name='namespace.package']") + assert len(named_package) == 1 + + # the other one namespace.package.subpackage + named_sub_package = dom.findall(".//package[@name='namespace.package.subpackage']") + assert len(named_sub_package) == 1 + + +def unbackslash(v: Any) -> Any: """Find strings in `v`, and replace backslashes with slashes throughout.""" if isinstance(v, (tuple, list)): return [unbackslash(vv) for vv in v] @@ -324,7 +366,7 @@ class XmlPackageStructureTest(XmlTestHelpers, CoverageTest): """Tests about the package structure reported in the coverage.xml file.""" - def package_and_class_tags(self, cov): + def package_and_class_tags(self, cov: Coverage) -> Iterator[Tuple[str, Dict[str, Any]]]: """Run an XML report on `cov`, and get the package and class tags.""" cov.xml_report() dom = ElementTree.parse("coverage.xml") @@ -332,16 +374,16 @@ if node.tag in ('package', 'class'): yield (node.tag, {a:v for a,v in node.items() if a in ('name', 'filename')}) - def assert_package_and_class_tags(self, cov, result): + def assert_package_and_class_tags(self, cov: Coverage, result: Any) -> None: """Check the XML package and class tags from `cov` match `result`.""" assert unbackslash(list(self.package_and_class_tags(cov))) == unbackslash(result) - def test_package_names(self): + def test_package_names(self) -> None: self.make_tree(width=1, depth=3) self.make_file("main.py", """\ from d0.d0 import f0 """) - cov = coverage.Coverage(source=".") + cov = coverage.Coverage(source=["."]) self.start_import_stop(cov, "main") self.assert_package_and_class_tags(cov, [ ('package', {'name': "."}), @@ -354,12 +396,12 @@ ('class', {'filename': "d0/d0/f0.py", 'name': "f0.py"}), ]) - def test_package_depth_1(self): + def test_package_depth_1(self) -> None: self.make_tree(width=1, depth=4) self.make_file("main.py", """\ from d0.d0 import f0 """) - cov = coverage.Coverage(source=".") + cov = coverage.Coverage(source=["."]) self.start_import_stop(cov, "main") cov.set_option("xml:package_depth", 1) @@ -375,12 +417,12 @@ ('class', {'filename': "d0/f0.py", 'name': "f0.py"}), ]) - def test_package_depth_2(self): + def test_package_depth_2(self) -> None: self.make_tree(width=1, depth=4) self.make_file("main.py", """\ from d0.d0 import f0 """) - cov = coverage.Coverage(source=".") + cov = coverage.Coverage(source=["."]) self.start_import_stop(cov, "main") cov.set_option("xml:package_depth", 2) @@ -397,12 +439,12 @@ ('class', {'filename': "d0/d0/f0.py", 'name': "f0.py"}), ]) - def test_package_depth_3(self): + def test_package_depth_3(self) -> None: self.make_tree(width=1, depth=4) self.make_file("main.py", """\ from d0.d0 import f0 """) - cov = coverage.Coverage(source=".") + cov = coverage.Coverage(source=["."]) self.start_import_stop(cov, "main") cov.set_option("xml:package_depth", 3) @@ -420,7 +462,7 @@ ('class', {'filename': "d0/d0/d0/f0.py", 'name': "f0.py"}), ]) - def test_source_prefix(self): + def test_source_prefix(self) -> None: # https://github.com/nedbat/coveragepy/issues/465 # https://github.com/nedbat/coveragepy/issues/526 self.make_file("src/mod.py", "print(17)") @@ -434,21 +476,22 @@ dom = ElementTree.parse("coverage.xml") self.assert_source(dom, "src") - def test_relative_source(self): + @pytest.mark.parametrize("trail", ["", "/", "\\"]) + def test_relative_source(self, trail: str) -> None: + if trail == "\\" and not env.WINDOWS: + pytest.skip("trailing backslash is only for Windows") self.make_file("src/mod.py", "print(17)") - cov = coverage.Coverage(source=["src"]) + cov = coverage.Coverage(source=[f"src{trail}"]) cov.set_option("run:relative_files", True) self.start_import_stop(cov, "mod", modfile="src/mod.py") cov.xml_report() - with open("coverage.xml") as x: - print(x.read()) dom = ElementTree.parse("coverage.xml") elts = dom.findall(".//sources/source") assert [elt.text for elt in elts] == ["src"] -def compare_xml(expected, actual, **kwargs): +def compare_xml(expected: str, actual: str, actual_extra: bool = False) -> None: """Specialized compare function for our XML files.""" source_path = coverage.files.relative_directory().rstrip(r"\/") @@ -456,15 +499,15 @@ (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), (r' version="[-.\w]+"', ' version="VERSION"'), (r'<source>\s*.*?\s*</source>', '<source>%s</source>' % re.escape(source_path)), - (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'), + (r'/coverage\.readthedocs\.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'), ] - compare(expected, actual, scrubs=scrubs, **kwargs) + compare(expected, actual, scrubs=scrubs, actual_extra=actual_extra) class XmlGoldTest(CoverageTest): """Tests of XML reporting that use gold files.""" - def test_a_xml_1(self): + def test_a_xml_1(self) -> None: self.make_file("a.py", """\ if 1 < 2: # Needed a < to look at HTML entities. @@ -478,7 +521,7 @@ cov.xml_report(a, outfile="coverage.xml") compare_xml(gold_path("xml/x_xml"), ".", actual_extra=True) - def test_a_xml_2(self): + def test_a_xml_2(self) -> None: self.make_file("a.py", """\ if 1 < 2: # Needed a < to look at HTML entities. @@ -498,7 +541,7 @@ cov.xml_report(a) compare_xml(gold_path("xml/x_xml"), "xml_2") - def test_y_xml_branch(self): + def test_y_xml_branch(self) -> None: self.make_file("y.py", """\ def choice(x): if x < 2: diff -Nru python-coverage-6.5.0+dfsg1/tox.ini python-coverage-7.2.7+dfsg1/tox.ini --- python-coverage-6.5.0+dfsg1/tox.ini 2022-09-29 16:36:36.000000000 +0000 +++ python-coverage-7.2.7+dfsg1/tox.ini 2023-05-29 19:46:30.000000000 +0000 @@ -2,21 +2,23 @@ # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt [tox] -# When changing this list, be sure to check the [gh-actions] list below. +# When changing this list, be sure to check the [gh] list below. # PYVERSIONS -envlist = py{37,38,39,310,311}, pypy3, doc, lint +envlist = py{37,38,39,310,311,312}, pypy3, doc, lint, mypy skip_missing_interpreters = {env:COVERAGE_SKIP_MISSING_INTERPRETERS:True} toxworkdir = {env:TOXWORKDIR:.tox} [testenv] usedevelop = True +download = True extras = toml +# PYVERSIONS deps = -r requirements/pip.pip -r requirements/pytest.pip - py{37,38,39,310}: -r requirements/light-threads.pip + py{37,38,39,310,311}: -r requirements/light-threads.pip # Windows can't update the pip version with pip running, so use Python # to install things. @@ -25,19 +27,20 @@ passenv = * setenv = pypy{3,37,38,39}: COVERAGE_NO_CTRACER=no C extension under PyPy - jython: COVERAGE_NO_CTRACER=no C extension under Jython - jython: PYTEST_ADDOPTS=-n 0 # For some tests, we need .pyc files written in the current directory, # so override any local setting. PYTHONPYCACHEPREFIX= + PYTHONWARNINGS=ignore:removed in Python 3.14; use ast.Constant:DeprecationWarning +# $set_env.py: COVERAGE_PIP_ARGS - Extra arguments for `pip install` +# `--no-build-isolation` will let tox work with no network. commands = # Create tests/zipmods.zip python igor.py zip_mods # Build the C extension and test with the CTracer python setup.py --quiet build_ext --inplace - python -m pip install -q -e . + python -m pip install {env:COVERAGE_PIP_ARGS} -q -e . python igor.py test_with_tracer c {posargs} # Remove the C extension so that we can test the PyTracer @@ -57,7 +60,7 @@ # return. deps = -r doc/requirements.pip -whitelist_externals = +allowlist_externals = make commands = # If this command fails, see the comment at the top of doc/cmd.rst @@ -69,32 +72,51 @@ - sphinx-build -b html -b linkcheck -aEnQW doc doc/_build/html [testenv:lint] +# Minimum of PYVERSIONS +basepython = python3.7 deps = -r requirements/lint.pip setenv = - LINTABLE = coverage tests doc ci igor.py setup.py __main__.py + {[testenv]setenv} + LINTABLE=coverage tests doc ci igor.py setup.py __main__.py commands = python -m tabnanny {env:LINTABLE} - python igor.py check_eol # If this command fails, see the comment at the top of doc/cmd.rst python -m cogapp -cP --check --verbosity=1 doc/*.rst python -m cogapp -cP --check --verbosity=1 .github/workflows/*.yml #doc8 -q --ignore-path 'doc/_*' doc CHANGES.rst README.rst + python -m pylint --notes= {env:LINTABLE} + check-manifest --ignore 'doc/sample_html/*,.treerc' # If 'build -q' becomes a thing (https://github.com/pypa/build/issues/188), - # this can be simplifed: + # this can be simplified: python igor.py quietly "python -m build" twine check dist/* - python -m pylint --notes= {env:LINTABLE} - check-manifest --ignore 'doc/sample_html/*,.treerc' -[gh-actions] +[testenv:mypy] +basepython = python3.8 + +deps = + -r requirements/mypy.pip + +setenv = + {[testenv]setenv} + TYPEABLE=coverage tests + +commands = + # PYVERSIONS + mypy --python-version=3.8 {env:TYPEABLE} + mypy --python-version=3.12 {env:TYPEABLE} + +[gh] +# https://pypi.org/project/tox-gh/ # PYVERSIONS python = - 3.7: py37 - 3.8: py38 - 3.9: py39 - 3.10: py310 - 3.11: py311 - pypy-3: pypy3 + 3.7 = py37 + 3.8 = py38 + 3.9 = py39 + 3.10 = py310 + 3.11 = py311 + 3.12 = py312 + pypy-3 = pypy3