From f9063035c3c96a63ebd4041ee7e08a4f3cf4bec4 Mon Sep 17 00:00:00 2001 From: Peter Ebden Date: Mon, 13 Jul 2015 17:05:40 +0100 Subject: Add support for searching inside .pex archives similarly to .zip and .egg. --- coverage/python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'coverage') diff --git a/coverage/python.py b/coverage/python.py index b1667f6..22ca214 100644 --- a/coverage/python.py +++ b/coverage/python.py @@ -63,7 +63,7 @@ def get_zip_bytes(filename): an empty string if the file is empty. """ - markers = ['.zip'+os.sep, '.egg'+os.sep] + markers = ['.zip'+os.sep, '.egg'+os.sep, '.pex'+os.sep] for marker in markers: if marker in filename: parts = filename.split(marker) -- cgit v1.2.1 From cadcb4ca7a7d26589e5d42df1b4494636c39ed61 Mon Sep 17 00:00:00 2001 From: loic Date: Fri, 16 Dec 2016 13:29:18 +0100 Subject: --source and --include are mutually exclusive #265 Add a sanity_check function to CoverageConfig to perform sanity checks after configuration values have been collected from files, arguments etc. Raise an error if --source and --include are both set. The config tests are amended because some of them have both source and include set. Based on the original patch from Nathan Land close #265 --- coverage/config.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'coverage') diff --git a/coverage/config.py b/coverage/config.py index ad3efa9..f3e296c 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -383,6 +383,12 @@ class CoverageConfig(object): # If we get here, we didn't find the option. raise CoverageException("No such option: %r" % option_name) + def sanity_check(self): + if ((self.source is not None) and + (self.include is not None)): + raise CoverageException( + "--include and --source are mutually exclusive") + def read_coverage_config(config_file, **kwargs): """Read the coverage.py configuration. @@ -439,4 +445,6 @@ def read_coverage_config(config_file, **kwargs): # 4) from constructor arguments: config.from_args(**kwargs) + config.sanity_check() + return config_file, config -- cgit v1.2.1 From 2ccdbbd9c205ca33757b9210e0168603088333a5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 16 Dec 2016 21:15:55 -0500 Subject: Clean up and credit the #265 work --- coverage/config.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'coverage') diff --git a/coverage/config.py b/coverage/config.py index f3e296c..287844b 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -368,7 +368,6 @@ class CoverageConfig(object): Returns the value of the option. """ - # Check all the hard-coded options. for option_spec in self.CONFIG_FILE_OPTIONS: attr, where = option_spec[:2] @@ -384,10 +383,9 @@ class CoverageConfig(object): raise CoverageException("No such option: %r" % option_name) def sanity_check(self): - if ((self.source is not None) and - (self.include is not None)): - raise CoverageException( - "--include and --source are mutually exclusive") + """Check interactions among settings, and raise if there's a problem.""" + if (self.source is not None) and (self.include is not None): + raise CoverageException("--include and --source are mutually exclusive") def read_coverage_config(config_file, **kwargs): @@ -446,5 +444,5 @@ def read_coverage_config(config_file, **kwargs): config.from_args(**kwargs) config.sanity_check() - + return config_file, config -- cgit v1.2.1 From 8ea587ab6a390051153bc8465040025f28cea79b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 17 Dec 2016 16:14:58 -0500 Subject: Provide a more useful error message if failing to run a non-Python file. #514 --- coverage/execfile.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) (limited to 'coverage') diff --git a/coverage/execfile.py b/coverage/execfile.py index 3e20a52..58b0540 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -10,7 +10,7 @@ import types from coverage.backward import BUILTINS from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec -from coverage.misc import ExceptionDuringRun, NoCode, NoSource, isolate_module +from coverage.misc import CoverageException, ExceptionDuringRun, NoCode, NoSource, isolate_module from coverage.phystokens import compile_unicode from coverage.python import get_python_source @@ -166,11 +166,17 @@ def run_python_file(filename, args, package=None, modulename=None, path0=None): sys.path[0] = path0 if path0 is not None else my_path0 try: - # Make a code object somehow. - if filename.endswith((".pyc", ".pyo")): - code = make_code_from_pyc(filename) - else: - code = make_code_from_py(filename) + try: + # Make a code object somehow. + if filename.endswith((".pyc", ".pyo")): + code = make_code_from_pyc(filename) + else: + code = make_code_from_py(filename) + except CoverageException: + raise + except Exception as exc: + msg = "Couldn't run {filename!r} as Python code: {exc.__class__.__name__}: {exc}" + raise CoverageException(msg.format(filename=filename, exc=exc)) # Execute the code object. try: -- cgit v1.2.1 From c7a90694cccf5904a3d69fd2d9cdbe66061cb1de Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 18 Dec 2016 10:37:16 -0500 Subject: Add some to the ArcStart docstring, and remove defaultable arguments --- coverage/parser.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) (limited to 'coverage') diff --git a/coverage/parser.py b/coverage/parser.py index 71334b6..92ff047 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -459,8 +459,12 @@ class TryBlock(object): class ArcStart(collections.namedtuple("Arc", "lineno, cause")): """The information needed to start an arc. - `lineno` is the line number the arc starts from. `cause` is a fragment - used as the startmsg for AstArcAnalyzer.missing_arc_fragments. + `lineno` is the line number the arc starts from. + + `cause` is an English text fragment used as the `startmsg` for + AstArcAnalyzer.missing_arc_fragments. It will be used to describe why an + arc wasn't executed, so should fit well into a sentence of the form, + "Line 17 didn't run because {cause}." """ def __new__(cls, lineno, cause=None): @@ -591,7 +595,7 @@ class AstArcAnalyzer(object): node_name = node.__class__.__name__ if node_name not in self.OK_TO_DEFAULT: print("*** Unhandled: {0}".format(node)) - return set([ArcStart(self.line_for_node(node), cause=None)]) + return set([ArcStart(self.line_for_node(node))]) @contract(returns='ArcStarts') def add_body_arcs(self, body, from_start=None, prev_starts=None): @@ -722,7 +726,7 @@ class AstArcAnalyzer(object): self.add_arc(last, lineno) last = lineno # The body is handled in collect_arcs. - return set([ArcStart(last, cause=None)]) + return set([ArcStart(last)]) _handle__ClassDef = _handle_decorated @@ -799,7 +803,7 @@ class AstArcAnalyzer(object): self.block_stack.append(try_block) start = self.line_for_node(node) - exits = self.add_body_arcs(node.body, from_start=ArcStart(start, cause=None)) + exits = self.add_body_arcs(node.body, from_start=ArcStart(start)) # We're done with the `try` body, so this block no longer handles # exceptions. We keep the block so the `finally` clause can pick up -- cgit v1.2.1 From 63d10d30aa1d64950f62ff7f767c32416bb11ce0 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 18 Dec 2016 12:02:45 -0500 Subject: More doc and cleanup of ast parsing --- coverage/parser.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) (limited to 'coverage') diff --git a/coverage/parser.py b/coverage/parser.py index 92ff047..5302310 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -433,23 +433,35 @@ class ByteParser(object): class LoopBlock(object): """A block on the block stack representing a `for` or `while` loop.""" + @contract(start=int) def __init__(self, start): + # The line number where the loop starts. self.start = start + # A set of ArcStarts, the arcs from break statements exiting this loop. self.break_exits = set() class FunctionBlock(object): """A block on the block stack representing a function definition.""" + @contract(start=int, name=str) def __init__(self, start, name): + # The line number where the function starts. self.start = start + # The name of the function. self.name = name class TryBlock(object): """A block on the block stack representing a `try` block.""" - def __init__(self, handler_start=None, final_start=None): + @contract(handler_start='int|None', final_start='int|None') + def __init__(self, handler_start, final_start): + # The line number of the first "except" handler, if any. self.handler_start = handler_start + # The line number of the "finally:" clause, if any. self.final_start = final_start + + # The ArcStarts for breaks/continues/returns/raises inside the "try:" + # that need to route through the "finally:" clause. self.break_from = set() self.continue_from = set() self.return_from = set() @@ -464,7 +476,8 @@ class ArcStart(collections.namedtuple("Arc", "lineno, cause")): `cause` is an English text fragment used as the `startmsg` for AstArcAnalyzer.missing_arc_fragments. It will be used to describe why an arc wasn't executed, so should fit well into a sentence of the form, - "Line 17 didn't run because {cause}." + "Line 17 didn't run because {cause}." The fragment can include "{lineno}" + to have `lineno` interpolated into it. """ def __new__(cls, lineno, cause=None): @@ -592,7 +605,6 @@ class AstArcAnalyzer(object): return handler(node) if 0: - node_name = node.__class__.__name__ if node_name not in self.OK_TO_DEFAULT: print("*** Unhandled: {0}".format(node)) return set([ArcStart(self.line_for_node(node))]) @@ -638,6 +650,15 @@ class AstArcAnalyzer(object): # listcomps hidden in lists: x = [[i for i in range(10)]] # nested function definitions + + ## Exit processing: process_*_exits + + # These functions process the four kinds of jump exits: break, continue, + # raise, and return. To figure out where an exit goes, we have to look at + # the block stack context. For example, a break will jump to the nearest + # enclosing loop block, or the nearest enclosing finally block, whichever + # is nearer. + @contract(exits='ArcStarts') def process_break_exits(self, exits): """Add arcs due to jumps from `exits` being breaks.""" @@ -696,7 +717,8 @@ class AstArcAnalyzer(object): ) break - ## Handlers + + ## Handlers: _handle__* @contract(returns='ArcStarts') def _handle__Break(self, node): @@ -753,7 +775,7 @@ class AstArcAnalyzer(object): else_exits = self.add_body_arcs(node.orelse, from_start=from_start) exits |= else_exits else: - # no else clause: exit from the for line. + # No else clause: exit from the for line. exits.add(from_start) return exits @@ -799,7 +821,7 @@ class AstArcAnalyzer(object): else: final_start = None - try_block = TryBlock(handler_start=handler_start, final_start=final_start) + try_block = TryBlock(handler_start, final_start) self.block_stack.append(try_block) start = self.line_for_node(node) @@ -862,6 +884,7 @@ class AstArcAnalyzer(object): return exits + @contract(returns='ArcStarts') def _combine_finally_starts(self, starts, exits): """Helper for building the cause of `finally` branches.""" causes = [] -- cgit v1.2.1 From 8b47f416b40c7999df51ad5e417fb06e220d746b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 18 Dec 2016 17:49:47 -0500 Subject: More ast parsing clean-up. --- coverage/parser.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) (limited to 'coverage') diff --git a/coverage/parser.py b/coverage/parser.py index 5302310..aa2dcef 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -510,7 +510,9 @@ class AstArcAnalyzer(object): self.arcs = set() - # A map from arc pairs to a pair of sentence fragments: (startmsg, endmsg). + # A map from arc pairs to a list of pairs of sentence fragments: + # { (start, end): [(startmsg, endmsg), ...], } + # # For an arc from line 17, they should be usable like: # "Line 17 {endmsg}, because {startmsg}" self.missing_arc_fragments = collections.defaultdict(list) @@ -587,6 +589,7 @@ class AstArcAnalyzer(object): # Modules have no line number, they always start at 1. return 1 + # The node types that just flow to the next node with no complications. OK_TO_DEFAULT = set([ "Assign", "Assert", "AugAssign", "Delete", "Exec", "Expr", "Global", "Import", "ImportFrom", "Nonlocal", "Pass", "Print", @@ -603,11 +606,15 @@ class AstArcAnalyzer(object): handler = getattr(self, "_handle__" + node_name, None) if handler is not None: return handler(node) - - if 0: - if node_name not in self.OK_TO_DEFAULT: + else: + # No handler: either it's something that's ok to default (a simple + # statement), or it's something we overlooked. Change this 0 to 1 + # to see if it's overlooked. + if 0 and node_name not in self.OK_TO_DEFAULT: print("*** Unhandled: {0}".format(node)) - return set([ArcStart(self.line_for_node(node))]) + + # Default for simple statements: one exit from this node. + return set([ArcStart(self.line_for_node(node))]) @contract(returns='ArcStarts') def add_body_arcs(self, body, from_start=None, prev_starts=None): @@ -651,8 +658,8 @@ class AstArcAnalyzer(object): # nested function definitions - ## Exit processing: process_*_exits - + # Exit processing: process_*_exits + # # These functions process the four kinds of jump exits: break, continue, # raise, and return. To figure out where an exit goes, we have to look at # the block stack context. For example, a break will jump to the nearest @@ -718,7 +725,11 @@ class AstArcAnalyzer(object): break - ## Handlers: _handle__* + # Handlers: _handle__* + # + # Each handler deals with a specific AST node type, dispatched from + # add_arcs. These functions mirror the Python semantics of each syntactic + # construct. @contract(returns='ArcStarts') def _handle__Break(self, node): -- cgit v1.2.1