summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.hgignore1
-rw-r--r--.hgtags2
-rw-r--r--.travis.yml6
-rw-r--r--CHANGES.rst31
-rw-r--r--CONTRIBUTORS.txt4
-rw-r--r--MANIFEST.in2
-rw-r--r--Makefile5
-rw-r--r--README.rst24
-rw-r--r--__main__.py11
-rw-r--r--appveyor.yml7
-rw-r--r--circle.yml18
-rw-r--r--coverage/backunittest.py7
-rw-r--r--coverage/cmdline.py13
-rw-r--r--coverage/collector.py17
-rw-r--r--coverage/config.py77
-rw-r--r--coverage/control.py572
-rw-r--r--coverage/debug.py5
-rw-r--r--coverage/disposition.py37
-rw-r--r--coverage/execfile.py17
-rw-r--r--coverage/htmlfiles/coverage_html.js11
-rw-r--r--coverage/htmlfiles/style.css1
-rw-r--r--coverage/inorout.py461
-rw-r--r--coverage/multiproc.py17
-rw-r--r--coverage/plugin.py3
-rw-r--r--coverage/python.py28
-rw-r--r--coverage/version.py2
-rw-r--r--doc/cmd.rst7
-rw-r--r--doc/conf.py4
-rw-r--r--doc/config.rst5
-rw-r--r--doc/contributing.rst2
-rw-r--r--doc/faq.rst6
-rw-r--r--doc/index.rst12
-rw-r--r--doc/install.rst4
-rw-r--r--doc/plugins.rst2
-rw-r--r--doc/python-coverage.1.txt6
-rw-r--r--doc/requirements.pip6
-rw-r--r--doc/trouble.rst2
-rw-r--r--howto.txt8
-rw-r--r--igor.py5
-rw-r--r--metacov.ini4
-rw-r--r--perf/perf_measure.py2
-rw-r--r--requirements/dev.pip8
-rw-r--r--requirements/pytest.pip5
-rw-r--r--requirements/tox.pip4
-rw-r--r--requirements/wheel.pip5
-rw-r--r--setup.py4
-rw-r--r--tests/modules/process_test/try_execfile.py11
-rw-r--r--tests/test_api.py3
-rw-r--r--tests/test_arcs.py4
-rw-r--r--tests/test_cmdline.py2
-rw-r--r--tests/test_concurrency.py28
-rw-r--r--tests/test_config.py15
-rw-r--r--tests/test_coverage.py13
-rw-r--r--tests/test_debug.py4
-rw-r--r--tests/test_farm.py4
-rw-r--r--tests/test_oddball.py11
-rw-r--r--tests/test_parser.py20
-rw-r--r--tests/test_process.py50
-rw-r--r--tox-new.ini53
-rw-r--r--tox.ini29
-rw-r--r--tox_wheels.ini4
62 files changed, 942 insertions, 790 deletions
diff --git a/.gitignore b/.gitignore
index 74520d51..b952771a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,7 @@ setuptools-*.egg
.tox*
.noseids
.cache
+.pytest_cache
.hypothesis
# Stuff in the test directory.
diff --git a/.hgignore b/.hgignore
index ce114b13..53576e14 100644
--- a/.hgignore
+++ b/.hgignore
@@ -28,6 +28,7 @@ setuptools-*.egg
.tox*
.noseids
.cache
+.pytest_cache
.hypothesis
# Stuff in the test directory.
diff --git a/.hgtags b/.hgtags
index 8f9ad85c..48574a0f 100644
--- a/.hgtags
+++ b/.hgtags
@@ -65,3 +65,5 @@ dd2d866194d2eca05862230e6003c6e04fc2fdc0 coverage-4.3.2
ed196840b79136f17ab493699ec83dcf7dbfe973 coverage-4.4.1
b65ae46a6504b8d577e967bd3fdcfcaceec95528 coverage-4.4.2
102b2250a123537e640cd014f5df281822e79cec coverage-4.5
+dda8b38e71d0bd2bde79d644f7265e1c02ce02f9 coverage-4.5.1
+865c64d99227b40e9f92586f63f2b61ebbe12d48 coverage-5.0a1
diff --git a/.travis.yml b/.travis.yml
index 5e28d48b..df238e57 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,23 +3,21 @@
language: python
+cache: pip
sudo: false
python:
- - '2.6'
- '2.7'
- - '3.3'
- '3.4'
- '3.5'
- '3.6'
- 'pypy'
+ - 'pypy3.5'
env:
matrix:
- COVERAGE_COVERAGE=no
- COVERAGE_COVERAGE=yes
-sudo: false
-
install:
- pip install -r requirements/ci.pip
diff --git a/CHANGES.rst b/CHANGES.rst
index e4243b9b..5c8b4eec 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -11,10 +11,35 @@ Change history for Coverage.py
..
.. .. _changes_781:
..
- ..
.. Version 7.8.1 --- 2021-07-27
.. ----------------------------
+Unreleased
+----------
+
+
+.. _changes_50a1:
+
+Version 5.0a1 --- 2018-06-05
+----------------------------
+
+- Coverage.py no longer supports Python 2.6 or 3.3.
+
+- The location of the configuration file can now be specified with a
+ ``COVERAGE_RCFILE`` environment variable, as requested in `issue 650`_.
+
+- A new warning (already-imported) is issued if measurable files have already
+ been imported before coverage.py started measurement. See
+ :ref:`cmd_warnings` for more information.
+
+- Running coverage many times for small runs in a single process should be
+ faster, closing `issue 625`_. Thanks, David MacIver.
+
+- Large HTML report pages load faster. Thanks, Pankaj Pandey.
+
+.. _issue 625: https://bitbucket.org/ned/coveragepy/issues/625/lstat-dominates-in-the-case-of-small
+.. _issue 650: https://bitbucket.org/ned/coveragepy/issues/650/allow-setting-configuration-file-location
+
.. _changes_451:
@@ -485,7 +510,7 @@ Work from the PyCon 2016 Sprints!
.. _issue 478: https://bitbucket.org/ned/coveragepy/issues/478/help-shows-silly-program-name-when-running
.. _issue 484: https://bitbucket.org/ned/coveragepy/issues/484/multiprocessing-greenlet-concurrency
.. _issue 492: https://bitbucket.org/ned/coveragepy/issues/492/subprocess-coverage-strange-detection-of
-.. _unittest-mixins: https://pypi.python.org/pypi/unittest-mixins
+.. _unittest-mixins: https://pypi.org/project/unittest-mixins/
.. _changes_41:
@@ -1804,7 +1829,7 @@ Version 3.2b4 --- 2009-12-01
- On Python 3.x, setuptools has been replaced by `Distribute`_.
-.. _Distribute: https://pypi.python.org/pypi/distribute
+.. _Distribute: https://pypi.org/project/distribute/
Version 3.2b3 --- 2009-11-23
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 52cedc0f..549a83dc 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -52,6 +52,7 @@ George Paci
George Song
Greg Rogers
Guillaume Chazarain
+Hugo van Kemenade
Ilia Meerovich
Imri Goldberg
Ionel Cristian Mărieș
@@ -80,6 +81,8 @@ Mickie Betz
Nathan Land
Noel O'Boyle
Olivier Grisel
+Ori Avtalion
+Pankaj Pandey
Pablo Carballo
Patrick Mezard
Peter Baughman
@@ -98,6 +101,7 @@ Stephen Finucane
Steve Leonard
Steve Peak
Ted Wexler
+Thijs Triemstra
Titus Brown
Ville Skyttä
Yury Selivanov
diff --git a/MANIFEST.in b/MANIFEST.in
index 462f24ff..275f7526 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -14,7 +14,6 @@ include TODO.txt
include __main__.py
include .travis.yml
include appveyor.yml
-include circle.yml
include howto.txt
include igor.py
include metacov.ini
@@ -22,6 +21,7 @@ include pylintrc
include setup.py
include tox.ini
include tox_wheels.ini
+include .editorconfig
recursive-include ci *.*
exclude ci/appveyor.token
diff --git a/Makefile b/Makefile
index 6174d777..d9bc1775 100644
--- a/Makefile
+++ b/Makefile
@@ -26,6 +26,7 @@ clean:
-rm -f setuptools-*.egg distribute-*.egg distribute-*.tar.gz
-rm -rf doc/_build doc/_spell doc/sample_html_beta
-rm -rf .tox_kits
+ -rm -rf .cache .pytest_cache
sterile: clean
-rm -rf .tox*
@@ -50,10 +51,10 @@ test:
TOX_SMOKE_ARGS = -n 6 -m "not expensive" --maxfail=3 $(ARGS)
smoke:
- COVERAGE_NO_PYTRACER=1 tox -e py26,py33 -- $(TOX_SMOKE_ARGS)
+ COVERAGE_NO_PYTRACER=1 tox -e py27,py34 -- $(TOX_SMOKE_ARGS)
pysmoke:
- COVERAGE_NO_CTRACER=1 tox -e py26,py33 -- $(TOX_SMOKE_ARGS)
+ COVERAGE_NO_CTRACER=1 tox -e py27,py34 -- $(TOX_SMOKE_ARGS)
metacov:
COVERAGE_COVERAGE=yes tox $(ARGS)
diff --git a/README.rst b/README.rst
index 4dac3ea8..00715592 100644
--- a/README.rst
+++ b/README.rst
@@ -19,8 +19,8 @@ library to determine which lines are executable, and which have been executed.
Coverage.py runs on many versions of Python:
-* CPython 2.6, 2.7 and 3.3 through 3.7.
-* PyPy2 5.10 and PyPy3 5.10.
+* CPython 2.7 and 3.4 through 3.7.
+* PyPy2 6.0 and PyPy3 6.0.
* Jython 2.7.1, though not for reporting.
* IronPython 2.7.7, though not for reporting.
@@ -32,7 +32,9 @@ Documentation is on `Read the Docs`_. Code repository and issue tracker are on
.. _GitHub: https://github.com/nedbat/coveragepy
-**New in 4.5:** Configurator plug-ins.
+**New in 5.0:** Dropped support for Python 2.6 and 3.3.
+
+New in 4.5: Configurator plug-ins.
New in 4.4: Suppressable warnings, continuous coverage measurement.
@@ -85,25 +87,25 @@ Licensed under the `Apache 2.0 License`_. For details, see `NOTICE.txt`_.
:target: https://requires.io/github/nedbat/coveragepy/requirements/?branch=master
:alt: Requirements status
.. |kit| image:: https://badge.fury.io/py/coverage.svg
- :target: https://pypi.python.org/pypi/coverage
+ :target: https://pypi.org/project/coverage/
:alt: PyPI status
.. |format| image:: https://img.shields.io/pypi/format/coverage.svg
- :target: https://pypi.python.org/pypi/coverage
+ :target: https://pypi.org/project/coverage/
:alt: Kit format
.. |downloads| image:: https://img.shields.io/pypi/dw/coverage.svg
- :target: https://pypi.python.org/pypi/coverage
+ :target: https://pypi.org/project/coverage/
:alt: Weekly PyPI downloads
.. |versions| image:: https://img.shields.io/pypi/pyversions/coverage.svg
- :target: https://pypi.python.org/pypi/coverage
+ :target: https://pypi.org/project/coverage/
:alt: Python versions supported
.. |status| image:: https://img.shields.io/pypi/status/coverage.svg
- :target: https://pypi.python.org/pypi/coverage
+ :target: https://pypi.org/project/coverage/
:alt: Package stability
.. |license| image:: https://img.shields.io/pypi/l/coverage.svg
- :target: https://pypi.python.org/pypi/coverage
+ :target: https://pypi.org/project/coverage/
:alt: License
-.. |codecov| image:: http://codecov.io/github/nedbat/coveragepy/coverage.svg?branch=master&precision=2
- :target: http://codecov.io/github/nedbat/coveragepy?branch=master
+.. |codecov| image:: https://codecov.io/github/nedbat/coveragepy/coverage.svg?branch=master&precision=2
+ :target: https://codecov.io/github/nedbat/coveragepy?branch=master
:alt: Coverage!
.. |saythanks| image:: https://img.shields.io/badge/saythanks.io-%E2%98%BC-1EAEDB.svg
:target: https://saythanks.io/to/nedbat
diff --git a/__main__.py b/__main__.py
index c998e1da..f1f2b4f6 100644
--- a/__main__.py
+++ b/__main__.py
@@ -8,12 +8,5 @@ import os
PKG = 'coverage'
-try:
- run_globals = runpy.run_module(PKG, run_name='__main__', alter_sys=True)
- executed = os.path.splitext(os.path.basename(run_globals['__file__']))[0]
- if executed != '__main__': # For Python 2.5 compatibility
- raise ImportError(
- 'Incorrectly executed %s instead of __main__' % executed
- )
-except ImportError: # For Python 2.6 compatibility
- runpy.run_module('%s.__main__' % PKG, run_name='__main__', alter_sys=True)
+run_globals = runpy.run_module(PKG, run_name='__main__', alter_sys=True)
+executed = os.path.splitext(os.path.basename(run_globals['__file__']))[0]
diff --git a/appveyor.yml b/appveyor.yml
index f6b40660..fe99f630 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -5,6 +5,9 @@ version: '{branch}-{build}'
shallow_clone: true
+cache:
+ - '%LOCALAPPDATA%\pip\Cache'
+
environment:
CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\ci\\run_with_env.cmd"
@@ -96,9 +99,9 @@ install:
# Upgrade to the latest version of pip to avoid it displaying warnings
# about it being out of date.
- - "pip install --disable-pip-version-check --user --upgrade pip"
+ - "python -m pip install --disable-pip-version-check --user --upgrade pip"
# And upgrade virtualenv to get the latest pip inside .tox virtualenvs.
- - "pip install --disable-pip-version-check --user --upgrade virtualenv"
+ - "python -m pip install --disable-pip-version-check --user --upgrade virtualenv"
# Install requirements.
- "%CMD_IN_ENV% pip install -r requirements/ci.pip"
diff --git a/circle.yml b/circle.yml
deleted file mode 100644
index a52959ef..00000000
--- a/circle.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-# Circle CI configuration for coverage.py.
-# https://circleci.com/gh/nedbat/coveragepy
-
-machine:
- python:
- version: 2.7.6
- post:
- - pyenv global pypy-2.4.0 2.6.8 2.7.9 3.3.3 3.4.2
-
-dependencies:
- pre:
- - pip install -U pip
- override:
- - pip install -r requirements/tox.pip
-
-test:
- override:
- - tox
diff --git a/coverage/backunittest.py b/coverage/backunittest.py
index 09574ccb..1b084835 100644
--- a/coverage/backunittest.py
+++ b/coverage/backunittest.py
@@ -3,12 +3,7 @@
"""Implementations of unittest features from the future."""
-# Use unittest2 if it's available, otherwise unittest. This gives us
-# back-ported features for 2.6.
-try:
- import unittest2 as unittest
-except ImportError:
- import unittest
+import unittest
def unittest_has(method):
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 7b86054e..ea86b445 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -44,7 +44,7 @@ class Opts(object):
)
debug = optparse.make_option(
'', '--debug', action='store', metavar="OPTS",
- help="Debug options, separated by commas",
+ help="Debug options, separated by commas. [env: COVERAGE_DEBUG]",
)
directory = optparse.make_option(
'-d', '--directory', action='store', metavar="DIR",
@@ -115,7 +115,11 @@ class Opts(object):
)
rcfile = optparse.make_option(
'', '--rcfile', action='store',
- help="Specify configuration file. Defaults to '.coveragerc'",
+ help=(
+ "Specify configuration file. "
+ "By default '.coveragerc', 'setup.cfg' and 'tox.ini' are tried. "
+ "[env: COVERAGE_RCFILE]"
+ ),
)
source = optparse.make_option(
'', '--source', action='store', metavar="SRC1,SRC2,...",
@@ -124,7 +128,7 @@ class Opts(object):
timid = optparse.make_option(
'', '--timid', action='store_true',
help=(
- "Use a simpler but slower trace method. Try this if you get "
+ "Use a simpler but slower trace method. Try this if you get "
"seemingly impossible results!"
),
)
@@ -475,6 +479,7 @@ class CoverageScript(object):
include=include,
debug=debug,
concurrency=options.concurrency,
+ check_preimported=True,
)
if options.action == "debug":
@@ -656,7 +661,7 @@ class CoverageScript(object):
self.coverage.load()
data = self.coverage.data
print(info_header("data"))
- print("path: %s" % self.coverage.data_files.filename)
+ print("path: %s" % self.coverage._data_files.filename)
if data:
print("has_arcs: %r" % data.has_arcs())
summary = data.line_counts(fullpath=True)
diff --git a/coverage/collector.py b/coverage/collector.py
index 72ab32b6..bc385fc2 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -9,6 +9,7 @@ import sys
from coverage import env
from coverage.backward import litems, range # pylint: disable=redefined-builtin
from coverage.debug import short_stack
+from coverage.disposition import FileDisposition
from coverage.files import abs_file
from coverage.misc import CoverageException, isolate_module
from coverage.pytracer import PyTracer
@@ -33,11 +34,6 @@ except ImportError:
CTracer = None
-class FileDisposition(object):
- """A simple value type for recording what to do with a file."""
- pass
-
-
def should_start_context(frame):
"""Who-Tests-What hack: Determine whether this frame begins a new who-context."""
fn_name = frame.f_code.co_name
@@ -107,6 +103,7 @@ class Collector(object):
self.origin = short_stack()
self.concur_id_func = None
+ self.abs_file_cache = {}
# We can handle a few concurrency options here, but only one at a time.
these_concurrencies = self.SUPPORTED_CONCURRENCIES.intersection(concurrency)
@@ -373,6 +370,14 @@ class Collector(object):
for tracer in self.tracers:
tracer.data = data
+ def cached_abs_file(self, filename):
+ """A locally cached version of `abs_file`."""
+ key = (type(filename), filename)
+ try:
+ return self.abs_file_cache[key]
+ except KeyError:
+ return self.abs_file_cache.setdefault(key, abs_file(filename))
+
def save_data(self, covdata):
"""Save the collected data to a `CoverageData`.
@@ -398,7 +403,7 @@ class Collector(object):
else:
raise runtime_err # pylint: disable=raising-bad-type
- return dict((abs_file(k), v) for k, v in items)
+ return dict((self.cached_abs_file(k), v) for k, v in items)
if self.branch:
covdata.add_arcs(abs_file_dict(self.data))
diff --git a/coverage/config.py b/coverage/config.py
index 7b8f2bd0..effa382f 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -6,8 +6,8 @@
import collections
import os
import re
-import sys
+from coverage import env
from coverage.backward import configparser, iitems, string_class
from coverage.misc import contract, CoverageException, isolate_module
@@ -33,7 +33,7 @@ class HandyConfigParser(configparser.RawConfigParser):
def read(self, filenames):
"""Read a file name as UTF-8 configuration data."""
kwargs = {}
- if sys.version_info >= (3, 2):
+ if env.PYVERSION >= (3, 2):
kwargs['encoding'] = "utf-8"
return configparser.RawConfigParser.read(self, filenames, **kwargs)
@@ -175,8 +175,12 @@ class CoverageConfig(object):
def __init__(self):
"""Initialize the configuration attributes to their defaults."""
# Metadata about the config.
+ # We tried to read these config files.
self.attempted_config_files = []
- self.config_files = []
+ # We did read these config files, but maybe didn't find any content for us.
+ self.config_files_read = []
+ # The file that gave us our configuration.
+ self.config_file = None
# Defaults for [run] and [report]
self._include = None
@@ -262,7 +266,7 @@ class CoverageConfig(object):
if not files_read:
return False
- self.config_files.extend(files_read)
+ self.config_files_read.extend(files_read)
any_set = False
try:
@@ -305,9 +309,14 @@ class CoverageConfig(object):
# then it was used. If we're piggybacking on someone else's file,
# then it was only used if we found some settings in it.
if our_file:
- return True
+ used = True
else:
- return any_set
+ used = any_set
+
+ if used:
+ self.config_file = filename
+
+ return used
CONFIG_FILE_OPTIONS = [
# These are *args for _set_attr_from_config_option:
@@ -425,6 +434,34 @@ class CoverageConfig(object):
raise CoverageException("No such option: %r" % option_name)
+def config_files_to_try(config_file):
+ """What config files should we try to read?
+
+ Returns a list of tuples:
+ (filename, is_our_file, was_file_specified)
+ """
+
+ # Some API users were specifying ".coveragerc" to mean the same as
+ # True, so make it so.
+ if config_file == ".coveragerc":
+ config_file = True
+ 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:
+ specified_file = True
+ if not specified_file:
+ # Still no file specified. Default to .coveragerc
+ config_file = ".coveragerc"
+ files_to_try = [
+ (config_file, True, specified_file),
+ ("setup.cfg", False, False),
+ ("tox.ini", False, False),
+ ]
+ return files_to_try
+
+
def read_coverage_config(config_file, **kwargs):
"""Read the coverage.py configuration.
@@ -435,10 +472,7 @@ def read_coverage_config(config_file, **kwargs):
setting values in the configuration.
Returns:
- config_file, config:
- config_file is the value to use for config_file in other
- invocations of coverage.
-
+ config:
config is a CoverageConfig object read from the appropriate
configuration file.
@@ -449,25 +483,14 @@ def read_coverage_config(config_file, **kwargs):
# 2) from a file:
if config_file:
- # Some API users were specifying ".coveragerc" to mean the same as
- # True, so make it so.
- if config_file == ".coveragerc":
- config_file = True
- specified_file = (config_file is not True)
- if not specified_file:
- config_file = ".coveragerc"
-
- for fname, our_file in [(config_file, True),
- ("setup.cfg", False),
- ("tox.ini", False)]:
- config_read = config.from_file(fname, our_file=our_file)
- is_config_file = fname == config_file
-
- if not config_read and is_config_file and specified_file:
- raise CoverageException("Couldn't read '%s' as a config file" % fname)
+ files_to_try = config_files_to_try(config_file)
+ for fname, our_file, specified_file in files_to_try:
+ config_read = config.from_file(fname, our_file=our_file)
if config_read:
break
+ if specified_file:
+ raise CoverageException("Couldn't read '%s' as a config file" % fname)
# 3) from environment variables:
env_data_file = os.environ.get('COVERAGE_FILE')
@@ -486,4 +509,4 @@ def read_coverage_config(config_file, **kwargs):
config.html_dir = os.path.expanduser(config.html_dir)
config.xml_output = os.path.expanduser(config.xml_output)
- return config_file, config
+ return config
diff --git a/coverage/control.py b/coverage/control.py
index b82c8047..80012f57 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -5,14 +5,10 @@
import atexit
-import inspect
-import itertools
import os
import platform
-import re
import sys
import time
-import traceback
from coverage import env
from coverage.annotate import AnnotateReporter
@@ -21,16 +17,15 @@ from coverage.collector import Collector
from coverage.config import read_coverage_config
from coverage.data import CoverageData, CoverageDataFiles
from coverage.debug import DebugControl, write_formatted_info
-from coverage.files import TreeMatcher, FnmatchMatcher
-from coverage.files import PathAliases, find_python_files, prep_patterns
-from coverage.files import canonical_filename, set_relative_directory
-from coverage.files import ModuleMatcher, abs_file
+from coverage.disposition import disposition_debug_msg
+from coverage.files import PathAliases, set_relative_directory, abs_file
from coverage.html import HtmlReporter
+from coverage.inorout import InOrOut
from coverage.misc import CoverageException, bool_or_none, join_regex
from coverage.misc import file_be_gone, isolate_module
from coverage.plugin import FileReporter
from coverage.plugin_support import Plugins
-from coverage.python import PythonFileReporter, source_for_file
+from coverage.python import PythonFileReporter
from coverage.results import Analysis, Numbers
from coverage.summary import SummaryReporter
from coverage.xmlreport import XmlReporter
@@ -43,22 +38,6 @@ except ImportError: # pragma: only jytho
os = isolate_module(os)
-# 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
-if env.PYPY:
- try:
- import _structseq
- except ImportError:
- pass
-
- try:
- import _pypy_irc_topic
- except ImportError:
- pass
-
class Coverage(object):
"""Programmatic access to coverage.py.
@@ -74,11 +53,12 @@ class Coverage(object):
cov.html_report(directory='covhtml')
"""
+
def __init__(
self, data_file=None, data_suffix=None, cover_pylib=None,
auto_data=False, timid=None, branch=None, config_file=True,
source=None, omit=None, include=None, debug=None,
- concurrency=None,
+ concurrency=None, check_preimported=False,
):
"""
`data_file` is the base name of the data file to use, defaulting to
@@ -132,15 +112,23 @@ class Coverage(object):
"eventlet", "gevent", "multiprocessing", or "thread" (the default).
This can also be a list of these strings.
+ If `check_preimported` is true, then when coverage is started, the
+ aleady-imported files will be checked to see if they should be measured
+ by coverage. Importing measured files before coverage is started can
+ mean that code is missed.
+
.. versionadded:: 4.0
The `concurrency` parameter.
.. versionadded:: 4.2
The `concurrency` parameter can now be a list of strings.
+ .. versionadded:: 4.6
+ The `check_preimported` parameter.
+
"""
# Build our configuration from a number of sources.
- self.config_file, self.config = read_coverage_config(
+ self.config = read_coverage_config(
config_file=config_file,
data_file=data_file, cover_pylib=cover_pylib, timid=timid,
branch=branch, parallel=bool_or_none(data_suffix),
@@ -153,31 +141,24 @@ class Coverage(object):
self._debug_file = None
self._auto_load = self._auto_save = auto_data
- self._data_suffix = data_suffix
-
- # The matchers for _should_trace.
- self.source_match = None
- self.source_pkgs_match = None
- self.pylib_match = self.cover_match = None
- self.include_match = self.omit_match = None
+ self._data_suffix_specified = data_suffix
# Is it ok for no data to be collected?
self._warn_no_data = True
self._warn_unimported_source = True
+ self._warn_preimported_source = check_preimported
# A record of all the warnings that have been issued.
self._warnings = []
# Other instance attributes, set later.
- self.omit = self.include = self.source = None
- self.source_pkgs_unmatched = None
- self.source_pkgs = None
- self.data = self.data_files = self.collector = None
- self.plugins = None
- self.pylib_paths = self.cover_paths = None
- self.data_suffix = self.run_suffix = None
+ self.data = self._data_files = self._collector = None
+ self._plugins = None
+ self._inorout = None
+ self._inorout_class = InOrOut
+ self._data_suffix = self._run_suffix = None
self._exclude_re = None
- self.debug = None
+ self._debug = None
# State machine variables:
# Have we initialized everything?
@@ -215,7 +196,7 @@ class Coverage(object):
self._debug_file = open(debug_file_name, "a")
else:
self._debug_file = sys.stderr
- self.debug = DebugControl(self.config.debug, self._debug_file)
+ self._debug = DebugControl(self.config.debug, self._debug_file)
# _exclude_re is a dict that maps exclusion list names to compiled regexes.
self._exclude_re = {}
@@ -223,41 +204,28 @@ class Coverage(object):
set_relative_directory()
# Load plugins
- self.plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug)
+ self._plugins = Plugins.load_plugins(self.config.plugins, self.config, self._debug)
# Run configuring plugins.
- for plugin in self.plugins.configurers:
+ for plugin in self._plugins.configurers:
# We need an object with set_option and get_option. Either self or
# self.config will do. Choosing randomly stops people from doing
# other things with those objects, against the public API. Yes,
# this is a bit childish. :)
plugin.configure([self, self.config][int(time.time()) % 2])
- # The source argument can be directories or package names.
- self.source = []
- self.source_pkgs = []
- for src in self.config.source or []:
- if os.path.isdir(src):
- self.source.append(canonical_filename(src))
- else:
- self.source_pkgs.append(src)
- self.source_pkgs_unmatched = self.source_pkgs[:]
-
- self.omit = prep_patterns(self.config.run_omit)
- self.include = prep_patterns(self.config.run_include)
-
concurrency = self.config.concurrency or []
if "multiprocessing" in concurrency:
if not patch_multiprocessing:
raise CoverageException( # pragma: only jython
"multiprocessing is not supported on this Python"
)
- patch_multiprocessing(rcfile=self.config_file)
+ patch_multiprocessing(rcfile=self.config.config_file)
# Multi-processing uses parallel for the subprocesses, so also use
# it for the main process.
self.config.parallel = True
- self.collector = Collector(
+ self._collector = Collector(
should_trace=self._should_trace,
check_include=self._check_include_omit_etc,
timid=self.config.timid,
@@ -267,320 +235,73 @@ class Coverage(object):
)
# Early warning if we aren't going to be able to support plugins.
- if self.plugins.file_tracers and not self.collector.supports_plugins:
+ if self._plugins.file_tracers and not self._collector.supports_plugins:
self._warn(
"Plugin file tracers (%s) aren't supported with %s" % (
", ".join(
plugin._coverage_plugin_name
- for plugin in self.plugins.file_tracers
+ for plugin in self._plugins.file_tracers
),
- self.collector.tracer_name(),
+ self._collector.tracer_name(),
)
)
- for plugin in self.plugins.file_tracers:
+ for plugin in self._plugins.file_tracers:
plugin._coverage_enabled = False
+ # Create the file classifying substructure.
+ self._inorout = self._inorout_class(warn=self._warn)
+ self._inorout.configure(self.config)
+ self._inorout.plugins = self._plugins
+ self._inorout.disp_class = self._collector.file_disposition_class
+
# Suffixes are a bit tricky. We want to use the data suffix only when
# collecting data, not when combining data. So we save it as
- # `self.run_suffix` now, and promote it to `self.data_suffix` if we
+ # `self._run_suffix` now, and promote it to `self._data_suffix` if we
# find that we are collecting data later.
- if self._data_suffix or self.config.parallel:
- if not isinstance(self._data_suffix, string_class):
+ if self._data_suffix_specified or self.config.parallel:
+ if not isinstance(self._data_suffix_specified, string_class):
# if data_suffix=True, use .machinename.pid.random
- self._data_suffix = True
+ self._data_suffix_specified = True
else:
- self._data_suffix = None
- self.data_suffix = None
- self.run_suffix = self._data_suffix
+ self._data_suffix_specified = None
+ self._data_suffix = None
+ self._run_suffix = self._data_suffix_specified
# Create the data file. We do this at construction time so that the
# data file will be written into the directory where the process
# started rather than wherever the process eventually chdir'd to.
- self.data = CoverageData(debug=self.debug)
- self.data_files = CoverageDataFiles(
- basename=self.config.data_file, warn=self._warn, debug=self.debug,
+ self.data = CoverageData(debug=self._debug)
+ self._data_files = CoverageDataFiles(
+ basename=self.config.data_file, warn=self._warn, debug=self._debug,
)
- # The directories for files considered "installed with the interpreter".
- self.pylib_paths = set()
- if not self.config.cover_pylib:
- # 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.
- for m in (atexit, inspect, os, platform, _pypy_irc_topic, re, _structseq, traceback):
- if m is not None and hasattr(m, "__file__"):
- self.pylib_paths.add(self._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.
- structseq_new = _structseq.structseq_new
- try:
- structseq_file = structseq_new.func_code.co_filename
- except AttributeError:
- structseq_file = structseq_new.__code__.co_filename
- self.pylib_paths.add(self._canonical_path(structseq_file))
-
- # To avoid tracing the coverage.py code itself, we skip anything
- # located where we are.
- self.cover_paths = [self._canonical_path(__file__, directory=True)]
- if env.TESTING:
- # Don't include our own test code.
- self.cover_paths.append(os.path.join(self.cover_paths[0], "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.
- import contracts
- import six
- for mod in [contracts, six]:
- self.cover_paths.append(self._canonical_path(mod))
-
# Set the reporting precision.
Numbers.set_precision(self.config.precision)
atexit.register(self._atexit)
- # Create the matchers we need for _should_trace
- if self.source or self.source_pkgs:
- self.source_match = TreeMatcher(self.source)
- self.source_pkgs_match = ModuleMatcher(self.source_pkgs)
- else:
- if self.cover_paths:
- self.cover_match = TreeMatcher(self.cover_paths)
- if self.pylib_paths:
- self.pylib_match = TreeMatcher(self.pylib_paths)
- if self.include:
- self.include_match = FnmatchMatcher(self.include)
- if self.omit:
- self.omit_match = FnmatchMatcher(self.omit)
-
# The user may want to debug things, show info if desired.
self._write_startup_debug()
def _write_startup_debug(self):
"""Write out debug info at startup if needed."""
wrote_any = False
- with self.debug.without_callers():
- if self.debug.should('config'):
+ with self._debug.without_callers():
+ if self._debug.should('config'):
config_info = sorted(self.config.__dict__.items())
- write_formatted_info(self.debug, "config", config_info)
+ write_formatted_info(self._debug, "config", config_info)
wrote_any = True
- if self.debug.should('sys'):
- write_formatted_info(self.debug, "sys", self.sys_info())
- for plugin in self.plugins:
+ if self._debug.should('sys'):
+ write_formatted_info(self._debug, "sys", self.sys_info())
+ for plugin in self._plugins:
header = "sys: " + plugin._coverage_plugin_name
info = plugin.sys_info()
- write_formatted_info(self.debug, header, info)
+ write_formatted_info(self._debug, header, info)
wrote_any = True
if wrote_any:
- write_formatted_info(self.debug, "end", ())
-
- def _canonical_path(self, morf, directory=False):
- """Return the canonical path of the module or file `morf`.
-
- If the module is a package, then return its directory. If it is a
- module, then return its file, unless `directory` is True, in which
- case return its enclosing directory.
-
- """
- morf_path = PythonFileReporter(morf, self).filename
- if morf_path.endswith("__init__.py") or directory:
- morf_path = os.path.split(morf_path)[0]
- return morf_path
-
- def _name_for_module(self, module_globals, filename):
- """Get the name of the module for a set of globals and file name.
-
- For configurability's sake, we allow __main__ modules to be matched by
- their importable name.
-
- If loaded via runpy (aka -m), we can usually recover the "original"
- full dotted module name, otherwise, we resort to interpreting the
- file name to get the module's name. In the case that the module name
- can't be determined, None is returned.
-
- """
- if module_globals is None: # pragma: only ironpython
- # IronPython doesn't provide globals: https://github.com/IronLanguages/main/issues/1296
- module_globals = {}
-
- dunder_name = module_globals.get('__name__', None)
-
- 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
- if hasattr(loader, attrname):
- fullname = getattr(loader, attrname)
- else:
- continue
-
- if isinstance(fullname, str) and fullname != '__main__':
- # Module loaded via: runpy -m
- return fullname
-
- # Script as first argument to Python command line.
- inspectedname = inspect.getmodulename(filename)
- if inspectedname is not None:
- return inspectedname
- else:
- return dunder_name
-
- def _should_trace_internal(self, filename, frame):
- """Decide whether to trace execution in `filename`, with a reason.
-
- This function is called from the trace function. As each new file name
- is encountered, this function determines whether it is traced or not.
-
- Returns a FileDisposition object.
-
- """
- original_filename = filename
- disp = _disposition_init(self.collector.file_disposition_class, filename)
-
- def nope(disp, reason):
- """Simple helper to make it easy to return NO."""
- disp.trace = False
- disp.reason = reason
- return disp
-
- # Compiled Python files have two file names: frame.f_code.co_filename is
- # the file name at the time the .pyc was compiled. The second name is
- # __file__, which is where the .pyc was actually loaded from. Since
- # .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__')
- if dunder_file:
- filename = source_for_file(dunder_file)
- 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
- # exec'ing code. If it seems like something is wrong with
- # the frame's file name, then just use the original.
- filename = original_filename
-
- if not filename:
- # Empty string is pretty useless.
- return nope(disp, "empty string isn't a file name")
-
- if filename.startswith('memory:'):
- return nope(disp, "memory isn't traceable")
-
- if filename.startswith('<'):
- # Lots of non-file execution is represented with artificial
- # file names like "<string>", "<doctest readme.txt[0]>", or
- # "<exec_function>". 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")
-
- # pyexpat does a dumb thing, calling the trace function explicitly from
- # C code with a C file name.
- if re.search(r"[/\\]Modules[/\\]pyexpat.c", filename):
- return nope(disp, "pyexpat lies about itself")
-
- # Jython reports the .class file to the tracer, use the source file.
- if filename.endswith("$py.class"):
- filename = filename[:-9] + ".py"
-
- canonical = canonical_filename(filename)
- disp.canonical_filename = canonical
-
- # Try the plugins, see if they have an opinion about the file.
- plugin = None
- for plugin in self.plugins.file_tracers:
- if not plugin._coverage_enabled:
- continue
-
- try:
- file_tracer = plugin.file_tracer(canonical)
- if file_tracer is not None:
- file_tracer._coverage_plugin = plugin
- disp.trace = True
- disp.file_tracer = file_tracer
- if file_tracer.has_dynamic_source_filename():
- disp.has_dynamic_filename = True
- else:
- disp.source_filename = canonical_filename(
- file_tracer.source_filename()
- )
- break
- except Exception:
- self._warn(
- "Disabling plug-in %r due to an exception:" % (
- plugin._coverage_plugin_name
- )
- )
- traceback.print_exc()
- plugin._coverage_enabled = False
- continue
- else:
- # No plugin wanted it: it's Python.
- disp.trace = True
- disp.source_filename = canonical
-
- if not disp.has_dynamic_filename:
- if not disp.source_filename:
- raise CoverageException(
- "Plugin %r didn't set source_filename for %r" %
- (plugin, disp.original_filename)
- )
- reason = self._check_include_omit_etc_internal(
- disp.source_filename, frame,
- )
- if reason:
- nope(disp, reason)
-
- return disp
-
- def _check_include_omit_etc_internal(self, filename, frame):
- """Check a file name against the include, omit, etc, rules.
-
- Returns a string or None. String means, don't trace, and is the reason
- why. None means no reason found to not trace.
-
- """
- modulename = self._name_for_module(frame.f_globals, filename)
-
- # If the user specified source or include, then that's authoritative
- # about the outer bound of what to measure and we don't have to apply
- # any canned exclusions. If they didn't, then we have to exclude the
- # stdlib and coverage.py directories.
- if self.source_match:
- if self.source_pkgs_match.match(modulename):
- if modulename in self.source_pkgs_unmatched:
- self.source_pkgs_unmatched.remove(modulename)
- elif not self.source_match.match(filename):
- return "falls outside the --source trees"
- elif self.include_match:
- if not self.include_match.match(filename):
- return "falls outside the --include trees"
- else:
- # If we aren't supposed to trace installed code, then check if this
- # is near the Python standard library and skip it if so.
- if self.pylib_match and self.pylib_match.match(filename):
- return "is in the stdlib"
-
- # We exclude the coverage.py code itself, since a little of it
- # will be measured otherwise.
- if self.cover_match and self.cover_match.match(filename):
- return "is part of coverage.py"
-
- # Check the file against the omit pattern.
- if self.omit_match and self.omit_match.match(filename):
- return "is inside an --omit pattern"
-
- # No reason found to skip this file.
- return None
+ write_formatted_info(self._debug, "end", ())
def _should_trace(self, filename, frame):
"""Decide whether to trace execution in `filename`.
@@ -588,9 +309,9 @@ class Coverage(object):
Calls `_should_trace_internal`, and returns the FileDisposition.
"""
- disp = self._should_trace_internal(filename, frame)
- if self.debug.should('trace'):
- self.debug.write(_disposition_debug_msg(disp))
+ disp = self._inorout.should_trace(filename, frame)
+ if self._debug.should('trace'):
+ self._debug.write(disposition_debug_msg(disp))
return disp
def _check_include_omit_etc(self, filename, frame):
@@ -599,13 +320,13 @@ class Coverage(object):
Returns a boolean: True if the file should be traced, False if not.
"""
- reason = self._check_include_omit_etc_internal(filename, frame)
- if self.debug.should('trace'):
+ reason = self._inorout.check_include_omit_etc(filename, frame)
+ if self._debug.should('trace'):
if not reason:
msg = "Including %r" % (filename,)
else:
msg = "Not including %r: %s" % (filename, reason)
- self.debug.write(msg)
+ self._debug.write(msg)
return not reason
@@ -621,7 +342,7 @@ class Coverage(object):
self._warnings.append(msg)
if slug:
msg = "%s (%s)" % (msg, slug)
- if self.debug.should('pid'):
+ if self._debug.should('pid'):
msg = "[%d] %s" % (os.getpid(), msg)
sys.stderr.write("Coverage.py warning: %s\n" % msg)
@@ -673,8 +394,8 @@ class Coverage(object):
def load(self):
"""Load previously-collected coverage data from the data file."""
self._init()
- self.collector.reset()
- self.data_files.read(self.data)
+ self._collector.reset()
+ self._data_files.read(self.data)
def start(self):
"""Start measuring code coverage.
@@ -688,29 +409,32 @@ class Coverage(object):
"""
self._init()
- if self.include:
- if self.source or self.source_pkgs:
- self._warn("--include is ignored because --source is set", slug="include-ignored")
- if self.run_suffix:
+ self._inorout.warn_conflicting_settings()
+
+ if self._run_suffix:
# Calling start() means we're running code, so use the run_suffix
# as the data_suffix when we eventually save the data.
- self.data_suffix = self.run_suffix
+ self._data_suffix = self._run_suffix
if self._auto_load:
self.load()
- self.collector.start()
+ # See if we think some code that would eventually be measured has already been imported.
+ if self._warn_preimported_source:
+ self._inorout.warn_already_imported_files()
+
+ self._collector.start()
self._started = True
def stop(self):
"""Stop measuring code coverage."""
if self._started:
- self.collector.stop()
+ self._collector.stop()
self._started = False
def _atexit(self):
"""Clean up on process shutdown."""
- if self.debug.should("process"):
- self.debug.write("atexit: {0!r}".format(self))
+ if self._debug.should("process"):
+ self._debug.write("atexit: {0!r}".format(self))
if self._started:
self.stop()
if self._auto_save:
@@ -724,9 +448,9 @@ class Coverage(object):
"""
self._init()
- self.collector.reset()
+ self._collector.reset()
self.data.erase()
- self.data_files.erase(parallel=self.config.parallel)
+ self._data_files.erase(parallel=self.config.parallel)
def clear_exclude(self, which='exclude'):
"""Clear the exclude list."""
@@ -779,7 +503,7 @@ class Coverage(object):
"""Save the collected coverage data to the data file."""
self._init()
self.get_data()
- self.data_files.write(self.data, suffix=self.data_suffix)
+ self._data_files.write(self.data, suffix=self._data_suffix)
def combine(self, data_paths=None, strict=False):
"""Combine together a number of similarly-named coverage data files.
@@ -814,7 +538,7 @@ class Coverage(object):
for pattern in paths[1:]:
aliases.add(pattern, result)
- self.data_files.combine_parallel_data(
+ self._data_files.combine_parallel_data(
self.data, aliases=aliases, data_paths=data_paths, strict=strict,
)
@@ -830,7 +554,7 @@ class Coverage(object):
"""
self._init()
- if self.collector.save_data(self.data):
+ if self._collector.save_data(self.data):
self._post_save_work()
return self.data
@@ -845,83 +569,19 @@ class Coverage(object):
# If there are still entries in the source_pkgs_unmatched list,
# then we never encountered those packages.
if self._warn_unimported_source:
- for pkg in self.source_pkgs_unmatched:
- self._warn_about_unmeasured_code(pkg)
+ self._inorout.warn_unimported_source()
# Find out if we got any data.
if not self.data and self._warn_no_data:
self._warn("No data was collected.", slug="no-data-collected")
# Find files that were never executed at all.
- for pkg in self.source_pkgs:
- if (not pkg in sys.modules or
- not hasattr(sys.modules[pkg], '__file__') or
- not os.path.exists(sys.modules[pkg].__file__)):
- continue
- pkg_file = source_for_file(sys.modules[pkg].__file__)
- self._find_unexecuted_files(self._canonical_path(pkg_file))
-
- for src in self.source:
- self._find_unexecuted_files(src)
+ for file_path, plugin_name in self._inorout.find_unexecuted_files():
+ self.data.touch_file(file_path, plugin_name)
if self.config.note:
self.data.add_run_info(note=self.config.note)
- def _warn_about_unmeasured_code(self, pkg):
- """Warn about a package or module that we never traced.
-
- `pkg` is a string, the name of the package or module.
-
- """
- mod = sys.modules.get(pkg)
- if mod is None:
- self._warn("Module %s was never imported." % pkg, slug="module-not-imported")
- return
-
- is_namespace = hasattr(mod, '__path__') and not hasattr(mod, '__file__')
- has_file = hasattr(mod, '__file__') and os.path.exists(mod.__file__)
-
- if is_namespace:
- # A namespace package. It's OK for this not to have been traced,
- # since there is no code directly in it.
- return
-
- if not has_file:
- self._warn("Module %s has no Python source." % pkg, slug="module-not-python")
- return
-
- # The module was in sys.modules, and seems like a module with code, but
- # we never measured it. I guess that means it was imported before
- # coverage even started.
- self._warn(
- "Module %s was previously imported, but not measured" % pkg,
- slug="module-not-measured",
- )
-
- def _find_plugin_files(self, src_dir):
- """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_unexecuted_files(self, src_dir):
- """Find unexecuted files in `src_dir`.
-
- Search for files in `src_dir` that are probably importable,
- and add them as unexecuted files in `self.data`.
-
- """
- py_files = ((py_file, None) for py_file in find_python_files(src_dir))
- 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.
- continue
- self.data.touch_file(file_path, plugin_name)
-
# Backward compatibility with version 1.
def analysis(self, morf):
"""Like `analysis2` but doesn't return excluded line numbers."""
@@ -976,7 +636,7 @@ class Coverage(object):
abs_morf = abs_file(morf)
plugin_name = self.data.file_tracer(abs_morf)
if plugin_name:
- plugin = self.plugins.get(plugin_name)
+ plugin = self._plugins.get(plugin_name)
if plugin:
file_reporter = plugin.file_reporter(abs_morf)
@@ -1165,14 +825,13 @@ class Coverage(object):
info = [
('version', covmod.__version__),
('coverage', covmod.__file__),
- ('cover_paths', self.cover_paths),
- ('pylib_paths', self.pylib_paths),
- ('tracer', self.collector.tracer_name()),
- ('plugins.file_tracers', plugin_info(self.plugins.file_tracers)),
- ('plugins.configurers', plugin_info(self.plugins.configurers)),
- ('config_files', self.config.attempted_config_files),
- ('configs_read', self.config.config_files),
- ('data_path', self.data_files.filename),
+ ('tracer', self._collector.tracer_name()),
+ ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)),
+ ('plugins.configurers', plugin_info(self._plugins.configurers)),
+ ('configs_attempted', self.config.attempted_config_files),
+ ('configs_read', self.config.config_files_read),
+ ('config_file', self.config.config_file),
+ ('data_path', self._data_files.filename),
('python', sys.version.replace('\n', '')),
('platform', platform.platform()),
('implementation', platform.python_implementation()),
@@ -1187,51 +846,11 @@ class Coverage(object):
('command_line', " ".join(getattr(sys, 'argv', ['???']))),
]
- matcher_names = [
- 'source_match', 'source_pkgs_match',
- 'include_match', 'omit_match',
- 'cover_match', 'pylib_match',
- ]
-
- for matcher_name in matcher_names:
- matcher = getattr(self, matcher_name)
- if matcher:
- matcher_info = matcher.info()
- else:
- matcher_info = '-none-'
- info.append((matcher_name, matcher_info))
+ info.extend(self._inorout.sys_info())
return info
-# FileDisposition "methods": FileDisposition is a pure value object, so it can
-# be implemented in either C or Python. Acting on them is done with these
-# functions.
-
-def _disposition_init(cls, original_filename):
- """Construct and initialize a new FileDisposition object."""
- disp = cls()
- disp.original_filename = original_filename
- disp.canonical_filename = original_filename
- disp.source_filename = None
- disp.trace = False
- disp.reason = ""
- disp.file_tracer = None
- disp.has_dynamic_filename = False
- return disp
-
-
-def _disposition_debug_msg(disp):
- """Make a nice debug message of what the FileDisposition is doing."""
- if disp.trace:
- msg = "Tracing %r" % (disp.original_filename,)
- if disp.file_tracer:
- msg += ": will be traced by %r" % disp.file_tracer
- else:
- msg = "Not tracing %r: %s" % (disp.original_filename, disp.reason)
- return msg
-
-
def process_startup():
"""Call this at Python start-up to perhaps measure coverage.
@@ -1277,10 +896,11 @@ def process_startup():
cov = Coverage(config_file=cps)
process_startup.coverage = cov
- cov.start()
cov._warn_no_data = False
cov._warn_unimported_source = False
+ cov._warn_preimported_source = False
cov._auto_save = True
+ cov.start()
return cov
diff --git a/coverage/debug.py b/coverage/debug.py
index e68736f6..6e6e8013 100644
--- a/coverage/debug.py
+++ b/coverage/debug.py
@@ -215,7 +215,7 @@ class DebugOutputFile(object): # pragma: debugging
self.write("New process: executable: %s\n" % (sys.executable,))
self.write("New process: cmd: %s\n" % (cmd,))
if hasattr(os, 'getppid'):
- self.write("New process: parent pid: %s\n" % (os.getppid(),))
+ self.write("New process: pid: %s, parent pid: %s\n" % (os.getpid(), os.getppid()))
SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one'
@@ -234,7 +234,8 @@ class DebugOutputFile(object): # pragma: debugging
# on a class attribute. Yes, this is aggressively gross.
the_one = sys.modules.get(cls.SYS_MOD_NAME)
if the_one is None:
- assert fileobj is not None
+ if fileobj is None:
+ fileobj = open("/tmp/debug_log.txt", "a")
sys.modules[cls.SYS_MOD_NAME] = the_one = cls(fileobj, show_process, filters)
return the_one
diff --git a/coverage/disposition.py b/coverage/disposition.py
new file mode 100644
index 00000000..e9b8ba65
--- /dev/null
+++ b/coverage/disposition.py
@@ -0,0 +1,37 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
+
+"""Simple value objects for tracking what to do with files."""
+
+
+class FileDisposition(object):
+ """A simple value type for recording what to do with a file."""
+ pass
+
+
+# FileDisposition "methods": FileDisposition is a pure value object, so it can
+# be implemented in either C or Python. Acting on them is done with these
+# functions.
+
+def disposition_init(cls, original_filename):
+ """Construct and initialize a new FileDisposition object."""
+ disp = cls()
+ disp.original_filename = original_filename
+ disp.canonical_filename = original_filename
+ disp.source_filename = None
+ disp.trace = False
+ disp.reason = ""
+ disp.file_tracer = None
+ disp.has_dynamic_filename = False
+ return disp
+
+
+def disposition_debug_msg(disp):
+ """Make a nice debug message of what the FileDisposition is doing."""
+ if disp.trace:
+ msg = "Tracing %r" % (disp.original_filename,)
+ if disp.file_tracer:
+ msg += ": will be traced by %r" % disp.file_tracer
+ else:
+ msg = "Not tracing %r: %s" % (disp.original_filename, disp.reason)
+ return msg
diff --git a/coverage/execfile.py b/coverage/execfile.py
index 42e0d96a..68417f8a 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -9,6 +9,7 @@ import struct
import sys
import types
+from coverage import env
from coverage.backward import BUILTINS
from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec
from coverage.misc import CoverageException, ExceptionDuringRun, NoCode, NoSource, isolate_module
@@ -111,7 +112,15 @@ def run_python_module(modulename, args):
pathname = os.path.abspath(pathname)
args[0] = pathname
- run_python_file(pathname, args, package=packagename, modulename=modulename, path0="")
+ # Python 3.7.0b3 changed the behavior of the sys.path[0] entry for -m. It
+ # used to be an empty string (meaning the current directory). It changed
+ # to be the actual path to the current directory, so that os.chdir wouldn't
+ # affect the outcome.
+ if env.PYVERSION >= (3, 7, 0, 'beta', 3):
+ path0 = os.getcwd()
+ else:
+ path0 = ""
+ run_python_file(pathname, args, package=packagename, modulename=modulename, path0=path0)
def run_python_file(filename, args, package=None, modulename=None, path0=None):
@@ -128,7 +137,7 @@ def run_python_file(filename, args, package=None, modulename=None, path0=None):
function will decide on a value.
"""
- if modulename is None and sys.version_info >= (3, 3):
+ if modulename is None and env.PYVERSION >= (3, 3):
modulename = '__main__'
# Create a module to serve as __main__
@@ -255,7 +264,7 @@ def make_code_from_pyc(filename):
raise NoCode("Bad magic number in .pyc file")
date_based = True
- if sys.version_info >= (3, 7, 0, 'alpha', 4):
+ if env.PYVERSION >= (3, 7, 0, 'alpha', 4):
flags = struct.unpack('<L', fpyc.read(4))[0]
hash_based = flags & 0x01
if hash_based:
@@ -264,7 +273,7 @@ def make_code_from_pyc(filename):
if date_based:
# Skip the junk in the header that we don't need.
fpyc.read(4) # Skip the moddate.
- if sys.version_info >= (3, 3):
+ if env.PYVERSION >= (3, 3):
# 3.3 added another long to the header (size), skip it.
fpyc.read(4)
diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js
index f6f5de20..c1a41192 100644
--- a/coverage/htmlfiles/coverage_html.js
+++ b/coverage/htmlfiles/coverage_html.js
@@ -555,11 +555,16 @@ coverage.resize_scroll_markers = function () {
var previous_line = -99,
last_mark,
- last_top;
+ last_top,
+ offsets = {};
+ // Calculate line offsets outside loop to prevent relayouts
+ c.missed_lines.each(function() {
+ offsets[this.id] = $(this).offset().top;
+ });
c.missed_lines.each(function () {
- var line_top = Math.round($(this).offset().top * marker_scale),
- id_name = $(this).attr('id'),
+ var id_name = $(this).attr('id'),
+ line_top = Math.round(offsets[id_name] * marker_scale),
line_number = parseInt(id_name.substring(1, id_name.length));
if (line_number === previous_line + 1) {
diff --git a/coverage/htmlfiles/style.css b/coverage/htmlfiles/style.css
index 86b82091..12e90645 100644
--- a/coverage/htmlfiles/style.css
+++ b/coverage/htmlfiles/style.css
@@ -365,6 +365,7 @@ td.text {
height: 100%;
background: white;
border-left: 1px solid #eee;
+ will-change: transform; /* for faster scrolling of fixed element in Chrome */
}
#scroll_marker .marker {
diff --git a/coverage/inorout.py b/coverage/inorout.py
new file mode 100644
index 00000000..c0f27d78
--- /dev/null
+++ b/coverage/inorout.py
@@ -0,0 +1,461 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
+
+"""Determining whether files are being measured/reported or not."""
+
+# For finding the stdlib
+import atexit
+import inspect
+import itertools
+import os
+import platform
+import re
+import sys
+import traceback
+
+from coverage import env
+from coverage.disposition import FileDisposition, disposition_init
+from coverage.files import TreeMatcher, FnmatchMatcher, ModuleMatcher
+from coverage.files import prep_patterns, find_python_files, canonical_filename
+from coverage.misc import CoverageException
+from coverage.python import source_for_file, source_for_morf
+
+
+# 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
+if env.PYPY:
+ try:
+ import _structseq
+ except ImportError:
+ pass
+
+ try:
+ import _pypy_irc_topic
+ except ImportError:
+ pass
+
+
+def canonical_path(morf, directory=False):
+ """Return the canonical path of the module or file `morf`.
+
+ If the module is a package, then return its directory. If it is a
+ module, then return its file, unless `directory` is True, in which
+ case return its enclosing directory.
+
+ """
+ morf_path = canonical_filename(source_for_morf(morf))
+ if morf_path.endswith("__init__.py") or directory:
+ morf_path = os.path.split(morf_path)[0]
+ return morf_path
+
+
+def name_for_module(filename, frame):
+ """Get the name of the module for a filename and frame.
+
+ For configurability's sake, we allow __main__ modules to be matched by
+ their importable name.
+
+ If loaded via runpy (aka -m), we can usually recover the "original"
+ full dotted module name, otherwise, we resort to interpreting the
+ file name to get the module's name. In the case that the module name
+ can't be determined, None is returned.
+
+ """
+ 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 = module_globals.get('__name__', None)
+
+ 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
+ if hasattr(loader, attrname):
+ fullname = getattr(loader, attrname)
+ else:
+ continue
+
+ if isinstance(fullname, str) and fullname != '__main__':
+ # Module loaded via: runpy -m
+ return fullname
+
+ # Script as first argument to Python command line.
+ inspectedname = inspect.getmodulename(filename)
+ if inspectedname is not None:
+ return inspectedname
+ else:
+ return dunder_name
+
+
+def module_is_namespace(mod):
+ """Is the module object `mod` a PEP420 namespace module?"""
+ return hasattr(mod, '__path__') and getattr(mod, '__file__', None) is None
+
+
+def module_has_file(mod):
+ """Does the module object `mod` have an existing __file__ ?"""
+ mod__file__ = getattr(mod, '__file__', None)
+ if mod__file__ is None:
+ return False
+ return os.path.exists(mod__file__)
+
+
+class InOrOut(object):
+ """Machinery for determining what files to measure."""
+
+ def __init__(self, warn):
+ self.warn = warn
+
+ # The matchers for should_trace.
+ self.source_match = None
+ self.source_pkgs_match = None
+ self.pylib_paths = self.cover_paths = None
+ self.pylib_match = self.cover_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
+
+ def configure(self, config):
+ """Apply the configuration to get ready for decision-time."""
+ for src in config.source or []:
+ if os.path.isdir(src):
+ self.source.append(canonical_filename(src))
+ else:
+ 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)
+
+ # The directories for files considered "installed with the interpreter".
+ self.pylib_paths = set()
+ if not config.cover_pylib:
+ # 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.
+ for m in (atexit, inspect, os, platform, _pypy_irc_topic, re, _structseq, traceback):
+ if m is not None and hasattr(m, "__file__"):
+ self.pylib_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.
+ structseq_new = _structseq.structseq_new
+ try:
+ structseq_file = structseq_new.func_code.co_filename
+ except AttributeError:
+ structseq_file = structseq_new.__code__.co_filename
+ self.pylib_paths.add(canonical_path(structseq_file))
+
+ # To avoid tracing the coverage.py code itself, we skip anything
+ # located where we are.
+ self.cover_paths = [canonical_path(__file__, directory=True)]
+ if env.TESTING:
+ # Don't include our own test code.
+ self.cover_paths.append(os.path.join(self.cover_paths[0], "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.
+ import contracts
+ import six
+ for mod in [contracts, six]:
+ self.cover_paths.append(canonical_path(mod))
+
+ # Create the matchers we need for should_trace
+ if self.source or self.source_pkgs:
+ self.source_match = TreeMatcher(self.source)
+ self.source_pkgs_match = ModuleMatcher(self.source_pkgs)
+ else:
+ if self.cover_paths:
+ self.cover_match = TreeMatcher(self.cover_paths)
+ if self.pylib_paths:
+ self.pylib_match = TreeMatcher(self.pylib_paths)
+ if self.include:
+ self.include_match = FnmatchMatcher(self.include)
+ if self.omit:
+ self.omit_match = FnmatchMatcher(self.omit)
+
+ def should_trace(self, filename, frame=None):
+ """Decide whether to trace execution in `filename`, with a reason.
+
+ This function is called from the trace function. As each new file name
+ is encountered, this function determines whether it is traced or not.
+
+ Returns a FileDisposition object.
+
+ """
+ original_filename = filename
+ disp = disposition_init(self.disp_class, filename)
+
+ def nope(disp, reason):
+ """Simple helper to make it easy to return NO."""
+ disp.trace = False
+ disp.reason = reason
+ return disp
+
+ if frame is not None:
+ # Compiled Python files have two file names: frame.f_code.co_filename is
+ # the file name at the time the .pyc was compiled. The second name is
+ # __file__, which is where the .pyc was actually loaded from. Since
+ # .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__')
+ if dunder_file:
+ filename = source_for_file(dunder_file)
+ 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
+ # exec'ing code. If it seems like something is wrong with
+ # the frame's file name, then just use the original.
+ filename = original_filename
+
+ if not filename:
+ # Empty string is pretty useless.
+ return nope(disp, "empty string isn't a file name")
+
+ if filename.startswith('memory:'):
+ return nope(disp, "memory isn't traceable")
+
+ if filename.startswith('<'):
+ # Lots of non-file execution is represented with artificial
+ # file names like "<string>", "<doctest readme.txt[0]>", or
+ # "<exec_function>". 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")
+
+ # pyexpat does a dumb thing, calling the trace function explicitly from
+ # C code with a C file name.
+ if re.search(r"[/\\]Modules[/\\]pyexpat.c", filename):
+ return nope(disp, "pyexpat lies about itself")
+
+ # Jython reports the .class file to the tracer, use the source file.
+ if filename.endswith("$py.class"):
+ filename = filename[:-9] + ".py"
+
+ canonical = canonical_filename(filename)
+ disp.canonical_filename = canonical
+
+ # Try the plugins, see if they have an opinion about the file.
+ plugin = None
+ for plugin in self.plugins.file_tracers:
+ if not plugin._coverage_enabled:
+ continue
+
+ try:
+ file_tracer = plugin.file_tracer(canonical)
+ if file_tracer is not None:
+ file_tracer._coverage_plugin = plugin
+ disp.trace = True
+ disp.file_tracer = file_tracer
+ if file_tracer.has_dynamic_source_filename():
+ disp.has_dynamic_filename = True
+ else:
+ disp.source_filename = canonical_filename(
+ file_tracer.source_filename()
+ )
+ break
+ except Exception:
+ self.warn(
+ "Disabling plug-in %r due to an exception:" % (plugin._coverage_plugin_name)
+ )
+ traceback.print_exc()
+ plugin._coverage_enabled = False
+ continue
+ else:
+ # No plugin wanted it: it's Python.
+ disp.trace = True
+ disp.source_filename = canonical
+
+ if not disp.has_dynamic_filename:
+ if not disp.source_filename:
+ raise CoverageException(
+ "Plugin %r didn't set source_filename for %r" %
+ (plugin, disp.original_filename)
+ )
+ reason = self.check_include_omit_etc(disp.source_filename, frame)
+ if reason:
+ nope(disp, reason)
+
+ return disp
+
+ def check_include_omit_etc(self, filename, frame):
+ """Check a file name against the include, omit, etc, rules.
+
+ Returns a string or None. String means, don't trace, and is the reason
+ why. None means no reason found to not trace.
+
+ """
+ modulename = name_for_module(filename, frame)
+
+ # If the user specified source or include, then that's authoritative
+ # about the outer bound of what to measure and we don't have to apply
+ # any canned exclusions. If they didn't, then we have to exclude the
+ # stdlib and coverage.py directories.
+ if self.source_match:
+ if self.source_pkgs_match.match(modulename):
+ if modulename in self.source_pkgs_unmatched:
+ self.source_pkgs_unmatched.remove(modulename)
+ elif not self.source_match.match(filename):
+ return "falls outside the --source trees"
+ elif self.include_match:
+ if not self.include_match.match(filename):
+ return "falls outside the --include trees"
+ else:
+ # If we aren't supposed to trace installed code, then check if this
+ # is near the Python standard library and skip it if so.
+ if self.pylib_match and self.pylib_match.match(filename):
+ return "is in the stdlib"
+
+ # We exclude the coverage.py code itself, since a little of it
+ # will be measured otherwise.
+ if self.cover_match and self.cover_match.match(filename):
+ return "is part of coverage.py"
+
+ # Check the file against the omit pattern.
+ if self.omit_match and self.omit_match.match(filename):
+ return "is inside an --omit pattern"
+
+ # No reason found to skip this file.
+ return None
+
+ def warn_conflicting_settings(self):
+ """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):
+ """Warn if files have already been imported that we will be measuring."""
+ if self.include or self.source or self.source_pkgs:
+ warned = set()
+ for mod in list(sys.modules.values()):
+ filename = getattr(mod, "__file__", None)
+ if filename is None:
+ continue
+ if filename in warned:
+ continue
+
+ disp = self.should_trace(filename)
+ if disp.trace:
+ msg = "Already imported a file that will be measured: {0}".format(filename)
+ self.warn(msg, slug="already-imported")
+ warned.add(filename)
+
+ def warn_unimported_source(self):
+ """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):
+ """Warn about a package or module that we never traced.
+
+ `pkg` is a string, the name of the package or module.
+
+ """
+ mod = sys.modules.get(pkg)
+ if mod is None:
+ self.warn("Module %s was never imported." % pkg, slug="module-not-imported")
+ return
+
+ if module_is_namespace(mod):
+ # A namespace package. It's OK for this not to have been traced,
+ # since there is no code directly in it.
+ return
+
+ if not module_has_file(mod):
+ self.warn("Module %s has no Python source." % pkg, slug="module-not-python")
+ return
+
+ # The module was in sys.modules, and seems like a module with code, but
+ # we never measured it. I guess that means it was imported before
+ # coverage even started.
+ self.warn(
+ "Module %s was previously imported, but not measured" % pkg,
+ slug="module-not-measured",
+ )
+
+ def find_unexecuted_files(self):
+ """Find files in the areas of interest that weren't traced.
+
+ Yields pairs: file path, and responsible plug-in name.
+ """
+ for pkg in self.source_pkgs:
+ if (not pkg in sys.modules or
+ not module_has_file(sys.modules[pkg])):
+ continue
+ pkg_file = source_for_file(sys.modules[pkg].__file__)
+ for ret in self._find_unexecuted_files(canonical_path(pkg_file)):
+ yield ret
+
+ for src in self.source:
+ for ret in self._find_unexecuted_files(src):
+ yield ret
+
+ def _find_plugin_files(self, src_dir):
+ """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_unexecuted_files(self, src_dir):
+ """Find unexecuted files in `src_dir`.
+
+ Search for files in `src_dir` that are probably importable,
+ and add them as unexecuted files in `self.data`.
+
+ """
+ py_files = ((py_file, None) for py_file in find_python_files(src_dir))
+ 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.
+ continue
+ yield file_path, plugin_name
+
+ def sys_info(self):
+ """Our information for Coverage.sys_info.
+
+ Returns a list of (key, value) pairs.
+ """
+ info = [
+ ('cover_paths', self.cover_paths),
+ ('pylib_paths', self.pylib_paths),
+ ]
+
+ matcher_names = [
+ 'source_match', 'source_pkgs_match',
+ 'include_match', 'omit_match',
+ 'cover_match', 'pylib_match',
+ ]
+
+ for matcher_name in matcher_names:
+ matcher = getattr(self, matcher_name)
+ if matcher:
+ matcher_info = matcher.info()
+ else:
+ matcher_info = '-none-'
+ info.append((matcher_name, matcher_info))
+
+ return info
diff --git a/coverage/multiproc.py b/coverage/multiproc.py
index fe837318..bbc88fbe 100644
--- a/coverage/multiproc.py
+++ b/coverage/multiproc.py
@@ -6,19 +6,16 @@
import multiprocessing
import multiprocessing.process
import os
-import sys
+from coverage import env
from coverage.misc import contract
# An attribute that will be set on the module to indicate that it has been
# monkey-patched.
PATCHED_MARKER = "_coverage$patched"
-# The environment variable that specifies the rcfile for subprocesses.
-COVERAGE_RCFILE_ENV = "_COVERAGE_RCFILE"
-
-if sys.version_info >= (3, 4):
+if env.PYVERSION >= (3, 4):
OriginalProcess = multiprocessing.process.BaseProcess
else:
OriginalProcess = multiprocessing.Process
@@ -31,10 +28,10 @@ class ProcessWithCoverage(OriginalProcess):
def _bootstrap(self):
"""Wrapper around _bootstrap to start coverage."""
from coverage import Coverage # avoid circular import
- rcfile = os.environ[COVERAGE_RCFILE_ENV]
- cov = Coverage(data_suffix=True, config_file=rcfile)
+ cov = Coverage(data_suffix=True)
+ cov._warn_preimported_source = False
cov.start()
- debug = cov.debug
+ debug = cov._debug
try:
if debug.should("multiproc"):
debug.write("Calling multiprocessing bootstrap")
@@ -73,14 +70,14 @@ def patch_multiprocessing(rcfile):
if hasattr(multiprocessing, PATCHED_MARKER):
return
- if sys.version_info >= (3, 4):
+ if env.PYVERSION >= (3, 4):
OriginalProcess._bootstrap = ProcessWithCoverage._bootstrap
else:
multiprocessing.Process = ProcessWithCoverage
# Set the value in ProcessWithCoverage that will be pickled into the child
# process.
- os.environ[COVERAGE_RCFILE_ENV] = rcfile
+ os.environ["COVERAGE_RCFILE"] = rcfile
# When spawning processes rather than forking them, we have no state in the
# new process. We sneak in there with a Stowaway: we stuff one of our own
diff --git a/coverage/plugin.py b/coverage/plugin.py
index db7ca0a7..415246ab 100644
--- a/coverage/plugin.py
+++ b/coverage/plugin.py
@@ -134,7 +134,8 @@ class CoveragePlugin(object):
This will only be invoked if `filename` returns non-None from
:meth:`file_tracer`. It's an error to return None from this method.
- Returns a :class:`FileReporter` object to use to report on `filename`.
+ Returns a :class:`FileReporter` object to use to report on `filename`,
+ or the string `"python"` to have coverage.py treat the file as Python.
"""
_needs_to_implement(self, "file_reporter")
diff --git a/coverage/python.py b/coverage/python.py
index 372347f5..834bc332 100644
--- a/coverage/python.py
+++ b/coverage/python.py
@@ -97,7 +97,7 @@ def get_zip_bytes(filename):
def source_for_file(filename):
- """Return the source file for `filename`.
+ """Return the source filename for `filename`.
Given a file name being traced, return the best guess as to the source
file to attribute it to.
@@ -129,22 +129,28 @@ def source_for_file(filename):
return filename
+def source_for_morf(morf):
+ """Get the source filename for the module-or-file `morf`."""
+ 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.
+ # This could be a PEP-420 namespace package.
+ raise CoverageException("Module {0} has no file".format(morf))
+ else:
+ filename = morf
+
+ filename = source_for_file(files.unicode_filename(filename))
+ return filename
+
+
class PythonFileReporter(FileReporter):
"""Report support for a Python file."""
def __init__(self, morf, coverage=None):
self.coverage = coverage
- if hasattr(morf, '__file__'):
- filename = morf.__file__
- elif isinstance(morf, types.ModuleType):
- # A module should have had .__file__, otherwise we can't use it.
- # This could be a PEP-420 namespace package.
- raise CoverageException("Module {0} has no file".format(morf))
- else:
- filename = morf
-
- filename = source_for_file(files.unicode_filename(filename))
+ filename = source_for_morf(morf)
super(PythonFileReporter, self).__init__(files.canonical_filename(filename))
diff --git a/coverage/version.py b/coverage/version.py
index 7dc59e27..0e6b0f9c 100644
--- a/coverage/version.py
+++ b/coverage/version.py
@@ -5,7 +5,7 @@
# This file is exec'ed in setup.py, don't import anything!
# Same semantics as sys.version_info.
-version_info = (4, 5, 1, 'final', 0)
+version_info = (5, 0, 0, 'alpha', 2)
def _make_version(major, minor, micro, releaselevel, serial):
diff --git a/doc/cmd.rst b/doc/cmd.rst
index ef4c1135..baf1ca08 100644
--- a/doc/cmd.rst
+++ b/doc/cmd.rst
@@ -171,6 +171,13 @@ could affect the measurement process. The possible warnings include:
when coverage started. This meant coverage.py couldn't monitor its
execution.
+* "Already imported a file that will be measured: XXX (already-imported)"
+
+ File XXX had already been imported when coverage.py started measurement. Your
+ setting for ``--source`` or ``--include`` indicates that you wanted to
+ measure that file. Lines will be missing from the coverage report since the
+ execution during import hadn't been measured.
+
* "--include is ignored because --source is set (include-ignored)"
Both ``--include`` and ``--source`` were specified while running code. Both
diff --git a/doc/conf.py b/doc/conf.py
index 08c88537..503387b5 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -56,9 +56,9 @@ copyright = u'2009\N{EN DASH}2018, Ned Batchelder' # CHANGEME
# built documents.
#
# The short X.Y version.
-version = '4.5' # CHANGEME
+version = '5.0' # CHANGEME
# The full version, including alpha/beta/rc tags.
-release = '4.5.1' # CHANGEME
+release = '5.0a1' # CHANGEME
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/doc/config.rst b/doc/config.rst
index c1fb4b1b..062aa740 100644
--- a/doc/config.rst
+++ b/doc/config.rst
@@ -33,8 +33,9 @@ configuration file are tied to your source code and how it should be measured,
so it should be stored with your source, and checked into source control,
rather than put in your home directory.
-A different name for the configuration file can be specified with the
-``--rcfile=FILE`` command line option.
+A different location for the configuration file can be specified with the
+``--rcfile=FILE`` command line option or with the ``COVERAGE_RCFILE``
+environment variable.
Coverage.py will read settings from other usual configuration files if no other
configuration file is used. It will automatically read from "setup.cfg" or
diff --git a/doc/contributing.rst b/doc/contributing.rst
index 3f628109..1b06bed7 100644
--- a/doc/contributing.rst
+++ b/doc/contributing.rst
@@ -117,7 +117,7 @@ the second uses the C implementation.
To limit tox to just a few versions of Python, use the ``-e`` switch::
- $ tox -e py27,py33
+ $ tox -e py27,py37
To run just a few tests, you can use `pytest test selectors`_::
diff --git a/doc/faq.rst b/doc/faq.rst
index a0145362..fb9dbeb2 100644
--- a/doc/faq.rst
+++ b/doc/faq.rst
@@ -109,15 +109,15 @@ __ https://nedbatchelder.com/blog/200710/flaws_in_coverage_measurement.html
- `trialcoverage`_ is a plug-in for Twisted trial.
- .. _trialcoverage: https://pypi.python.org/pypi/trialcoverage
+ .. _trialcoverage: https://pypi.org/project/trialcoverage/
- `pytest-coverage`_
- .. _pytest-coverage: https://pypi.python.org/pypi/pytest-coverage
+ .. _pytest-coverage: https://pypi.org/project/pytest-coverage/
- `django-coverage`_ for use with Django.
- .. _django-coverage: https://pypi.python.org/pypi/django-coverage
+ .. _django-coverage: https://pypi.org/project/django-coverage/
**Q: Where can I get more help with coverage.py?**
diff --git a/doc/index.rst b/doc/index.rst
index 44e53946..c6ddf5de 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -77,21 +77,21 @@ not.
.. ifconfig:: prerelease
- The latest version is coverage.py 4.4b1, released April 4th 2017. It is
+ The latest version is coverage.py 5.0a1, released June 5th 2018. It is
supported on:
- * Python versions 2.6, 2.7, 3.3, 3.4, 3.5, and 3.6.
+ * Python versions 2.7, 3.4, 3.5, 3.6, and 3.7.
- * PyPy2 5.6 and PyPy3 5.5.
+ * PyPy2 6.0 and PyPy3 6.0.
* Jython 2.7.1, though only for running code, not reporting.
* IronPython 2.7.7, though only for running code, not reporting.
**This is a pre-release build. The usual warnings about possible bugs
- apply.** The latest stable version is coverage.py 4.3.4, `described here`_.
+ apply.** The latest stable version is coverage.py 4.5.1, `described here`_.
-.. _described here: https://nedbatchelder.com/code/coverage
+.. _described here: http://coverage.readthedocs.io/
Quick start
@@ -145,7 +145,7 @@ Getting started is easy:
Then visit htmlcov/index.html in your browser, to see a
`report like this one`_.
-.. _coverage.py page on the Python Package Index: https://pypi.python.org/pypi/coverage
+.. _coverage.py page on the Python Package Index: https://pypi.org/project/coverage/
.. _report like this: https://nedbatchelder.com/files/sample_coverage_html/index.html
.. _report like this one: https://nedbatchelder.com/files/sample_coverage_html_beta/index.html
diff --git a/doc/install.rst b/doc/install.rst
index 29bc833e..4f3717b4 100644
--- a/doc/install.rst
+++ b/doc/install.rst
@@ -29,8 +29,8 @@ Installation
.. highlight:: console
-.. _coverage_pypi: https://pypi.python.org/pypi/coverage
-.. _setuptools: https://pypi.python.org/pypi/setuptools
+.. _coverage_pypi: https://pypi.org/project/coverage/
+.. _setuptools: https://pypi.org/project/setuptools/
You can install coverage.py in the usual ways. The simplest way is with pip::
diff --git a/doc/plugins.rst b/doc/plugins.rst
index f2bad6d4..e4967b4b 100644
--- a/doc/plugins.rst
+++ b/doc/plugins.rst
@@ -66,7 +66,7 @@ Some coverage.py plug-ins you might find useful:
* `Django template coverage.py plug-in`__: for measuring coverage in Django
templates.
- .. __: https://pypi.python.org/pypi/django_coverage_plugin
+ .. __: https://pypi.org/project/django_coverage_plugin/
* `Mako template coverage plug-in`__: for measuring coverage in Mako templates.
Doesn't work yet, probably needs some changes in Mako itself.
diff --git a/doc/python-coverage.1.txt b/doc/python-coverage.1.txt
index 94402b84..a415f080 100644
--- a/doc/python-coverage.1.txt
+++ b/doc/python-coverage.1.txt
@@ -8,7 +8,7 @@ measure code coverage of Python program execution
:Author: Ned Batchelder <ned@nedbatchelder.com>
:Author: |author|
-:Date: 2015-09-20
+:Date: 2018-05-28
:Copyright: Apache 2.0 license, attribution and disclaimer required.
:Manual section: 1
:Manual group: Coverage.py
@@ -219,6 +219,10 @@ COVERAGE_FILE
Path to the file where coverage measurements are collected to and
reported from. Default: ``.coverage`` in the current working directory.
+COVERAGE_RCFILE
+
+ Path to the configuration file, often named ``.coveragerc``.
+
HISTORY
=======
diff --git a/doc/requirements.pip b/doc/requirements.pip
index 73467c94..dbd6c8fa 100644
--- a/doc/requirements.pip
+++ b/doc/requirements.pip
@@ -3,9 +3,9 @@
# https://requires.io/github/nedbat/coveragepy/requirements/
pyenchant==2.0.0
-sphinx==1.6.6
-sphinxcontrib-spelling==4.0.1
-sphinx_rtd_theme==0.2.4
+sphinx==1.7.5
+sphinxcontrib-spelling==4.1.0
+sphinx_rtd_theme==0.3.1
# A version of doc8 with a -q flag.
git+https://github.com/nedbat/doc8.git#egg=doc8==0.0
diff --git a/doc/trouble.rst b/doc/trouble.rst
index e3951218..d152599a 100644
--- a/doc/trouble.rst
+++ b/doc/trouble.rst
@@ -64,7 +64,7 @@ timid=True`` configuration option.
DecoratorTools fiddles with the trace function. You will need to use
``--timid``.
-.. _DecoratorTools: https://pypi.python.org/pypi/DecoratorTools
+.. _DecoratorTools: https://pypi.org/project/DecoratorTools/
.. _TurboGears: http://turbogears.org/
diff --git a/howto.txt b/howto.txt
index 14c51916..b23757dc 100644
--- a/howto.txt
+++ b/howto.txt
@@ -10,6 +10,7 @@
- Update CHANGES.rst, including release date.
- Update README.rst
- "New in x.y:"
+ - Python versions supported
- Update docs
- Version, date and python versions in doc/index.rst
- Version and copyright date in doc/conf.py
@@ -57,7 +58,8 @@
- Update PyPi:
- upload kits:
- $ make kit_upload
- - Visit https://pypi.python.org/pypi?:action=pkg_edit&name=coverage :
+ - DON'T NEED TO DO THIS ANY MORE?
+ - Visit https://pypi.python.org/pypi?:action=pkg_edit&name=coverage :
- show/hide the proper versions.
- Tag the tree
- hg tag -m "Coverage 3.0.1" coverage-3.0.1
@@ -74,13 +76,17 @@
- Update readthedocs
- visit https://readthedocs.org/projects/coverage/versions/
- find the latest tag in the inactive list, edit it, make it active.
+ - keep just the latest version of each x.y release, make the rest inactive.
- IF NOT BETA:
+ - visit https://readthedocs.org/projects/coverage/builds/
+ - wait for the new tag build to finish successfully.
- visit https://readthedocs.org/dashboard/coverage/versions/
- change the default version to the new version
- Update bitbucket:
- Issue tracker should get new version number in picker.
# Note: don't delete old version numbers: it marks changes on the tickets
# with that number.
+- Visit the fixed issues on bitbucket and mention the version it was fixed in.
- Announce on coveragepy-announce@googlegroups.com .
- Announce on TIP.
diff --git a/igor.py b/igor.py
index 43ce3303..3f5ce12b 100644
--- a/igor.py
+++ b/igor.py
@@ -122,11 +122,8 @@ def run_tests_with_coverage(tracer, *runner_args):
import coverage
cov = coverage.Coverage(config_file="metacov.ini", data_suffix=False)
- # Cheap trick: the coverage.py code itself is excluded from measurement,
- # but if we clobber the cover_prefix in the coverage object, we can defeat
- # the self-detection.
- cov.cover_prefix = "Please measure coverage.py!"
cov._warn_unimported_source = False
+ cov._warn_preimported_source = False
cov.start()
try:
diff --git a/metacov.ini b/metacov.ini
index 55d0225e..eebfc0fd 100644
--- a/metacov.ini
+++ b/metacov.ini
@@ -35,6 +35,10 @@ exclude_lines =
# OS error conditions that we can't (or don't care to) replicate.
pragma: cant happen
+ # Obscure bugs in specific versions of interpreters, and so probably no
+ # longer tested.
+ pragma: obscure
+
# Jython needs special care.
pragma: only jython
skip.*Jython
diff --git a/perf/perf_measure.py b/perf/perf_measure.py
index 3b0ae52a..2125251a 100644
--- a/perf/perf_measure.py
+++ b/perf/perf_measure.py
@@ -78,7 +78,7 @@ class StressTest(object):
finally: # pragma: nested
# Stop coverage.py.
covered = time.perf_counter() - start
- stats = cov.collector.tracers[0].get_stats()
+ stats = cov._collector.tracers[0].get_stats()
if stats:
stats = stats.copy()
cov.stop()
diff --git a/requirements/dev.pip b/requirements/dev.pip
index 183d051f..98cac62e 100644
--- a/requirements/dev.pip
+++ b/requirements/dev.pip
@@ -11,11 +11,13 @@
# for linting.
greenlet==0.4.13
mock==2.0.0
-PyContracts==1.8.2
+PyContracts==1.8.3
pyenchant==2.0.0
-pylint==1.8.2
+pylint==1.9.1
unittest-mixins==1.4
+check-manifest==0.37
+readme_renderer==21.0
# for kitting.
requests==2.18.4
-twine==1.9.1
+twine==1.11.0
diff --git a/requirements/pytest.pip b/requirements/pytest.pip
index 17ccc0d8..d90c16b4 100644
--- a/requirements/pytest.pip
+++ b/requirements/pytest.pip
@@ -3,7 +3,6 @@
# The pytest specifics used by coverage.py
-# Keep pytest at 3.2.x until we are done with Python 2.6 and 3.3
-pytest==3.2.5
-pytest-xdist==1.20.1
+pytest==3.6.0
+pytest-xdist==1.22.2
flaky==3.4.0
diff --git a/requirements/tox.pip b/requirements/tox.pip
index b57aa388..a209ac7f 100644
--- a/requirements/tox.pip
+++ b/requirements/tox.pip
@@ -1,4 +1,4 @@
# The version of tox used by coverage.py
-tox==2.9.1
+tox==3.0.0
# Adds env recreation on requirements file changes.
-tox-battery==0.5
+tox-battery==0.5.1
diff --git a/requirements/wheel.pip b/requirements/wheel.pip
index 6dfe70b2..9c6bf0ca 100644
--- a/requirements/wheel.pip
+++ b/requirements/wheel.pip
@@ -1,4 +1,3 @@
# Things needed to make wheels for coverage.py
-setuptools==35.0.2
-# We need to stick with 0.29.0 until we drop 2.6 and 3.3
-wheel==0.29.0
+setuptools==39.2.0
+wheel==0.31.1
diff --git a/setup.py b/setup.py
index 77bc903c..99874fd4 100644
--- a/setup.py
+++ b/setup.py
@@ -24,10 +24,8 @@ License :: OSI Approved :: Apache Software License
Operating System :: OS Independent
Programming Language :: Python
Programming Language :: Python :: 2
-Programming Language :: Python :: 2.6
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
-Programming Language :: Python :: 3.3
Programming Language :: Python :: 3.4
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
@@ -107,7 +105,7 @@ setup_args = dict(
classifiers=classifier_list,
url="https://bitbucket.org/ned/coveragepy",
- python_requires=">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4",
+ python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4",
)
# A replacement for the build_ext command which raises a single exception
diff --git a/tests/modules/process_test/try_execfile.py b/tests/modules/process_test/try_execfile.py
index ec7dcbe5..3068327e 100644
--- a/tests/modules/process_test/try_execfile.py
+++ b/tests/modules/process_test/try_execfile.py
@@ -68,10 +68,15 @@ FN_VAL = my_function("fooey")
loader = globals().get('__loader__')
fullname = getattr(loader, 'fullname', None) or getattr(loader, 'name', None)
-# A more compact grouped-by-first-letter list of builtins.
+# 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):
- """Clump AB, CD, EF, etc."""
- return chr((ord(w[0]) + 1) & 0xFE)
+ """Figure out which CLUMP the first letter of w is in."""
+ for i, clump in enumerate(CLUMPS):
+ if w[0] in clump:
+ return i
+ return 99
builtin_dir = [" ".join(s) for _, s in itertools.groupby(dir(__builtins__), key=word_group)]
diff --git a/tests/test_api.py b/tests/test_api.py
index b461c503..feb8b2e6 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -5,6 +5,7 @@
import fnmatch
import os
+import os.path
import sys
import textwrap
import warnings
@@ -581,7 +582,7 @@ class SourceOmitIncludeTest(OmitIncludeTestsMixin, CoverageTest):
cov = coverage.Coverage(source=["pkg1"], include=["pkg2"])
with self.assert_warnings(cov, ["--include is ignored because --source is set"]):
cov.start()
- cov.stop()
+ cov.stop() # pragma: nested
def test_source_package_as_dir(self):
# pkg1 is a directory, since we cd'd into tests/modules in setUp.
diff --git a/tests/test_arcs.py b/tests/test_arcs.py
index ef71ea16..4bd804ba 100644
--- a/tests/test_arcs.py
+++ b/tests/test_arcs.py
@@ -407,8 +407,6 @@ class LoopArcTest(CoverageTest):
)
def test_other_comprehensions(self):
- if env.PYVERSION < (2, 7):
- self.skipTest("No set or dict comprehensions before 2.7")
# Set comprehension:
self.check_coverage("""\
o = ((1,2), (3,4))
@@ -431,8 +429,6 @@ class LoopArcTest(CoverageTest):
)
def test_multiline_dict_comp(self):
- if env.PYVERSION < (2, 7):
- self.skipTest("No set or dict comprehensions before 2.7")
if env.PYVERSION < (3, 5):
arcz = "-42 2B B-4 2-4"
else:
diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py
index 1b7c6653..66fcec3a 100644
--- a/tests/test_cmdline.py
+++ b/tests/test_cmdline.py
@@ -33,7 +33,7 @@ class BaseCmdLineTest(CoverageTest):
defaults.Coverage(
cover_pylib=None, data_suffix=None, timid=None, branch=None,
config_file=True, source=None, include=None, omit=None, debug=None,
- concurrency=None,
+ concurrency=None, check_preimported=True,
)
defaults.annotate(
directory=None, ignore_errors=None, include=None, omit=None, morfs=[],
diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py
index 71006042..88f2b50d 100644
--- a/tests/test_concurrency.py
+++ b/tests/test_concurrency.py
@@ -335,7 +335,7 @@ MULTI_CODE = """
import sys
def process_worker_main(args):
- # Need to pause, or the tasks go too quick, and some processes
+ # Need to pause, or the tasks go too quickly, and some processes
# in the pool don't get any work, and then don't record data.
time.sleep(0.02)
ret = work(*args)
@@ -359,7 +359,7 @@ MULTI_CODE = """
"""
-@flaky(max_runs=10) # Sometimes a test fails due to inherent randomness. Try one more time.
+@flaky(max_runs=10) # Sometimes a test fails due to inherent randomness. Try more times.
class MultiprocessingTest(CoverageTest):
"""Test support of the multiprocessing module."""
@@ -403,7 +403,7 @@ class MultiprocessingTest(CoverageTest):
last_line = self.squeezed_lines(out)[-1]
self.assertRegex(last_line, r"multi.py \d+ 0 100%")
- def test_multiprocessing(self):
+ def test_multiprocessing_simple(self):
nprocs = 3
upto = 30
code = (SQUARE_OR_CUBE_WORK + MULTI_CODE).format(NPROCS=nprocs, UPTO=upto)
@@ -464,7 +464,7 @@ def test_coverage_stop_in_threads():
has_started_coverage = []
has_stopped_coverage = []
- def run_thread():
+ def run_thread(): # pragma: nested
"""Check that coverage is stopping properly in threads."""
deadline = time.time() + 5
ident = threading.currentThread().ident
@@ -480,11 +480,11 @@ def test_coverage_stop_in_threads():
cov = coverage.coverage()
cov.start()
- t = threading.Thread(target=run_thread)
- t.start()
+ t = threading.Thread(target=run_thread) # pragma: nested
+ t.start() # pragma: nested
- time.sleep(0.1)
- cov.stop()
+ time.sleep(0.1) # pragma: nested
+ cov.stop() # pragma: nested
time.sleep(0.1)
assert has_started_coverage == [t.ident]
@@ -513,7 +513,7 @@ def test_thread_safe_save_data(tmpdir):
for module_name in module_names:
import_local_file(module_name)
- def random_load():
+ def random_load(): # pragma: nested
"""Import modules randomly to stress coverage."""
while should_run[0]:
module_name = random.choice(module_names)
@@ -529,12 +529,12 @@ def test_thread_safe_save_data(tmpdir):
cov = coverage.coverage()
cov.start()
- threads = [threading.Thread(target=random_load) for _ in range(10)]
- should_run[0] = True
- for t in threads:
+ threads = [threading.Thread(target=random_load) for _ in range(10)] # pragma: nested
+ should_run[0] = True # pragma: nested
+ for t in threads: # pragma: nested
t.start()
- time.sleep(duration)
+ time.sleep(duration) # pragma: nested
cov.stop()
@@ -546,7 +546,7 @@ def test_thread_safe_save_data(tmpdir):
for t in threads:
t.join()
- if (not imported) and duration < 10:
+ if (not imported) and duration < 10: # pragma: only failure
duration *= 2
finally:
diff --git a/tests/test_config.py b/tests/test_config.py
index 0b4d40b6..bbfa4677 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -103,6 +103,21 @@ class ConfigTest(CoverageTest):
cov = coverage.Coverage()
self.assertEqual(cov.config.debug, ["dataio", "pids", "callers", "fooey"])
+ def test_rcfile_from_environment(self):
+ self.make_file("here.ini", """\
+ [run]
+ data_file = overthere.dat
+ """)
+ self.set_environ("COVERAGE_RCFILE", "here.ini")
+ cov = coverage.Coverage()
+ self.assertEqual(cov.config.data_file, "overthere.dat")
+
+ def test_missing_rcfile_from_environment(self):
+ self.set_environ("COVERAGE_RCFILE", "nowhere.ini")
+ msg = "Couldn't read 'nowhere.ini' as a config file"
+ with self.assertRaisesRegex(CoverageException, msg):
+ coverage.Coverage()
+
def test_parse_errors(self):
# Im-parsable values raise CoverageException, with details.
bad_configs_and_msgs = [
diff --git a/tests/test_coverage.py b/tests/test_coverage.py
index 45abb2be..c8ac55df 100644
--- a/tests/test_coverage.py
+++ b/tests/test_coverage.py
@@ -582,12 +582,7 @@ class SimpleStatementTest(CoverageTest):
""",
[2, 3]
)
- if env.PYVERSION < (3, 7):
- # Before 3.7, module docstrings were included in the lnotab table,
- # unless they were the first line in the file?
- lines = [2, 3, 4]
- else:
- lines = [3, 4]
+ lines = [2, 3, 4]
self.check_coverage("""\
# Start with a comment, because it changes the behavior(!?)
'''I am a module docstring.'''
@@ -1147,11 +1142,7 @@ class CompoundStatementTest(CoverageTest):
[1,10,12,13], "")
def test_class_def(self):
- if env.PYVERSION < (3, 7):
- arcz="-22 2D DE E-2 23 36 6A A-2 -68 8-6 -AB B-A"
- else:
- # Python 3.7 no longer includes class docstrings in the lnotab table.
- arcz="-22 2D DE E-2 26 6A A-2 -68 8-6 -AB B-A"
+ arcz="-22 2D DE E-2 23 36 6A A-2 -68 8-6 -AB B-A"
self.check_coverage("""\
# A comment.
class theClass:
diff --git a/tests/test_debug.py b/tests/test_debug.py
index 38f31f58..c81ca24d 100644
--- a/tests/test_debug.py
+++ b/tests/test_debug.py
@@ -145,7 +145,7 @@ class DebugTraceTest(CoverageTest):
out_lines = self.f1_debug_output(["config"])
labels = """
- attempted_config_files branch config_files cover_pylib data_file
+ attempted_config_files branch config_files_read config_file cover_pylib data_file
debug exclude_list extra_css html_dir html_title ignore_errors
run_include run_omit parallel partial_always_list partial_list paths
precision show_missing source timid xml_output
@@ -162,7 +162,7 @@ class DebugTraceTest(CoverageTest):
out_lines = self.f1_debug_output(["sys"])
labels = """
- version coverage cover_paths pylib_paths tracer config_files
+ version coverage cover_paths pylib_paths tracer configs_attempted config_file
configs_read data_path python platform implementation executable
cwd path environment command_line cover_match pylib_match
""".split()
diff --git a/tests/test_farm.py b/tests/test_farm.py
index 1b52bc29..4fc0ea5a 100644
--- a/tests/test_farm.py
+++ b/tests/test_farm.py
@@ -36,7 +36,7 @@ def test_farm(filename):
# "rU" was deprecated in 3.4
-READ_MODE = "rU" if sys.version_info < (3, 4) else "r"
+READ_MODE = "rU" if env.PYVERSION < (3, 4) else "r"
class FarmTestCase(ModuleAwareMixin, SysPathAwareMixin, unittest.TestCase):
@@ -103,7 +103,7 @@ class FarmTestCase(ModuleAwareMixin, SysPathAwareMixin, unittest.TestCase):
"""Here to make unittest.TestCase happy, but will never be invoked."""
raise Exception("runTest isn't used in this class!")
- def __call__(self):
+ def __call__(self): # pylint: disable=arguments-differ
"""Execute the test from the run.py file."""
if _TEST_NAME_FILE: # pragma: debugging
with open(_TEST_NAME_FILE, "w") as f:
diff --git a/tests/test_oddball.py b/tests/test_oddball.py
index aa2f333c..5bd204d9 100644
--- a/tests/test_oddball.py
+++ b/tests/test_oddball.py
@@ -118,7 +118,7 @@ class RecursionTest(CoverageTest):
cov = coverage.Coverage()
self.start_import_stop(cov, "recur")
- pytrace = (cov.collector.tracer_name() == "PyTracer")
+ pytrace = (cov._collector.tracer_name() == "PyTracer")
expected_missing = [3]
if pytrace: # pragma: no metacov
expected_missing += [9, 10, 11]
@@ -398,15 +398,6 @@ class ExceptionTest(CoverageTest):
class DoctestTest(CoverageTest):
"""Tests invoked with doctest should measure properly."""
- def setUp(self):
- super(DoctestTest, self).setUp()
-
- # This test case exists because Python 2.4's doctest module didn't play
- # well with coverage. Nose fixes the problem by monkeypatching doctest.
- # I want to be sure there's no monkeypatch and that I'm getting the
- # doctest module that users of coverage will get.
- assert 'doctest' not in sys.modules
-
def test_doctest(self):
self.check_coverage('''\
def return_arg_or_void(arg):
diff --git a/tests/test_parser.py b/tests/test_parser.py
index afb87716..169319f5 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -165,11 +165,7 @@ class PythonParserTest(CoverageTest):
def func(x=25):
return 26
""")
- if env.PYVERSION < (3, 7):
- raw_statements = set([3, 4, 5, 6, 8, 9, 10, 13, 15, 16, 17, 20, 22, 23, 25, 26])
- else:
- # Python 3.7 no longer includes class docstrings in the lnotab table.
- raw_statements = set([3, 4, 5, 6, 8, 10, 13, 15, 16, 17, 20, 22, 23, 25, 26])
+ raw_statements = set([3, 4, 5, 6, 8, 9, 10, 13, 15, 16, 17, 20, 22, 23, 25, 26])
self.assertEqual(parser.raw_statements, raw_statements)
self.assertEqual(parser.statements, set([8]))
@@ -201,8 +197,14 @@ class PythonParserTest(CoverageTest):
pass
""")
self.assertEqual(parser.statements, set([1, 2, 4, 8, 10]))
- self.assertEqual(parser.arcs(), set(self.arcz_to_arcs(".1 14 48 8. .2 2. -8A A-8")))
- self.assertEqual(parser.exit_counts(), {1: 1, 2: 1, 4: 1, 8: 1, 10: 1})
+ expected_arcs = set(self.arcz_to_arcs(".1 14 48 8. .2 2. -8A A-8"))
+ expected_exits = {1: 1, 2: 1, 4: 1, 8: 1, 10: 1}
+ if env.PYVERSION >= (3, 7, 0, 'beta', 5):
+ # 3.7 changed how functions with only docstrings are numbered.
+ expected_arcs.update(set(self.arcz_to_arcs("-46 6-4")))
+ expected_exits.update({6: 1})
+ self.assertEqual(parser.arcs(), expected_arcs)
+ self.assertEqual(parser.exit_counts(), expected_exits)
class ParserMissingArcDescriptionTest(CoverageTest):
@@ -260,10 +262,6 @@ class ParserMissingArcDescriptionTest(CoverageTest):
)
def test_missing_arc_descriptions_for_small_callables(self):
- # We use 2.7 features here, so just skip this test on 2.6
- if env.PYVERSION < (2, 7):
- self.skipTest("No dict or set comps in 2.6")
-
parser = self.parse_text(u"""\
callables = [
lambda: 2,
diff --git a/tests/test_process.py b/tests/test_process.py
index 18564cb8..68262a57 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -585,8 +585,32 @@ class ProcessTest(CoverageTest):
self.assertIn("Trace function changed", out)
+ def test_warn_preimported(self):
+ self.make_file("hello.py", """\
+ import goodbye
+ import coverage
+ cov = coverage.Coverage(include=["good*"], check_preimported=True)
+ cov.start()
+ print(goodbye.f())
+ cov.stop()
+ """)
+ self.make_file("goodbye.py", """\
+ def f():
+ return "Goodbye!"
+ """)
+ goodbye_path = os.path.abspath("goodbye.py")
+
+ out = self.run_command("python hello.py")
+ self.assertIn("Goodbye!", out)
+
+ msg = (
+ "Coverage.py warning: "
+ "Already imported a file that will be measured: {0} "
+ "(already-imported)").format(goodbye_path)
+ self.assertIn(msg, out)
+
def test_note(self):
- if env.PYPY and env.PY3 and env.PYPYVERSION[:3] == (5, 10, 0):
+ if env.PYPY and env.PY3 and env.PYPYVERSION[:3] == (5, 10, 0): # pragma: obscure
# https://bitbucket.org/pypy/pypy/issues/2729/pypy3-510-incorrectly-decodes-astral-plane
self.skipTest("Avoid incorrect decoding astral plane JSON chars")
self.make_file(".coveragerc", """\
@@ -635,9 +659,6 @@ class ProcessTest(CoverageTest):
self.assertGreater(data.line_counts()['os.py'], 50)
def test_lang_c(self):
- if env.PY3 and sys.version_info < (3, 4):
- # Python 3.3 can't compile the non-ascii characters in the file name.
- self.skipTest("3.3 can't handle this test")
if env.JYTHON:
# Jython as of 2.7.1rc3 won't compile a filename that isn't utf8.
self.skipTest("Jython can't handle this test")
@@ -666,6 +687,11 @@ class ProcessTest(CoverageTest):
import coverage
print("No warnings!")
""")
+
+ # Some of our testing infrastructure can issue warnings.
+ # Turn it all off for the sub-process.
+ self.del_environ("COVERAGE_TESTING")
+
out = self.run_command("python allok.py")
self.assertEqual(out, "No warnings!\n")
@@ -676,9 +702,11 @@ class ProcessTest(CoverageTest):
pass
""")
self.make_file("run_twice.py", """\
+ import sys
import coverage
- for _ in [1, 2]:
+ for i in [1, 2]:
+ sys.stderr.write("Run %s\\n" % i)
inst = coverage.Coverage(source=['foo'])
inst.load()
inst.start()
@@ -689,15 +717,13 @@ class ProcessTest(CoverageTest):
out = self.run_command("python run_twice.py")
self.assertEqual(
out,
+ "Run 1\n"
+ "Run 2\n"
"Coverage.py warning: Module foo was previously imported, but not measured "
"(module-not-measured)\n"
)
def test_module_name(self):
- if sys.version_info < (2, 7):
- # Python 2.6 thinks that coverage is a package that can't be
- # executed
- self.skipTest("-m doesn't work the same < Python 2.7")
# https://bitbucket.org/ned/coveragepy/issues/478/help-shows-silly-program-name-when-running
out = self.run_command("python -m coverage")
self.assertIn("Use 'coverage help' for help", out)
@@ -739,7 +765,7 @@ class EnvironmentTest(CoverageTest):
self.assert_tryexecfile_output(out_cov, out_py)
def test_coverage_run_dir_is_like_python_dir(self):
- if sys.version_info == (3, 5, 4, 'final', 0):
+ if env.PYVERSION == (3, 5, 4, 'final', 0): # pragma: obscure
self.skipTest("3.5.4 broke this: https://bugs.python.org/issue32551")
with open(TRY_EXECFILE) as f:
self.make_file("with_main/__main__.py", f.read())
@@ -819,10 +845,6 @@ class EnvironmentTest(CoverageTest):
self.assert_tryexecfile_output(out_cov, out_py)
def test_coverage_run_dashm_is_like_python_dashm_with__main__207(self):
- if sys.version_info < (2, 7):
- # Coverage.py isn't bug-for-bug compatible in the behavior
- # of -m for Pythons < 2.7
- self.skipTest("-m doesn't work the same < Python 2.7")
# https://bitbucket.org/ned/coveragepy/issue/207
self.make_file("package/__init__.py", "print('init')")
self.make_file("package/__main__.py", "print('main')")
diff --git a/tox-new.ini b/tox-new.ini
deleted file mode 100644
index bc5f041a..00000000
--- a/tox-new.ini
+++ /dev/null
@@ -1,53 +0,0 @@
-# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
-# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
-
-# An experiment in using tox to install the sdist, and do the pytracer/ctracer
-# split. Doesn't yet work because the working tree is in the import path, so
-# "import coverage" finds the working tree instead of the sdist-installed code.
-# This can be fixed one of two ways:
-#
-# 1. By changing to a "src" layout, so that "import coverage" won't work in the
-# working tree, or
-#
-# 2. By removing the "__init__.py" from the tests directory, so that nose won't
-# add the working tree to the path. This will also mean changing a number of
-# import statements in the tests directory.
-
-[tox]
-envlist = py{26,27,33,34,35}-{c,py}tracer, pypy{24,26,3_24}-pytracer
-skip_missing_interpreters = True
-
-[testenv]
-commands =
- # Create tests/zipmods.zip, install the egg1 egg
- python igor.py zip_mods install_egg
-
- # Remove the C extension so that we can test the PyTracer
- pytracer: python igor.py remove_extension
- pytracer: python igor.py test_with_tracer py {posargs}
-
- ctracer: python igor.py test_with_tracer c {posargs}
-
-deps =
- # https://requires.io/github/nedbat/coveragepy/requirements/
- nose==1.3.7
- mock==1.3.0
- PyContracts==1.7.6
- py26: unittest2==1.1.0
- py{26,27}: gevent==1.0.2
- py{26,27}: eventlet==0.17.4
- py{26,27,33,34,35}: greenlet==0.4.9
-
-passenv = COVERAGE_*
-
-[testenv:pypy]
-basepython = pypy
-
-[testenv:pypy24]
-basepython = pypy2.4
-
-[testenv:pypy26]
-basepython = pypy2.6
-
-[testenv:pypy3_24]
-basepython = pypy3-2.4
diff --git a/tox.ini b/tox.ini
index c5194a77..8c35391f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,7 +2,7 @@
# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
[tox]
-envlist = py{26,27,33,34,35,36,37}, pypy{2,3}, jython, doc, lint
+envlist = py{27,34,35,36,37}, pypy{2,3}, jython, doc, lint
skip_missing_interpreters = {env:COVERAGE_SKIP_MISSING_INTERPRETERS:True}
toxworkdir = {env:TOXWORKDIR:.tox}
@@ -12,18 +12,17 @@ usedevelop = True
deps =
# https://requires.io/github/nedbat/coveragepy/requirements/
-rrequirements/pytest.pip
- pip==9.0.1
+ pip==10.0.1
# setuptools>=36 vendors packages which pollute the coverage output in tests
setuptools==35.0.2
mock==2.0.0
- PyContracts==1.8.0
+ PyContracts==1.8.3
unittest-mixins==1.4
#-e/Users/ned/unittest_mixins
- py26: unittest2==1.1.0
- py{27,33,34,35,36}: gevent==1.2.2
- py26: eventlet==0.21.0
- py{27,33,34,35,36,37}: eventlet==0.22.0
- py{26,27,33,34,35,36,37}: greenlet==0.4.13
+ # gevent 1.3 causes a failure: https://bitbucket.org/ned/coveragepy/issues/663/gevent-132-on-windows-fails
+ py{27,34,35,36}: gevent==1.2.2
+ py{27,34,35,36,37}: eventlet==0.23.0
+ py{27,34,35,36,37}: greenlet==0.4.13
# Windows can't update the pip version with pip running, so use Python
# to install things.
@@ -43,13 +42,6 @@ commands =
# Remove the C extension so that we can test the PyTracer
python igor.py zip_mods install_egg remove_extension
- # When running parallel tests, many processes might all try to import the
- # same modules at once. This should be safe, but especially on Python 3.3,
- # this caused a number of test failures trying to import usepkgs. To
- # prevent the race condition, pre-compile the tests/modules directory.
- py33: python -m compileall -q -f tests/modules
- py33: python -c "import time; time.sleep(1.1)"
-
# Test with the PyTracer
python igor.py test_with_tracer py {posargs}
@@ -57,9 +49,6 @@ commands =
python setup.py --quiet build_ext --inplace
python igor.py test_with_tracer c {posargs}
-[testenv:py26]
-install_command = python -m pip.__main__ install -U {opts} {packages}
-
[testenv:pypy]
# The "pypy" environment is for Travis. Probably can make Travis use one of
# the other environments...
@@ -93,6 +82,8 @@ setenv =
LINTABLE = coverage tests igor.py setup.py __main__.py
commands =
- python -m pylint --notes= {env:LINTABLE}
python -m tabnanny {env:LINTABLE}
python igor.py check_eol
+ check-manifest --ignore 'lab*,perf*,doc/sample_html*,.treerc'
+ python setup.py check -r -s
+ python -m pylint --notes= {env:LINTABLE}
diff --git a/tox_wheels.ini b/tox_wheels.ini
index 18715945..adf48bf7 100644
--- a/tox_wheels.ini
+++ b/tox_wheels.ini
@@ -2,14 +2,14 @@
# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
[tox]
-envlist = py{26,27,33,34,35,36,sys}
+envlist = py{27,34,35,36,sys}
toxworkdir = {toxinidir}/.tox_kits
[testenv]
deps =
-rrequirements/wheel.pip
-commands =
+commands =
python -c "import sys; print(sys.real_prefix)"
python setup.py bdist_wheel {posargs}