diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2014-10-20 18:37:46 -0400 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2014-10-20 18:37:46 -0400 |
commit | 17c94a9f94916ba892f7ef0518881776d6b55d66 (patch) | |
tree | 51a7eda6cf8d9e61adcb3ca791f9917065125085 /coverage | |
parent | ad4c7f3a5194f6966454d534f02e6b02633fa370 (diff) | |
parent | cd015c45c278aca757263746ed2e64c46d578ddd (diff) | |
download | python-coveragepy-git-17c94a9f94916ba892f7ef0518881776d6b55d66.tar.gz |
Merged pull request #18 manually
Diffstat (limited to 'coverage')
-rw-r--r-- | coverage/__init__.py | 16 | ||||
-rw-r--r-- | coverage/backward.py | 2 | ||||
-rw-r--r-- | coverage/cmdline.py | 122 | ||||
-rw-r--r-- | coverage/codeunit.py | 26 | ||||
-rw-r--r-- | coverage/collector.py | 71 | ||||
-rw-r--r-- | coverage/config.py | 49 | ||||
-rw-r--r-- | coverage/control.py | 167 | ||||
-rw-r--r-- | coverage/misc.py | 2 | ||||
-rw-r--r-- | coverage/parser.py | 4 | ||||
-rw-r--r-- | coverage/plugin.py | 40 | ||||
-rw-r--r-- | coverage/pytracer.py | 24 | ||||
-rw-r--r-- | coverage/results.py | 3 | ||||
-rw-r--r-- | coverage/test_helpers.py | 40 | ||||
-rw-r--r-- | coverage/tracer.c | 32 | ||||
-rw-r--r-- | coverage/version.py | 2 |
15 files changed, 322 insertions, 278 deletions
diff --git a/coverage/__init__.py b/coverage/__init__.py index 5ae32aba..67dd6e88 100644 --- a/coverage/__init__.py +++ b/coverage/__init__.py @@ -17,9 +17,9 @@ from coverage.plugin import CoveragePlugin coverage = Coverage # Module-level functions. The original API to this module was based on -# functions defined directly in the module, with a singleton of the coverage() +# functions defined directly in the module, with a singleton of the Coverage() # class. That design hampered programmability, so the current api uses -# explicitly-created coverage objects. But for backward compatibility, here we +# explicitly-created Coverage objects. But for backward compatibility, here we # define the top-level functions to create the singleton when they are first # called. @@ -28,7 +28,7 @@ coverage = Coverage _the_coverage = None def _singleton_method(name): - """Return a function to the `name` method on a singleton `coverage` object. + """Return a function to the `name` method on a singleton `Coverage` object. The singleton object is created the first time one of these functions is called. @@ -42,19 +42,19 @@ def _singleton_method(name): """Singleton wrapper around a coverage method.""" global _the_coverage if not _the_coverage: - _the_coverage = coverage(auto_data=True) + _the_coverage = Coverage(auto_data=True) return getattr(_the_coverage, name)(*args, **kwargs) import inspect - meth = getattr(coverage, name) + meth = getattr(Coverage, name) args, varargs, kw, defaults = inspect.getargspec(meth) argspec = inspect.formatargspec(args[1:], varargs, kw, defaults) docstring = meth.__doc__ wrapper.__doc__ = ("""\ - A first-use-singleton wrapper around coverage.%(name)s. + A first-use-singleton wrapper around Coverage.%(name)s. This wrapper is provided for backward compatibility with legacy code. - New code should use coverage.%(name)s directly. + New code should use Coverage.%(name)s directly. %(name)s%(argspec)s: @@ -96,7 +96,7 @@ except KeyError: # COPYRIGHT AND LICENSE # # Copyright 2001 Gareth Rees. All rights reserved. -# Copyright 2004-2013 Ned Batchelder. All rights reserved. +# Copyright 2004-2014 Ned Batchelder. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are diff --git a/coverage/backward.py b/coverage/backward.py index 9597449c..e839f6bd 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -148,7 +148,7 @@ def import_local_file(modname): if SourceFileLoader: mod = SourceFileLoader(modname, modfile).load_module() else: - for suff in imp.get_suffixes(): + for suff in imp.get_suffixes(): # pragma: part covered if suff[0] == '.py': break diff --git a/coverage/cmdline.py b/coverage/cmdline.py index bd10d5a8..3d1f5f6a 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -19,9 +19,12 @@ class Opts(object): '', '--branch', action='store_true', help="Measure branch coverage in addition to statement coverage." ) - coroutine = optparse.make_option( - '', '--coroutine', action='store', metavar="LIB", - help="Properly measure code using coroutines." + CONCURRENCY_CHOICES = ["thread", "gevent", "greenlet", "eventlet"] + concurrency = optparse.make_option( + '', '--concurrency', action='store', metavar="LIB", + choices=CONCURRENCY_CHOICES, + help="Properly measure code using a concurrency library. " + "Valid values are: %s." % ", ".join(CONCURRENCY_CHOICES) ) debug = optparse.make_option( '', '--debug', action='store', metavar="OPTS", @@ -46,8 +49,8 @@ class Opts(object): include = optparse.make_option( '', '--include', action='store', metavar="PAT1,PAT2,...", - help="Include files only when their filename path matches one of " - "these patterns. Usually needs quoting on the command line." + help="Include only files whose paths match one of these patterns." + "Accepts shell-style wildcards, which must be quoted." ) pylib = optparse.make_option( '-L', '--pylib', action='store_true', @@ -59,17 +62,11 @@ class Opts(object): help="Show line numbers of statements in each module that weren't " "executed." ) - old_omit = optparse.make_option( - '-o', '--omit', action='store', - metavar="PAT1,PAT2,...", - help="Omit files when their filename matches one of these patterns. " - "Usually needs quoting on the command line." - ) omit = optparse.make_option( '', '--omit', action='store', metavar="PAT1,PAT2,...", - help="Omit files when their filename matches one of these patterns. " - "Usually needs quoting on the command line." + help="Omit files whose paths match one of these patterns. " + "Accepts shell-style wildcards, which must be quoted." ) output_xml = optparse.make_option( '-o', '', action='store', dest="outfile", @@ -125,7 +122,7 @@ class CoverageOptionParser(optparse.OptionParser, object): self.set_defaults( actions=[], branch=None, - coroutine=None, + concurrency=None, debug=None, directory=None, fail_under=None, @@ -175,42 +172,17 @@ class CoverageOptionParser(optparse.OptionParser, object): raise self.OptionParserError -class ClassicOptionParser(CoverageOptionParser): - """Command-line parser for coverage.py classic arguments.""" +class GlobalOptionParser(CoverageOptionParser): + """Command-line parser for coverage.py global option arguments.""" def __init__(self): - super(ClassicOptionParser, self).__init__() - - self.add_action('-a', '--annotate', 'annotate') - self.add_action('-b', '--html', 'html') - self.add_action('-c', '--combine', 'combine') - self.add_action('-e', '--erase', 'erase') - self.add_action('-r', '--report', 'report') - self.add_action('-x', '--execute', 'execute') + super(GlobalOptionParser, self).__init__() self.add_options([ - Opts.directory, Opts.help, - Opts.ignore_errors, - Opts.pylib, - Opts.show_missing, - Opts.old_omit, - Opts.parallel_mode, - Opts.timid, Opts.version, ]) - def add_action(self, dash, dashdash, action_code): - """Add a specialized option that is the action to execute.""" - option = self.add_option(dash, dashdash, action='callback', - callback=self._append_action - ) - option.action_code = action_code - - def _append_action(self, option, opt_unused, value_unused, parser): - """Callback for an option that adds to the `actions` list.""" - parser.values.actions.append(option.action_code) - class CmdOptionParser(CoverageOptionParser): """Parse one of the new-style commands for coverage.py.""" @@ -320,7 +292,7 @@ CMDS = { [ Opts.append, Opts.branch, - Opts.coroutine, + Opts.concurrency, Opts.debug, Opts.pylib, Opts.parallel_mode, @@ -370,7 +342,7 @@ class CoverageScript(object): self.run_python_file = _run_python_file or run_python_file self.run_python_module = _run_python_module or run_python_module self.help_fn = _help_fn or self.help - self.classic = False + self.global_option = False self.coverage = None @@ -387,11 +359,11 @@ class CoverageScript(object): self.help_fn(topic='minimum_help') return OK - # The command syntax we parse depends on the first argument. Classic - # syntax always starts with an option. - self.classic = argv[0].startswith('-') - if self.classic: - parser = ClassicOptionParser() + # The command syntax we parse depends on the first argument. Global + # switch syntax always starts with an option. + self.global_option = argv[0].startswith('-') + if self.global_option: + parser = GlobalOptionParser() else: parser = CMDS.get(argv[0]) if not parser: @@ -429,7 +401,7 @@ class CoverageScript(object): omit = omit, include = include, debug = debug, - coroutine = options.coroutine, + concurrency = options.concurrency, ) if 'debug' in options.actions: @@ -508,7 +480,7 @@ class CoverageScript(object): """ # Handle help. if options.help: - if self.classic: + if self.global_option: self.help_fn(topic='help') else: self.help_fn(parser=parser) @@ -656,53 +628,6 @@ def unglob_args(args): HELP_TOPICS = { # ------------------------- -'classic': -r"""Coverage.py version %(__version__)s -Measure, collect, and report on code coverage in Python programs. - -Usage: - -coverage -x [-p] [-L] [--timid] MODULE.py [ARG1 ARG2 ...] - Execute the module, passing the given command-line arguments, collecting - coverage data. With the -p option, include the machine name and process - id in the .coverage file name. With -L, measure coverage even inside the - Python installed library, which isn't done by default. With --timid, use a - simpler but slower trace method. - -coverage -e - Erase collected coverage data. - -coverage -c - Combine data from multiple coverage files (as created by -p option above) - and store it into a single file representing the union of the coverage. - -coverage -r [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...] - Report on the statement coverage for the given files. With the -m - option, show line numbers of the statements that weren't executed. - -coverage -b -d DIR [-i] [-o DIR,...] [FILE1 FILE2 ...] - Create an HTML report of the coverage of the given files. Each file gets - its own page, with the file listing decorated to show executed, excluded, - and missed lines. - -coverage -a [-d DIR] [-i] [-o DIR,...] [FILE1 FILE2 ...] - Make annotated copies of the given files, marking statements that - are executed with > and statements that are missed with !. - --d DIR - Write output files for -b or -a to this directory. - --i Ignore errors while reporting or annotating. - --o DIR,... - Omit reporting or annotating files when their filename path starts with - a directory listed in the omit list. - e.g. coverage -i -r -o c:\python25,lib\enthought\traits - -Coverage data is saved in the file .coverage by default. Set the -COVERAGE_FILE environment variable to save it somewhere else. -""", -# ------------------------- 'help': """\ Coverage.py, version %(__version__)s Measure, collect, and report on code coverage in Python programs. @@ -720,7 +645,6 @@ Commands: xml Create an XML report of coverage results. Use "coverage help <command>" for detailed help on any command. -Use "coverage help classic" for help on older command syntax. For more information, see %(__url__)s """, # ------------------------- diff --git a/coverage/codeunit.py b/coverage/codeunit.py index c9ab2622..da617913 100644 --- a/coverage/codeunit.py +++ b/coverage/codeunit.py @@ -32,24 +32,16 @@ def code_unit_factory(morfs, file_locator, get_plugin=None): if isinstance(morf, string_class) and get_plugin: plugin = get_plugin(morf) if plugin: - klass = plugin.code_unit_class(morf) - #klass = DjangoTracer # NOT REALLY! TODO - # Hacked-in Mako support. Define COVERAGE_MAKO_PATH as a fragment of - # the path that indicates the Python file is actually a compiled Mako - # template. THIS IS TEMPORARY! - #MAKO_PATH = os.environ.get('COVERAGE_MAKO_PATH') - #if MAKO_PATH and isinstance(morf, string_class) and MAKO_PATH in morf: - # # Super hack! Do mako both ways! - # if 0: - # cu = PythonCodeUnit(morf, file_locator) - # cu.name += '_fako' - # code_units.append(cu) - # klass = MakoCodeUnit - #elif isinstance(morf, string_class) and morf.endswith(".html"): - # klass = DjangoCodeUnit + file_reporter = plugin.file_reporter(morf) + if file_reporter is None: + raise CoverageException( + "Plugin %r did not provide a file reporter for %r." % ( + plugin.plugin_name, morf + ) + ) else: - klass = PythonCodeUnit - code_units.append(klass(morf, file_locator)) + file_reporter = PythonCodeUnit(morf, file_locator) + code_units.append(file_reporter) return code_units diff --git a/coverage/collector.py b/coverage/collector.py index 85c8dc90..001bc3d3 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -23,6 +23,11 @@ except ImportError: sys.exit(1) CTracer = None +try: + import __pypy__ # pylint: disable=import-error +except ImportError: + __pypy__ = None + class Collector(object): """Collects trace data. @@ -46,7 +51,7 @@ class Collector(object): _collectors = [] def __init__(self, - should_trace, check_include, timid, branch, warn, coroutine, + should_trace, check_include, timid, branch, warn, concurrency, ): """Create a collector. @@ -68,7 +73,9 @@ class Collector(object): `warn` is a warning function, taking a single string message argument, to be used if a warning needs to be issued. - TODO: `coroutine` + `concurrency` is a string indicating the concurrency library in use. + Valid values are "greenlet", "eventlet", "gevent", or "thread" (the + default). """ self.should_trace = should_trace @@ -76,21 +83,21 @@ class Collector(object): self.warn = warn self.branch = branch self.threading = None - self.coroutine = coroutine + self.concurrency = concurrency - self.coroutine_id_func = None + self.concur_id_func = None try: - if coroutine == "greenlet": - import greenlet - self.coroutine_id_func = greenlet.getcurrent - elif coroutine == "eventlet": - import eventlet.greenthread - self.coroutine_id_func = eventlet.greenthread.getcurrent - elif coroutine == "gevent": - import gevent - self.coroutine_id_func = gevent.getcurrent - elif coroutine == "thread" or not coroutine: + if concurrency == "greenlet": + import greenlet # pylint: disable=import-error + self.concur_id_func = greenlet.getcurrent + elif concurrency == "eventlet": + import eventlet.greenthread # pylint: disable=import-error + self.concur_id_func = eventlet.greenthread.getcurrent + elif concurrency == "gevent": + import gevent # pylint: disable=import-error + self.concur_id_func = gevent.getcurrent + elif concurrency == "thread" or not concurrency: # It's important to import threading only if we need it. If # it's imported early, and the program being measured uses # gevent, then gevent's monkey-patching won't work properly. @@ -98,12 +105,12 @@ class Collector(object): self.threading = threading else: raise CoverageException( - "Don't understand coroutine=%s" % coroutine + "Don't understand concurrency=%s" % concurrency ) except ImportError: raise CoverageException( - "Couldn't trace with coroutine=%s, " - "the module isn't installed." % coroutine + "Couldn't trace with concurrency=%s, " + "the module isn't installed." % concurrency ) self.reset() @@ -134,7 +141,24 @@ class Collector(object): # A cache of the results from should_trace, the decision about whether # to trace execution in a file. A dict of filename to (filename or # None). - self.should_trace_cache = {} + if __pypy__ is not None: + # Alex Gaynor said: + # should_trace_cache is a strictly growing key: once a key is in + # it, it never changes. Further, the keys used to access it are + # generally constant, given sufficient context. That is to say, at + # any given point _trace() is called, pypy is able to know the key. + # This is because the key is determined by the physical source code + # line, and that's invariant with the call site. + # + # This property of a dict with immutable keys, combined with + # call-site-constant keys is a match for PyPy's module dict, + # which is optimized for such workloads. + # + # This gives a 20% benefit on the workload described at + # https://bitbucket.org/pypy/pypy/issue/1871/10x-slower-than-cpython-under-coverage + self.should_trace_cache = __pypy__.newdict("module") + else: + self.should_trace_cache = {} # Our active Tracers. self.tracers = [] @@ -148,13 +172,13 @@ class Collector(object): tracer.should_trace_cache = self.should_trace_cache tracer.warn = self.warn - if hasattr(tracer, 'coroutine_id_func'): - tracer.coroutine_id_func = self.coroutine_id_func - elif self.coroutine_id_func: + if hasattr(tracer, 'concur_id_func'): + tracer.concur_id_func = self.concur_id_func + elif self.concur_id_func: raise CoverageException( - "Can't support coroutine=%s with %s, " + "Can't support concurrency=%s with %s, " "only threads are supported" % ( - self.coroutine, self.tracer_name(), + self.concurrency, self.tracer_name(), ) ) @@ -204,6 +228,7 @@ class Collector(object): # Install the tracer on this thread. fn = self._start_tracer() + # Replay all the events from fullcoverage into the new trace function. for args in traces0: (frame, event, arg), lineno = args try: diff --git a/coverage/config.py b/coverage/config.py index c671ef75..4d599ee7 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -2,6 +2,8 @@ import os, re, sys from coverage.backward import string_class, iitems +from coverage.misc import CoverageException + # In py3, # ConfigParser was renamed to the more-standard configparser try: @@ -140,7 +142,7 @@ class CoverageConfig(object): # Defaults for [run] self.branch = False - self.coroutine = None + self.concurrency = None self.cover_pylib = False self.data_file = ".coverage" self.parallel = False @@ -173,15 +175,6 @@ class CoverageConfig(object): # Options for plugins self.plugin_options = {} - def from_environment(self, env_var): - """Read configuration from the `env_var` environment variable.""" - # Timidity: for nose users, read an environment variable. This is a - # cheap hack, since the rest of the command line arguments aren't - # recognized, but it solves some users' problems. - env = os.environ.get(env_var, '') - if env: - self.timid = ('--timid' in env) - MUST_BE_LIST = ["omit", "include", "debug", "plugins"] def from_args(self, **kwargs): @@ -235,7 +228,7 @@ class CoverageConfig(object): # [run] ('branch', 'run:branch', 'boolean'), - ('coroutine', 'run:coroutine'), + ('concurrency', 'run:concurrency'), ('cover_pylib', 'run:cover_pylib', 'boolean'), ('data_file', 'run:data_file'), ('debug', 'run:debug', 'list'), @@ -275,3 +268,37 @@ class CoverageConfig(object): def get_plugin_options(self, plugin): """Get a dictionary of options for the plugin named `plugin`.""" return self.plugin_options.get(plugin, {}) + + # TODO: docs for this. + def __setitem__(self, option_name, value): + # Check all the hard-coded options. + for option_spec in self.CONFIG_FILE_OPTIONS: + attr, where = option_spec[:2] + if where == option_name: + setattr(self, attr, value) + return + + # See if it's a plugin option. + plugin_name, _, key = option_name.partition(":") + if key and plugin_name in self.plugins: + self.plugin_options.setdefault(plugin_name, {})[key] = value + return + + # If we get here, we didn't find the option. + raise CoverageException("No such option: %r" % option_name) + + # TODO: docs for this. + def __getitem__(self, option_name): + # Check all the hard-coded options. + for option_spec in self.CONFIG_FILE_OPTIONS: + attr, where = option_spec[:2] + if where == option_name: + return getattr(self, attr) + + # See if it's a plugin option. + plugin_name, _, key = option_name.partition(":") + if key and plugin_name in self.plugins: + return self.plugin_options.get(plugin_name, {}).get(key) + + # If we get here, we didn't find the option. + raise CoverageException("No such option: %r" % option_name) diff --git a/coverage/control.py b/coverage/control.py index 86a2ae23..66979c33 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -33,7 +33,7 @@ class Coverage(object): To use:: - from coverage import coverage + from coverage import Coverage cov = Coverage() cov.start() @@ -45,7 +45,7 @@ class Coverage(object): 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, - debug_file=None, coroutine=None, plugins=None): + concurrency=None): """ `data_file` is the base name of the data file to use, defaulting to ".coverage". `data_suffix` is appended (with a dot) to `data_file` to @@ -81,22 +81,14 @@ class Coverage(object): will also accept a single string argument. `debug` is a list of strings indicating what debugging information is - desired. `debug_file` is the file to write debug messages to, - defaulting to stderr. + desired. - `coroutine` is a string indicating the coroutining library being used + `concurrency` is a string indicating the concurrency library being used in the measured code. Without this, coverage.py will get incorrect - results. Valid strings are "greenlet", "eventlet", or "gevent", which - are all equivalent. TODO: really? - - `plugins` TODO. + results. Valid strings are "greenlet", "eventlet", "gevent", or + "thread" (the default). """ - from coverage import __version__ - - # A record of all the warnings that have been issued. - self._warnings = [] - # Build our configuration from a number of sources: # 1: defaults: self.config = CoverageConfig() @@ -118,7 +110,6 @@ class Coverage(object): self.config.from_file("setup.cfg", section_prefix="coverage:") # 3: from environment variables: - self.config.from_environment('COVERAGE_OPTIONS') env_data_file = os.environ.get('COVERAGE_FILE') if env_data_file: self.config.data_file = env_data_file @@ -128,22 +119,69 @@ class Coverage(object): data_file=data_file, cover_pylib=cover_pylib, timid=timid, branch=branch, parallel=bool_or_none(data_suffix), source=source, omit=omit, include=include, debug=debug, - coroutine=coroutine, plugins=plugins, + concurrency=concurrency, ) + self._debug_file = None + self._auto_data = auto_data + self._data_suffix = data_suffix + + # The matchers for _should_trace. + self.source_match = None + self.pylib_match = self.cover_match = None + self.include_match = self.omit_match = None + + # Is it ok for no data to be collected? + self._warn_no_data = True + self._warn_unimported_source = True + + # 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 = self.file_locator = None + self.data = self.collector = None + self.plugins = self.file_tracers = None + self.pylib_dirs = self.cover_dir = None + self.data_suffix = self.run_suffix = None + self._exclude_re = None + self.debug = None + + # State machine variables: + # Have we initialized everything? + self._inited = False + # Have we started collecting and not stopped it? + self._started = False + # Have we measured some data and not harvested it? + self._measured = False + + def _init(self): + """Set all the initial state. + + This is called by the public methods to initialize state. This lets us + construct a Coverage object, then tweak its state before this function + is called. + + """ + from coverage import __version__ + + if self._inited: + return + # Create and configure the debugging controller. - self.debug = DebugControl(self.config.debug, debug_file or sys.stderr) + if self._debug_file is None: + self._debug_file = sys.stderr + self.debug = DebugControl(self.config.debug, self._debug_file) # Load plugins self.plugins = Plugins.load_plugins(self.config.plugins, self.config) - self.trace_judges = [] + self.file_tracers = [] for plugin in self.plugins: - if plugin_implements(plugin, "trace_judge"): - self.trace_judges.append(plugin) - self.trace_judges.append(None) # The Python case. - - self.auto_data = auto_data + if plugin_implements(plugin, "file_tracer"): + self.file_tracers.append(plugin) + self.file_tracers.append(None) # The Python case. # _exclude_re is a dict mapping exclusion list names to compiled # regexes. @@ -170,21 +208,21 @@ class Coverage(object): timid=self.config.timid, branch=self.config.branch, warn=self._warn, - coroutine=self.config.coroutine, + concurrency=self.config.concurrency, ) # 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 # find that we are collecting data later. - if data_suffix or self.config.parallel: - if not isinstance(data_suffix, string_class): + if self._data_suffix or self.config.parallel: + if not isinstance(self._data_suffix, string_class): # if data_suffix=True, use .machinename.pid.random - data_suffix = True + self._data_suffix = True else: - data_suffix = None + self._data_suffix = None self.data_suffix = None - self.run_suffix = data_suffix + self.run_suffix = self._data_suffix # Create the data file. We do this at construction time so that the # data file will be written into the directory where the process @@ -206,31 +244,24 @@ class Coverage(object): for m in (atexit, os, platform, random, socket, _structseq): if m is not None and hasattr(m, "__file__"): self.pylib_dirs.add(self._canonical_dir(m)) + if _structseq and not hasattr(_structseq, '__file__'): + # PyPy 2.4 has no __file__ in the builtin modules, but the code + # objects still have the filenames. So dig into one to find + # the path to exclude. + structseq_file = _structseq.structseq_new.func_code.co_filename + self.pylib_dirs.add(self._canonical_dir(structseq_file)) # To avoid tracing the coverage code itself, we skip anything located # where we are. self.cover_dir = self._canonical_dir(__file__) - # The matchers for _should_trace. - self.source_match = None - self.pylib_match = self.cover_match = None - self.include_match = self.omit_match = None - # Set the reporting precision. Numbers.set_precision(self.config.precision) - # Is it ok for no data to be collected? - self._warn_no_data = True - self._warn_unimported_source = True - - # State machine variables: - # Have we started collecting and not stopped it? - self._started = False - # Have we measured some data and not harvested it? - self._measured = False - atexit.register(self._atexit) + self._inited = True + def _canonical_dir(self, morf): """Return the canonical directory of the module or file `morf`.""" morf_filename = PythonCodeUnit(morf, self.file_locator).filename @@ -294,15 +325,27 @@ class Coverage(object): disp.canonical_filename = canonical # Try the plugins, see if they have an opinion about the file. - for plugin in self.trace_judges: + for plugin in self.file_tracers: if plugin: - plugin.trace_judge(disp) + #plugin.trace_judge(disp) + file_tracer = plugin.file_tracer(canonical) + if file_tracer is not None: + file_tracer.plugin_name = plugin.plugin_name + disp.trace = True + disp.file_tracer = file_tracer + disp.source_filename = self.file_locator.canonical_filename(file_tracer.source_filename()) else: disp.trace = True disp.source_filename = canonical + file_tracer = None if disp.trace: - disp.plugin = plugin - + if file_tracer: + disp.file_tracer = file_tracer + if disp.source_filename is None: + raise CoverageException( + "Plugin %r didn't set source_filename for %r" % + (plugin, disp.original_filename) + ) if disp.check_filters: reason = self._check_include_omit_etc(disp.source_filename) if reason: @@ -425,10 +468,12 @@ class Coverage(object): `usecache` is true or false, whether to read and write data on disk. """ + self._init() self.data.usefile(usecache) def load(self): """Load previously-collected coverage data from the data file.""" + self._init() self.collector.reset() self.data.read() @@ -442,11 +487,12 @@ class Coverage(object): process might not shut down cleanly. """ + self._init() 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 - if self.auto_data: + if self._auto_data: self.load() # Create the matchers we need for _should_trace @@ -478,14 +524,15 @@ class Coverage(object): def stop(self): """Stop measuring code coverage.""" + if self._started: + self.collector.stop() self._started = False - self.collector.stop() def _atexit(self): """Clean up on process shutdown.""" if self._started: self.stop() - if self.auto_data: + if self._auto_data: self.save() def erase(self): @@ -495,11 +542,13 @@ class Coverage(object): discarding the data file. """ + self._init() self.collector.reset() self.data.erase() def clear_exclude(self, which='exclude'): """Clear the exclude list.""" + self._init() setattr(self.config, which + "_list", []) self._exclude_regex_stale() @@ -518,6 +567,7 @@ class Coverage(object): is marked for special treatment during reporting. """ + self._init() excl_list = getattr(self.config, which + "_list") excl_list.append(regex) self._exclude_regex_stale() @@ -540,10 +590,12 @@ class Coverage(object): that are available, and their meaning. """ + self._init() return getattr(self.config, which + "_list") def save(self): """Save the collected coverage data to the data file.""" + self._init() data_suffix = self.data_suffix if data_suffix is True: # If data_suffix was a simple true value, then make a suffix with @@ -551,10 +603,9 @@ class Coverage(object): # `save()` at the last minute so that the pid will be correct even # if the process forks. extra = "" - if _TEST_NAME_FILE: - f = open(_TEST_NAME_FILE) - test_name = f.read() - f.close() + if _TEST_NAME_FILE: # pragma: debugging + with open(_TEST_NAME_FILE) as f: + test_name = f.read() extra = "." + test_name data_suffix = "%s%s.%s.%06d" % ( socket.gethostname(), extra, os.getpid(), @@ -572,6 +623,7 @@ class Coverage(object): current measurements. """ + self._init() aliases = None if self.config.paths: aliases = PathAliases(self.file_locator) @@ -587,6 +639,7 @@ class Coverage(object): Also warn about various problems collecting data. """ + self._init() if not self._measured: return @@ -644,6 +697,7 @@ class Coverage(object): coverage data. """ + self._init() analysis = self._analyze(morf) return ( analysis.filename, @@ -794,6 +848,7 @@ class Coverage(object): import coverage as covmod + self._init() try: implementation = platform.python_implementation() except AttributeError: @@ -843,7 +898,7 @@ class FileDisposition(object): self.check_filters = True self.trace = False self.reason = "" - self.plugin = None + self.file_tracer = None def debug_message(self): """Produce a debugging message explaining the outcome.""" diff --git a/coverage/misc.py b/coverage/misc.py index 6962ae32..a653bb62 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -55,7 +55,7 @@ def format_lines(statements, lines): return ret -def short_stack(): +def short_stack(): # pragma: debugging """Return a string summarizing the call stack.""" stack = inspect.stack()[:0:-1] return "\n".join("%30s : %s @%d" % (t[3],t[1],t[2]) for t in stack) diff --git a/coverage/parser.py b/coverage/parser.py index c5e95baa..e7b9c029 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -413,7 +413,7 @@ class ByteParser(object): for _, l in bp._bytes_lines(): yield l - def _block_stack_repr(self, block_stack): + def _block_stack_repr(self, block_stack): # pragma: debugging """Get a string version of `block_stack`, for debugging.""" blocks = ", ".join( "(%s, %r)" % (dis.opname[b[0]], b[1]) for b in block_stack @@ -552,7 +552,7 @@ class ByteParser(object): #self.validate_chunks(chunks) return chunks - def validate_chunks(self, chunks): + def validate_chunks(self, chunks): # pragma: debugging """Validate the rule that chunks have a single entrance.""" # starts is the entrances to the chunks starts = set(ch.byte for ch in chunks) diff --git a/coverage/plugin.py b/coverage/plugin.py index 35be41a9..5d1c5306 100644 --- a/coverage/plugin.py +++ b/coverage/plugin.py @@ -2,28 +2,33 @@ import sys +from coverage.misc import CoverageException + class CoveragePlugin(object): """Base class for coverage.py plugins.""" def __init__(self, options): self.options = options - def trace_judge(self, disposition): - """Decide whether to trace this file with this plugin. + def file_tracer(self, filename): + """Return a FileTracer object for this file.""" + return None + + def file_reporter(self, filename): + """Return the FileReporter class to use for filename. - Set disposition.trace to True if this plugin should trace this file. - May also set other attributes in `disposition`. + This will only be invoked if `filename` returns non-None from + `file_tracer`. It's an error to return None. """ - return None + raise Exception("Plugin %r needs to implement file_reporter" % self.plugin_name) - def source_file_name(self, filename): - """Return the source name for a given Python filename. - Can return None if tracing shouldn't continue. +class FileTracer(object): + """Support needed for files during the tracing phase.""" - """ - return filename + def source_filename(self): + return "xyzzy" def dynamic_source_file_name(self): """Returns a callable that can return a source name for a frame. @@ -38,9 +43,16 @@ class CoveragePlugin(object): """ return None - def code_unit_class(self, morf): - """Return the CodeUnit class to use for a module or filename.""" - return None + def line_number_range(self, frame): + """Given a call frame, return the range of source line numbers.""" + lineno = frame.f_lineno + return lineno, lineno + + +class FileReporter(object): + """Support needed for files during the reporting phase.""" + def __init__(self, filename): + self.filename = filename class Plugins(object): @@ -67,7 +79,7 @@ class Plugins(object): if plugin_class: options = config.get_plugin_options(module) plugin = plugin_class(options) - plugin.__name__ = module + plugin.plugin_name = module plugins.order.append(plugin) plugins.names[module] = plugin diff --git a/coverage/pytracer.py b/coverage/pytracer.py index 7563ae11..84071bb1 100644 --- a/coverage/pytracer.py +++ b/coverage/pytracer.py @@ -33,7 +33,7 @@ class PyTracer(object): # The threading module to use, if any. self.threading = None - self.plugin = [] + self.file_tracer = [] self.cur_file_dict = [] self.last_line = [0] @@ -62,7 +62,7 @@ class PyTracer(object): if self.arcs and self.cur_file_dict: pair = (self.last_line, -self.last_exc_firstlineno) self.cur_file_dict[pair] = None - self.plugin, self.cur_file_dict, self.last_line = ( + self.file_tracer, self.cur_file_dict, self.last_line = ( self.data_stack.pop() ) self.last_exc_back = None @@ -71,7 +71,7 @@ class PyTracer(object): # Entering a new function context. Decide if we should trace # in this file. self.data_stack.append( - (self.plugin, self.cur_file_dict, self.last_line) + (self.file_tracer, self.cur_file_dict, self.last_line) ) filename = frame.f_code.co_filename disp = self.should_trace_cache.get(filename) @@ -79,12 +79,12 @@ class PyTracer(object): disp = self.should_trace(filename, frame) self.should_trace_cache[filename] = disp - self.plugin = None + self.file_tracer = None self.cur_file_dict = None if disp.trace: tracename = disp.source_filename - if disp.plugin: - dyn_func = disp.plugin.dynamic_source_file_name() + if disp.file_tracer: + dyn_func = disp.file_tracer.dynamic_source_file_name() if dyn_func: tracename = dyn_func(tracename, frame) if tracename: @@ -95,17 +95,17 @@ class PyTracer(object): if tracename: if tracename not in self.data: self.data[tracename] = {} - if disp.plugin: - self.plugin_data[tracename] = disp.plugin.__name__ + if disp.file_tracer: + self.plugin_data[tracename] = disp.file_tracer.plugin_name self.cur_file_dict = self.data[tracename] - self.plugin = disp.plugin + self.file_tracer = disp.file_tracer # Set the last_line to -1 because the next arc will be entering a # code block, indicated by (-1, n). self.last_line = -1 elif event == 'line': # Record an executed line. - if self.plugin: - lineno_from, lineno_to = self.plugin.line_number_range(frame) + if self.file_tracer: + lineno_from, lineno_to = self.file_tracer.line_number_range(frame) else: lineno_from, lineno_to = frame.f_lineno, frame.f_lineno if lineno_from != -1: @@ -123,7 +123,7 @@ class PyTracer(object): first = frame.f_code.co_firstlineno self.cur_file_dict[(self.last_line, -first)] = None # Leaving this function, pop the filename stack. - self.plugin, self.cur_file_dict, self.last_line = ( + self.file_tracer, self.cur_file_dict, self.last_line = ( self.data_stack.pop() ) elif event == 'exception': diff --git a/coverage/results.py b/coverage/results.py index 6cbcbfc8..4449de56 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -11,9 +11,8 @@ class Analysis(object): def __init__(self, cov, code_unit): self.coverage = cov - self.code_unit = code_unit - self.filename = self.code_unit.filename + self.filename = code_unit.filename self.parser = code_unit.get_parser( exclude=self.coverage._exclude_regex('exclude') ) diff --git a/coverage/test_helpers.py b/coverage/test_helpers.py index efe68dcd..665593a2 100644 --- a/coverage/test_helpers.py +++ b/coverage/test_helpers.py @@ -132,8 +132,13 @@ class StdStreamCapturingMixin(TestCase): return self.captured_stderr.getvalue() -class TempDirMixin(TestCase): - """A test case mixin that creates a temp directory and files in it.""" +class TempDirMixin(SysPathAwareMixin, ModuleAwareMixin, TestCase): + """A test case mixin that creates a temp directory and files in it. + + Includes SysPathAwareMixin and ModuleAwareMixin, because making and using + temp dirs like this will also need that kind of isolation. + + """ # Our own setting: most of these tests run in their own temp directory. run_in_temp_dir = True @@ -143,12 +148,8 @@ class TempDirMixin(TestCase): if self.run_in_temp_dir: # Create a temporary directory. - noise = str(random.random())[2:] - self.temp_root = os.path.join(tempfile.gettempdir(), 'test_cover') - self.temp_dir = os.path.join(self.temp_root, noise) - os.makedirs(self.temp_dir) - self.old_dir = os.getcwd() - os.chdir(self.temp_dir) + self.temp_dir = self.make_temp_dir("test_cover") + self.chdir(self.temp_dir) # Modules should be importable from this temp directory. We don't # use '' because we make lots of different temp directories and @@ -161,15 +162,24 @@ class TempDirMixin(TestCase): class_behavior.test_method_made_any_files = False class_behavior.temp_dir = self.run_in_temp_dir - self.addCleanup(self.cleanup_temp_dir) + self.addCleanup(self.check_behavior) - def cleanup_temp_dir(self): - """Clean up the temp directories we made.""" + def make_temp_dir(self, slug="test_cover"): + """Make a temp directory that is cleaned up when the test is done.""" + name = "%s_%08d" % (slug, random.randint(0, 99999999)) + temp_dir = os.path.join(tempfile.gettempdir(), name) + os.makedirs(temp_dir) + self.addCleanup(shutil.rmtree, temp_dir) + return temp_dir - if self.run_in_temp_dir: - # Get rid of the temporary directory. - os.chdir(self.old_dir) - shutil.rmtree(self.temp_root) + def chdir(self, new_dir): + """Change directory, and change back when the test is done.""" + old_dir = os.getcwd() + os.chdir(new_dir) + self.addCleanup(os.chdir, old_dir) + + def check_behavior(self): + """Check that we did the right things.""" class_behavior = self.class_behavior() if class_behavior.test_method_made_any_files: diff --git a/coverage/tracer.c b/coverage/tracer.c index 5bf5c462..5a463fcb 100644 --- a/coverage/tracer.c +++ b/coverage/tracer.c @@ -81,7 +81,7 @@ typedef struct { /* Python objects manipulated directly by the Collector class. */ PyObject * should_trace; PyObject * warn; - PyObject * coroutine_id_func; + PyObject * concur_id_func; PyObject * data; PyObject * plugin_data; PyObject * should_trace_cache; @@ -104,8 +104,8 @@ typedef struct { (None). */ - DataStack data_stack; /* Used if we aren't doing coroutines. */ - PyObject * data_stack_index; /* Used if we are doing coroutines. */ + DataStack data_stack; /* Used if we aren't doing concurrency. */ + PyObject * data_stack_index; /* Used if we are doing concurrency. */ DataStack * data_stacks; int data_stacks_alloc; int data_stacks_used; @@ -191,7 +191,7 @@ CTracer_init(CTracer *self, PyObject *args_unused, PyObject *kwds_unused) self->should_trace = NULL; self->warn = NULL; - self->coroutine_id_func = NULL; + self->concur_id_func = NULL; self->data = NULL; self->plugin_data = NULL; self->should_trace_cache = NULL; @@ -234,7 +234,7 @@ CTracer_dealloc(CTracer *self) Py_XDECREF(self->should_trace); Py_XDECREF(self->warn); - Py_XDECREF(self->coroutine_id_func); + Py_XDECREF(self->concur_id_func); Py_XDECREF(self->data); Py_XDECREF(self->plugin_data); Py_XDECREF(self->should_trace_cache); @@ -327,18 +327,18 @@ CTracer_record_pair(CTracer *self, int l1, int l2) static int CTracer_set_pdata_stack(CTracer *self) { - if (self->coroutine_id_func != Py_None) { + if (self->concur_id_func != Py_None) { PyObject * co_obj = NULL; PyObject * stack_index = NULL; long the_index = 0; - co_obj = PyObject_CallObject(self->coroutine_id_func, NULL); + co_obj = PyObject_CallObject(self->concur_id_func, NULL); if (co_obj == NULL) { return RET_ERROR; } stack_index = PyDict_GetItem(self->data_stack_index, co_obj); if (stack_index == NULL) { - /* A new coroutine object. Make a new data stack. */ + /* A new concurrency object. Make a new data stack. */ the_index = self->data_stacks_used; stack_index = MyInt_FromLong(the_index); if (PyDict_SetItem(self->data_stack_index, co_obj, stack_index) < 0) { @@ -505,7 +505,7 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse if (MyText_Check(tracename)) { PyObject * file_data = PyDict_GetItem(self->data, tracename); - PyObject * disp_plugin = NULL; + PyObject * disp_file_tracer = NULL; PyObject * disp_plugin_name = NULL; if (file_data == NULL) { @@ -527,16 +527,16 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse if (self->plugin_data != NULL) { /* If the disposition mentions a plugin, record that. */ - disp_plugin = PyObject_GetAttrString(disposition, "plugin"); - if (disp_plugin == NULL) { + disp_file_tracer = PyObject_GetAttrString(disposition, "file_tracer"); + if (disp_file_tracer == NULL) { STATS( self->stats.errors++; ) Py_DECREF(tracename); Py_DECREF(disposition); return RET_ERROR; } - if (disp_plugin != Py_None) { - disp_plugin_name = PyObject_GetAttrString(disp_plugin, "__name__"); - Py_DECREF(disp_plugin); + if (disp_file_tracer != Py_None) { + disp_plugin_name = PyObject_GetAttrString(disp_file_tracer, "plugin_name"); + Py_DECREF(disp_file_tracer); if (disp_plugin_name == NULL) { STATS( self->stats.errors++; ) Py_DECREF(tracename); @@ -781,8 +781,8 @@ CTracer_members[] = { { "warn", T_OBJECT, offsetof(CTracer, warn), 0, PyDoc_STR("Function for issuing warnings.") }, - { "coroutine_id_func", T_OBJECT, offsetof(CTracer, coroutine_id_func), 0, - PyDoc_STR("Function for determining coroutine context") }, + { "concur_id_func", T_OBJECT, offsetof(CTracer, concur_id_func), 0, + PyDoc_STR("Function for determining concurrency context") }, { "data", T_OBJECT, offsetof(CTracer, data), 0, PyDoc_STR("The raw dictionary of trace data.") }, diff --git a/coverage/version.py b/coverage/version.py index 27c2f6b1..fe3f44fa 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -1,7 +1,7 @@ """The version and URL for coverage.py""" # This file is exec'ed in setup.py, don't import anything! -__version__ = "4.0a0" # see detailed history in CHANGES.txt +__version__ = "4.0a1" # see detailed history in CHANGES.txt __url__ = "http://nedbatchelder.com/code/coverage" if max(__version__).isalpha(): |