diff options
62 files changed, 942 insertions, 790 deletions
@@ -26,6 +26,7 @@ setuptools-*.egg .tox* .noseids .cache +.pytest_cache .hypothesis # Stuff in the test directory. @@ -28,6 +28,7 @@ setuptools-*.egg .tox* .noseids .cache +.pytest_cache .hypothesis # Stuff in the test directory. @@ -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 @@ -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) @@ -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/ @@ -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. @@ -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 @@ -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 @@ -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} |