From a3cb81edd6053a273447ba3821655320ed234a41 Mon Sep 17 00:00:00 2001 From: buck Date: Wed, 15 Oct 2014 12:04:05 -0700 Subject: make --source and -m play nice together --HG-- branch : __main__-support extra : rebase_source : c9ca9fddecafddd5ef4db9cc64ca1c0972130aab --- coverage/cmdline.py | 3 +- coverage/control.py | 107 +++++++++++++++++++++++++++----------------------- coverage/files.py | 44 +++++++++++++++++++++ doc/contributing.rst | 2 +- tests/test_cmdline.py | 9 +++-- tests/test_files.py | 62 ++++++++++++++++++++++++++++- tests/test_process.py | 49 +++++++++++++++++++++++ 7 files changed, 221 insertions(+), 55 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index e7efe5c4..a80d1168 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -519,9 +519,10 @@ class CoverageScript(object): # Set the first path element properly. old_path0 = sys.path[0] + main_module = args[0] if options.module else None # Run the script. - self.coverage.start() + self.coverage.start(main_module=main_module) code_ran = True try: if options.module: diff --git a/coverage/control.py b/coverage/control.py index 1191b9eb..5e154537 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -1,6 +1,6 @@ """Core control stuff for Coverage.""" -import atexit, os, platform, random, socket, sys +import atexit, inspect, os, platform, random, socket, sys from coverage.annotate import AnnotateReporter from coverage.backward import string_class, iitems @@ -12,6 +12,7 @@ from coverage.debug import DebugControl from coverage.plugin import CoveragePlugin, Plugins, overrides from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher from coverage.files import PathAliases, find_python_files, prep_patterns +from coverage.files import ModuleMatcher from coverage.html import HtmlReporter from coverage.misc import CoverageException, bool_or_none, join_regex from coverage.misc import file_be_gone @@ -140,7 +141,7 @@ class Coverage(object): # Other instance attributes, set later. self.omit = self.include = self.source = None - self.source_pkgs = self.file_locator = None + self.not_imported = 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 @@ -198,6 +199,7 @@ class Coverage(object): self.source.append(self.file_locator.canonical_filename(src)) else: self.source_pkgs.append(src) + self.not_imported = list(self.source_pkgs) self.omit = prep_patterns(self.config.omit) self.include = prep_patterns(self.config.include) @@ -269,6 +271,7 @@ class Coverage(object): # 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_dir: self.cover_match = TreeMatcher([self.cover_dir]) @@ -303,6 +306,34 @@ class Coverage(object): filename = filename[:-9] + ".py" return filename + def _name_for_module(self, module_namespace, filename): + """ + 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 filename to get the module's name. + In the case that the module name can't be deteremined, None is returned. + """ + # TODO: unit-test + dunder_name = module_namespace.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_namespace.get('__loader__', None) + if hasattr(loader, 'fullname') and isinstance(loader.fullname, str): + # module loaded via runpy -m + return loader.fullname + + # script as first argument to python cli + inspectedname = inspect.getmodulename(filename) + if inspectedname is not None: + return inspectedname + else: + return dunder_name + + def _should_trace_with_reason(self, filename, frame): """Decide whether to trace execution in `filename`, with a reason. @@ -318,8 +349,6 @@ class Coverage(object): disp.reason = reason return disp - self._check_for_packages() - # Compiled Python files have two filenames: frame.f_code.co_filename is # the filename at the time the .pyc was compiled. The second name is # __file__, which is where the .pyc was actually loaded from. Since @@ -334,6 +363,8 @@ class Coverage(object): # Empty string is pretty useless return nope(disp, "empty string isn't a filename") + modulename = self._name_for_module(frame.f_globals, filename) + if filename.startswith('memory:'): return nope(disp, "memory isn't traceable") @@ -373,7 +404,9 @@ class Coverage(object): (plugin, disp.original_filename) ) if disp.check_filters: - reason = self._check_include_omit_etc(disp.source_filename) + reason = self._check_include_omit_etc( + disp.source_filename, modulename, + ) if reason: nope(disp, reason) @@ -381,7 +414,7 @@ class Coverage(object): return nope(disp, "no plugin found") # TODO: a test that causes this. - def _check_include_omit_etc(self, filename): + def _check_include_omit_etc(self, filename, modulename): """Check a filename against the include, omit, etc, rules. Returns a string or None. String means, don't trace, and is the reason @@ -393,6 +426,12 @@ class Coverage(object): # any canned exclusions. If they didn't, then we have to exclude the # stdlib and coverage.py directories. if self.source_match: + match = self.source_pkgs_match.match(modulename) + if match: + if modulename in self.not_imported: + self.not_imported.remove(modulename) + return None # There's no reason to skip this file. + if not self.source_match.match(filename): return "falls outside the --source trees" elif self.include_match: @@ -448,46 +487,6 @@ class Coverage(object): self._warnings.append(msg) sys.stderr.write("Coverage.py warning: %s\n" % msg) - def _check_for_packages(self): - """Update the source_match matcher with latest imported packages.""" - # Our self.source_pkgs attribute is a list of package names we want to - # measure. Each time through here, we see if we've imported any of - # them yet. If so, we add its file to source_match, and we don't have - # to look for that package any more. - if self.source_pkgs: - found = [] - for pkg in self.source_pkgs: - try: - mod = sys.modules[pkg] - except KeyError: - continue - - found.append(pkg) - - try: - pkg_file = mod.__file__ - except AttributeError: - pkg_file = None - else: - d, f = os.path.split(pkg_file) - if f.startswith('__init__'): - # This is actually a package, return the directory. - pkg_file = d - else: - pkg_file = self._source_for_file(pkg_file) - pkg_file = self.file_locator.canonical_filename(pkg_file) - if not os.path.exists(pkg_file): - pkg_file = None - - if pkg_file: - self.source.append(pkg_file) - self.source_match.add(pkg_file) - else: - self._warn("Module %s has no Python source." % pkg) - - for pkg in found: - self.source_pkgs.remove(pkg) - def use_cache(self, usecache): """Control the use of a data file (incorrectly called a cache). @@ -652,11 +651,21 @@ class Coverage(object): self.data.add_plugin_data(self.collector.get_plugin_data()) self.collector.reset() - # If there are still entries in the source_pkgs list, then we never + # If there are still entries in the not_imported list, then we never # encountered those packages. if self._warn_unimported_source: - for pkg in self.source_pkgs: - self._warn("Module %s was never imported." % pkg) + for pkg in self.not_imported: + if pkg not in sys.modules: + self._warn("Module %s was never imported." % pkg) + elif not hasattr(sys.modules[pkg], '__file__'): + self._warn("Module %s has no Python source." % pkg) + else: + raise AssertionError('''\ +Unexpected third case: + name: %s + object: %r + __file__: %s''' % (pkg, sys.modules[pkg], sys.modules[pkg].__file__) + ) # Find out if we got any data. summary = self.data.summary() diff --git a/coverage/files.py b/coverage/files.py index 1ed7276e..5e7c35aa 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -173,6 +173,50 @@ class TreeMatcher(object): return False +class ModuleMatcher(object): + """A matcher for files in a tree.""" + def __init__(self, module_names, main_module=None): + self.modules = list(module_names) + self.main_module = main_module + self.not_imported = list(module_names) + + def __repr__(self): + if self.main_module: + main_module = ', main_module=%r' % self.main_module + else: + main_module = '' + return "" % (self.modules, main_module) + + def info(self): + """A list of strings for displaying when dumping state.""" + return ['main_module=%r' % self.main_module] + self.modules + + def add(self, module): + """Add another directory to the list we match for.""" + self.modules.append(module) + + def match(self, module_name): + """Does `module_name` indicate a module in one of our packages? + + On success, returns the matched module name, which can be different in + the case of __main__. + """ + if not module_name: + return False + elif module_name == '__main__': + module_name = self.main_module or module_name + + for m in self.modules: + if module_name.startswith(m): + if module_name == m: + return module_name + if module_name[len(m)] == '.': + # This is a module in the package + return module_name + + return False + + class FnmatchMatcher(object): """A matcher for files by filename pattern.""" def __init__(self, pats): diff --git a/doc/contributing.rst b/doc/contributing.rst index 88c99a99..8e1fd723 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -129,7 +129,7 @@ I try to keep the coverage.py as clean as possible. I use pylint to alert me to possible problems:: $ make lint - pylint --rcfile=.pylintrc coverage setup.py tests + pylint coverage setup.py tests python -m tabnanny coverage setup.py tests python igor.py check_eol diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 695c3bec..3032a2f1 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -33,6 +33,9 @@ class BaseCmdLineTest(CoverageTest): ignore_errors=None, include=None, omit=None, morfs=[], show_missing=None, ) + defaults.start( + main_module=None, + ) defaults.xml_report( ignore_errors=None, include=None, omit=None, morfs=[], outfile=None, ) @@ -472,7 +475,7 @@ class CmdLineTest(BaseCmdLineTest): self.cmd_executes("run -m mymodule", """\ .coverage() .erase() - .start() + .start(main_module='mymodule') .run_python_module('mymodule', ['mymodule']) .stop() .save() @@ -480,7 +483,7 @@ class CmdLineTest(BaseCmdLineTest): self.cmd_executes("run -m mymodule -qq arg1 arg2", """\ .coverage() .erase() - .start() + .start(main_module='mymodule') .run_python_module('mymodule', ['mymodule', '-qq', 'arg1', 'arg2']) .stop() .save() @@ -488,7 +491,7 @@ class CmdLineTest(BaseCmdLineTest): self.cmd_executes("run --branch -m mymodule", """\ .coverage(branch=True) .erase() - .start() + .start(main_module='mymodule') .run_python_module('mymodule', ['mymodule']) .stop() .save() diff --git a/tests/test_files.py b/tests/test_files.py index 648c76a9..d72788c4 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -2,7 +2,9 @@ import os, os.path -from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher +from coverage.files import ( + FileLocator, TreeMatcher, FnmatchMatcher, ModuleMatcher +) from coverage.files import PathAliases, find_python_files, abs_file from coverage.misc import CoverageException @@ -80,6 +82,64 @@ class MatcherTest(CoverageTest): for filepath, matches in matches_to_try: self.assertMatches(tm, filepath, matches) + def test_module_matcher(self): + matches_to_try = [ + ('test', True), + ('test', True), + ('trash', False), + ('testing', False), + ('test.x', True), + ('test.x.y.z', True), + ('py', False), + ('py.t', False), + ('py.test', True), + ('py.testing', False), + ('py.test.buz', True), + ('py.test.buz.baz', True), + ('__main__', False), + ('mymain', True), + ('yourmain', False), + ] + modules = ['test', 'py.test', 'mymain'] + for mm in ( + ModuleMatcher(modules), + ModuleMatcher(modules, main_module=None), + ModuleMatcher(modules, main_module='yourmain'), + ): + self.assertEqual( + mm.info(), + ['main_module=%r' % mm.main_module] + modules + ) + for modulename, matches in matches_to_try: + self.assertEqual( + mm.match(modulename), + modulename if matches else False, + modulename, + ) + + def test_module_matcher_dunder_main(self): + matches_to_try = [ + ('__main__', True), + ('mymain', True), + ('yourmain', False), + ] + modules = ['test', 'py.test', 'mymain'] + mm = ModuleMatcher(modules, main_module='mymain') + self.assertEqual(mm.info(), ["main_module='mymain'"] + modules) + for modulename, matches in matches_to_try: + if not matches: + expected = False + elif modulename == '__main__': + expected = mm.main_module + else: + expected = modulename + + self.assertEqual( + mm.match(modulename), + expected, + modulename, + ) + def test_fnmatch_matcher(self): matches_to_try = [ (self.make_file("sub/file1.py"), True), diff --git a/tests/test_process.py b/tests/test_process.py index ac5c6e1b..e53e64c9 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -331,6 +331,55 @@ class ProcessTest(CoverageTest): out_py = self.run_command("python -m tests.try_execfile") self.assertMultiLineEqual(out_cov, out_py) + def test_coverage_run_dashm_equal_to_doubledashsource(self): + """regression test for #328 + + When imported by -m, a module's __name__ is __main__, but we need the + --source machinery to know and respect the original name. + """ + # These -m commands assume the coverage tree is on the path. + out_cov = self.run_command( + "coverage run --source tests.try_execfile -m tests.try_execfile" + ) + out_py = self.run_command("python -m tests.try_execfile") + self.assertMultiLineEqual(out_cov, out_py) + + def test_coverage_run_dashm_superset_of_doubledashsource(self): + """Edge case: --source foo -m foo.bar""" + # These -m commands assume the coverage tree is on the path. + out_cov = self.run_command( + "coverage run --source tests -m tests.try_execfile" + ) + out_py = self.run_command("python -m tests.try_execfile") + self.assertMultiLineEqual(out_cov, out_py) + + st, out = self.run_command_status("coverage report") + self.assertEqual(st, 0) + self.assertEqual(self.line_count(out), 6, out) + + def test_coverage_run_script_imports_doubledashsource(self): + self.make_file("myscript", """\ + import sys + sys.dont_write_bytecode = True + + def main(): + import tests.try_execfile + + if __name__ == '__main__': + main() + """) + + # These -m commands assume the coverage tree is on the path. + out_cov = self.run_command( + "coverage run --source tests myscript" + ) + out_py = self.run_command("python myscript") + self.assertMultiLineEqual(out_cov, out_py) + + st, out = self.run_command_status("coverage report") + self.assertEqual(st, 0) + self.assertEqual(self.line_count(out), 6, out) + def test_coverage_run_dashm_is_like_python_dashm_off_path(self): # https://bitbucket.org/ned/coveragepy/issue/242 tryfile = os.path.join(here, "try_execfile.py") -- cgit v1.2.1 From d05360a060187452f49302467f87ead09d27c9ba Mon Sep 17 00:00:00 2001 From: Buck Golemon Date: Fri, 14 Nov 2014 14:00:27 -0800 Subject: ModuleFinder no longer takes a main-module argument. --HG-- branch : __main__-support extra : rebase_source : 3f4c29876960153907b65b6e8e818b8228ca4ec0 extra : histedit_source : 6567adaf9c89483b71501fd91e9d3c83ed3daec7%2C2c5c8a7af95b8b26af384b9a65815d077e4313ec --- coverage/cmdline.py | 3 +-- coverage/control.py | 5 ++--- coverage/files.py | 20 +++++--------------- tests/test_cmdline.py | 9 +++------ tests/test_files.py | 39 ++++++--------------------------------- 5 files changed, 17 insertions(+), 59 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index a80d1168..e7efe5c4 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -519,10 +519,9 @@ class CoverageScript(object): # Set the first path element properly. old_path0 = sys.path[0] - main_module = args[0] if options.module else None # Run the script. - self.coverage.start(main_module=main_module) + self.coverage.start() code_ran = True try: if options.module: diff --git a/coverage/control.py b/coverage/control.py index 5e154537..47e5b508 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -141,7 +141,7 @@ class Coverage(object): # Other instance attributes, set later. self.omit = self.include = self.source = None - self.not_imported = self.source_pkgs = self.file_locator = 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 @@ -426,8 +426,7 @@ class Coverage(object): # any canned exclusions. If they didn't, then we have to exclude the # stdlib and coverage.py directories. if self.source_match: - match = self.source_pkgs_match.match(modulename) - if match: + if self.source_pkgs_match.match(modulename): if modulename in self.not_imported: self.not_imported.remove(modulename) return None # There's no reason to skip this file. diff --git a/coverage/files.py b/coverage/files.py index 5e7c35aa..97888b62 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -175,21 +175,16 @@ class TreeMatcher(object): class ModuleMatcher(object): """A matcher for files in a tree.""" - def __init__(self, module_names, main_module=None): + def __init__(self, module_names): self.modules = list(module_names) - self.main_module = main_module self.not_imported = list(module_names) def __repr__(self): - if self.main_module: - main_module = ', main_module=%r' % self.main_module - else: - main_module = '' - return "" % (self.modules, main_module) + return "" % (self.modules) def info(self): """A list of strings for displaying when dumping state.""" - return ['main_module=%r' % self.main_module] + self.modules + return self.modules def add(self, module): """Add another directory to the list we match for.""" @@ -197,22 +192,17 @@ class ModuleMatcher(object): def match(self, module_name): """Does `module_name` indicate a module in one of our packages? - - On success, returns the matched module name, which can be different in - the case of __main__. """ if not module_name: return False - elif module_name == '__main__': - module_name = self.main_module or module_name for m in self.modules: if module_name.startswith(m): if module_name == m: - return module_name + return True if module_name[len(m)] == '.': # This is a module in the package - return module_name + return True return False diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 3032a2f1..695c3bec 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -33,9 +33,6 @@ class BaseCmdLineTest(CoverageTest): ignore_errors=None, include=None, omit=None, morfs=[], show_missing=None, ) - defaults.start( - main_module=None, - ) defaults.xml_report( ignore_errors=None, include=None, omit=None, morfs=[], outfile=None, ) @@ -475,7 +472,7 @@ class CmdLineTest(BaseCmdLineTest): self.cmd_executes("run -m mymodule", """\ .coverage() .erase() - .start(main_module='mymodule') + .start() .run_python_module('mymodule', ['mymodule']) .stop() .save() @@ -483,7 +480,7 @@ class CmdLineTest(BaseCmdLineTest): self.cmd_executes("run -m mymodule -qq arg1 arg2", """\ .coverage() .erase() - .start(main_module='mymodule') + .start() .run_python_module('mymodule', ['mymodule', '-qq', 'arg1', 'arg2']) .stop() .save() @@ -491,7 +488,7 @@ class CmdLineTest(BaseCmdLineTest): self.cmd_executes("run --branch -m mymodule", """\ .coverage(branch=True) .erase() - .start(main_module='mymodule') + .start() .run_python_module('mymodule', ['mymodule']) .stop() .save() diff --git a/tests/test_files.py b/tests/test_files.py index d72788c4..f6976a81 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -101,42 +101,15 @@ class MatcherTest(CoverageTest): ('yourmain', False), ] modules = ['test', 'py.test', 'mymain'] - for mm in ( - ModuleMatcher(modules), - ModuleMatcher(modules, main_module=None), - ModuleMatcher(modules, main_module='yourmain'), - ): - self.assertEqual( - mm.info(), - ['main_module=%r' % mm.main_module] + modules - ) - for modulename, matches in matches_to_try: - self.assertEqual( - mm.match(modulename), - modulename if matches else False, - modulename, - ) - - def test_module_matcher_dunder_main(self): - matches_to_try = [ - ('__main__', True), - ('mymain', True), - ('yourmain', False), - ] - modules = ['test', 'py.test', 'mymain'] - mm = ModuleMatcher(modules, main_module='mymain') - self.assertEqual(mm.info(), ["main_module='mymain'"] + modules) + mm = ModuleMatcher(modules) + self.assertEqual( + mm.info(), + modules + ) for modulename, matches in matches_to_try: - if not matches: - expected = False - elif modulename == '__main__': - expected = mm.main_module - else: - expected = modulename - self.assertEqual( mm.match(modulename), - expected, + matches, modulename, ) -- cgit v1.2.1 From 7ca11054c5b0f91bd77c16923ec443ccee6f2dd9 Mon Sep 17 00:00:00 2001 From: Buck Golemon Date: Fri, 14 Nov 2014 14:00:48 -0800 Subject: shim for pep302 __loader__ --HG-- branch : __main__-support extra : histedit_source : f07b26f0cfc575d81972546f1b7ae2ece84e2d87 --- coverage/execfile.py | 16 ++++++++++++++-- tests/try_execfile.py | 2 ++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/coverage/execfile.py b/coverage/execfile.py index 82cc2217..f03713ec 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -7,6 +7,15 @@ from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec from coverage.misc import ExceptionDuringRun, NoCode, NoSource +class DummyLoader(object): + """A shim for the pep302 __loader__, emulating pkgutil.ImpLoader. + + Currently only implements the .fullname attribute + """ + def __init__(self, fullname, *args): + self.fullname = fullname + + if importlib_util_find_spec: def find_module(modulename): """Find the module named `modulename`. @@ -92,10 +101,10 @@ def run_python_module(modulename, args): pathname = os.path.abspath(pathname) args[0] = pathname - run_python_file(pathname, args, package=packagename) + run_python_file(pathname, args, package=packagename, modulename=modulename) -def run_python_file(filename, args, package=None): +def run_python_file(filename, args, package=None, modulename=None): """Run a python file as if it were the main program on the command line. `filename` is the path to the file to execute, it need not be a .py file. @@ -111,6 +120,9 @@ def run_python_file(filename, args, package=None): main_mod.__file__ = filename if package: main_mod.__package__ = package + if modulename: + main_mod.__loader__ = DummyLoader(modulename) + main_mod.__builtins__ = BUILTINS # Set sys.argv properly. diff --git a/tests/try_execfile.py b/tests/try_execfile.py index 314ffae0..fd430e6b 100644 --- a/tests/try_execfile.py +++ b/tests/try_execfile.py @@ -60,6 +60,8 @@ globals_to_check = { '__doc__': __doc__, '__builtins__.has_open': hasattr(__builtins__, 'open'), '__builtins__.dir': dir(__builtins__), + '__loader__ exists': '__loader__' in globals(), + '__loader__.fullname': getattr(globals().get('__loader__', None), 'fullname', None), '__package__': __package__, 'DATA': DATA, 'FN_VAL': FN_VAL, -- cgit v1.2.1 From 148a2291e1e7b04eecaa15c90b125cc8199d7974 Mon Sep 17 00:00:00 2001 From: Buck Golemon Date: Fri, 14 Nov 2014 16:33:41 -0800 Subject: they changed the way __loader__ looks in 3.3 --HG-- branch : __main__-support extra : amend_source : 7abd8465667b4bd4b3eeac24391c27c68edf9a95 --- coverage/control.py | 11 ++++++++--- coverage/execfile.py | 8 +++++++- tests/try_execfile.py | 7 +++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 47e5b508..7a85103b 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -322,9 +322,14 @@ class Coverage(object): return dunder_name loader = module_namespace.get('__loader__', None) - if hasattr(loader, 'fullname') and isinstance(loader.fullname, str): - # module loaded via runpy -m - return loader.fullname + for attrname in ('fullname', 'name'): # attribute renamed in py3.2 + if ( + hasattr(loader, 'fullname') and + isinstance(loader.fullname, str) and + loader.fullname != '__main__' + ): + # module loaded via runpy -m + return loader.fullname # script as first argument to python cli inspectedname = inspect.getmodulename(filename) diff --git a/coverage/execfile.py b/coverage/execfile.py index f03713ec..4b5f3af4 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -7,6 +7,12 @@ from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec from coverage.misc import ExceptionDuringRun, NoCode, NoSource +if sys.version_info >= (3, 3): + DEFAULT_FULLNAME = '__main__' +else: + DEFAULT_FULLNAME = None + + class DummyLoader(object): """A shim for the pep302 __loader__, emulating pkgutil.ImpLoader. @@ -104,7 +110,7 @@ def run_python_module(modulename, args): run_python_file(pathname, args, package=packagename, modulename=modulename) -def run_python_file(filename, args, package=None, modulename=None): +def run_python_file(filename, args, package=None, modulename=DEFAULT_FULLNAME): """Run a python file as if it were the main program on the command line. `filename` is the path to the file to execute, it need not be a .py file. diff --git a/tests/try_execfile.py b/tests/try_execfile.py index fd430e6b..ee5bae5c 100644 --- a/tests/try_execfile.py +++ b/tests/try_execfile.py @@ -54,14 +54,17 @@ def my_function(a): FN_VAL = my_function("fooey") +loader = globals().get('__loader__') +fullname = getattr(loader, 'fullname', None) or getattr(loader, 'name', None) + globals_to_check = { '__name__': __name__, '__file__': __file__, '__doc__': __doc__, '__builtins__.has_open': hasattr(__builtins__, 'open'), '__builtins__.dir': dir(__builtins__), - '__loader__ exists': '__loader__' in globals(), - '__loader__.fullname': getattr(globals().get('__loader__', None), 'fullname', None), + '__loader__ exists': loader is not None, + '__loader__.fullname': fullname, '__package__': __package__, 'DATA': DATA, 'FN_VAL': FN_VAL, -- cgit v1.2.1 From ae3deed9e39b528abc2259a2fe7f7178623fce8e Mon Sep 17 00:00:00 2001 From: Buck Golemon Date: Fri, 14 Nov 2014 17:19:00 -0800 Subject: pypy3 sets sys.__file__ to a nonexistant file --HG-- branch : __main__-support --- coverage/control.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/coverage/control.py b/coverage/control.py index 7a85103b..1b932da3 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -661,7 +661,10 @@ class Coverage(object): for pkg in self.not_imported: if pkg not in sys.modules: self._warn("Module %s was never imported." % pkg) - elif not hasattr(sys.modules[pkg], '__file__'): + elif not ( + hasattr(sys.modules[pkg], '__file__') and + os.path.exists(sys.modules[pkg].__file__) + ): self._warn("Module %s has no Python source." % pkg) else: raise AssertionError('''\ -- cgit v1.2.1 From 5082e667fbc65db03d32138fcdfa087a6fc073a6 Mon Sep 17 00:00:00 2001 From: Buck Golemon Date: Fri, 14 Nov 2014 17:52:52 -0800 Subject: ned code review, part 1 --HG-- branch : __main__-support --- coverage/control.py | 9 ++++----- coverage/files.py | 3 +-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 1b932da3..6a136da4 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -199,7 +199,6 @@ class Coverage(object): self.source.append(self.file_locator.canonical_filename(src)) else: self.source_pkgs.append(src) - self.not_imported = list(self.source_pkgs) self.omit = prep_patterns(self.config.omit) self.include = prep_patterns(self.config.include) @@ -432,8 +431,8 @@ class Coverage(object): # stdlib and coverage.py directories. if self.source_match: if self.source_pkgs_match.match(modulename): - if modulename in self.not_imported: - self.not_imported.remove(modulename) + if modulename in self.source_pkgs: + self.source_pkgs.remove(modulename) return None # There's no reason to skip this file. if not self.source_match.match(filename): @@ -655,10 +654,10 @@ class Coverage(object): self.data.add_plugin_data(self.collector.get_plugin_data()) self.collector.reset() - # If there are still entries in the not_imported list, then we never + # If there are still entries in the source_pkgs list, then we never # encountered those packages. if self._warn_unimported_source: - for pkg in self.not_imported: + for pkg in self.source_pkgs: if pkg not in sys.modules: self._warn("Module %s was never imported." % pkg) elif not ( diff --git a/coverage/files.py b/coverage/files.py index 97888b62..332c4225 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -174,10 +174,9 @@ class TreeMatcher(object): class ModuleMatcher(object): - """A matcher for files in a tree.""" + """A matcher for modules in a tree.""" def __init__(self, module_names): self.modules = list(module_names) - self.not_imported = list(module_names) def __repr__(self): return "" % (self.modules) -- cgit v1.2.1 From b3ccb75241566c1e1a814ae99a84637fd0ac2b44 Mon Sep 17 00:00:00 2001 From: Buck Golemon Date: Sat, 15 Nov 2014 14:01:06 -0800 Subject: add necessary tests, fix attrname, once it was shown to fail --HG-- branch : __main__-support --- coverage/control.py | 12 ++++-- tests/test_process.py | 102 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 105 insertions(+), 9 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 6a136da4..e568f643 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -322,13 +322,17 @@ class Coverage(object): loader = module_namespace.get('__loader__', None) for attrname in ('fullname', 'name'): # attribute renamed in py3.2 + if hasattr(loader, attrname): + fullname = getattr(loader, attrname) + else: + continue + if ( - hasattr(loader, 'fullname') and - isinstance(loader.fullname, str) and - loader.fullname != '__main__' + isinstance(fullname, str) and + fullname != '__main__' ): # module loaded via runpy -m - return loader.fullname + return fullname # script as first argument to python cli inspectedname = inspect.getmodulename(filename) diff --git a/tests/test_process.py b/tests/test_process.py index e53e64c9..df26aaf5 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -662,11 +662,10 @@ class FailUnderTest(CoverageTest): self.assertEqual(st, 2) -class ProcessStartupTest(CoverageTest): - """Test that we can measure coverage in subprocesses.""" - +class ProcessCoverageMixin(object): + """Set up a .pth file that causes all subprocesses to be coverage'd""" def setUp(self): - super(ProcessStartupTest, self).setUp() + super(ProcessCoverageMixin, self).setUp() # Find a place to put a .pth file. pth_contents = "import coverage; coverage.process_startup()\n" for d in sys.path: # pragma: part covered @@ -684,10 +683,14 @@ class ProcessStartupTest(CoverageTest): raise Exception("Couldn't find a place for the .pth file") def tearDown(self): - super(ProcessStartupTest, self).tearDown() + super(ProcessCoverageMixin, self).tearDown() # Clean up the .pth file we made. os.remove(self.pth_path) + +class ProcessStartupTest(ProcessCoverageMixin, CoverageTest): + """Test that we can measure coverage in subprocesses.""" + def test_subprocess_with_pth_files(self): # pragma: not covered if os.environ.get('COVERAGE_COVERAGE', ''): raise SkipTest( @@ -720,3 +723,92 @@ class ProcessStartupTest(CoverageTest): data = coverage.CoverageData() data.read_file(".mycovdata") self.assertEqual(data.summary()['sub.py'], 2) + + +class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest): + """Show that we can configure {[run]source} during process-level coverage. + + There are two interesting variables: + 1) -m versus a simple script argument (eg `python myscript`) + 2) filtering for the top-level (main.py) or second-level (sub.py) module + 3) whether the files are in a package or not + + ... for a total of eight tests. + """ + def assert_pth_and_source_work_together(self, dashm, package, source): + def fullname(modname): + if package and dashm: + return '.'.join((package, modname)) + else: + return modname + + def path(basename): + return os.path.join(package, basename) + + if os.environ.get('COVERAGE_COVERAGE', ''): + raise SkipTest( + "Can't test subprocess pth file suppport during metacoverage" + ) + # Main will run sub.py + self.make_file(path("main.py"), """\ + import %s + if True: pass + """ % fullname('sub')) + if package: + self.make_file(path("__init__.py"), '') + # sub.py will write a few lines. + self.make_file(path("sub.py"), """\ + with open("out.txt", "w") as f: + f.write("Hello, world!\\n") + """) + self.make_file("coverage.ini", """\ + [run] + source = %s + """ % fullname(source) + ) + + self.set_environ("COVERAGE_PROCESS_START", "coverage.ini") + + if dashm: + cmd = (sys.executable, dashm, fullname('main')) + else: + cmd = (sys.executable, path('main.py')) + + from subprocess import Popen + Popen(cmd).wait() + + with open("out.txt") as f: + self.assertEqual(f.read(), "Hello, world!\n") + + # Read the data from .coverage + self.assert_exists(".coverage") + data = coverage.CoverageData() + data.read_file(".coverage") + summary = data.summary() + print(summary) + self.assertEqual(summary[source + '.py'], 2) + self.assertEqual(len(summary), 1) + + def test_dashm_main(self): + self.assert_pth_and_source_work_together('-m', '', 'main') + + def test_script_main(self): + self.assert_pth_and_source_work_together('', '', 'main') + + def test_dashm_sub(self): + self.assert_pth_and_source_work_together('-m', '', 'sub') + + def test_script_sub(self): + self.assert_pth_and_source_work_together('', '', 'sub') + + def test_dashm_pkg_main(self): + self.assert_pth_and_source_work_together('-m', 'pkg', 'main') + + def test_script_pkg_main(self): + self.assert_pth_and_source_work_together('', 'pkg', 'main') + + def test_dashm_pkg_sub(self): + self.assert_pth_and_source_work_together('-m', 'pkg', 'sub') + + def test_script_pkg_sub(self): + self.assert_pth_and_source_work_together('', 'pkg', 'sub') -- cgit v1.2.1