diff -r dc171b1d8261 -r 362cd1b6f81a eric6/DebugClients/Python/coverage/misc.py --- a/eric6/DebugClients/Python/coverage/misc.py Wed Feb 19 19:38:36 2020 +0100 +++ b/eric6/DebugClients/Python/coverage/misc.py Sat Feb 22 14:27:42 2020 +0100 @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Miscellaneous stuff for coverage.py.""" @@ -8,6 +8,10 @@ import inspect import locale import os +import os.path +import random +import re +import socket import sys import types @@ -45,9 +49,14 @@ return _decorator +# Environment COVERAGE_NO_CONTRACTS=1 can turn off contracts while debugging +# tests to remove noise from stack traces. +# $set_env.py: COVERAGE_NO_CONTRACTS - Disable PyContracts to simplify stack traces. +USE_CONTRACTS = env.TESTING and not bool(int(os.environ.get("COVERAGE_NO_CONTRACTS", 0))) + # Use PyContracts for assertion testing on parameters and returns, but only if # we are running our own test suite. -if env.TESTING: +if USE_CONTRACTS: from contracts import contract # pylint: disable=unused-import from contracts import new_contract as raw_new_contract @@ -69,11 +78,11 @@ """Ensure that only one of the argnames is non-None.""" def _decorator(func): argnameset = set(name.strip() for name in argnames.split(",")) - def _wrapped(*args, **kwargs): + def _wrapper(*args, **kwargs): vals = [kwargs.get(name) for name in argnameset] assert sum(val is not None for val in vals) == 1 return func(*args, **kwargs) - return _wrapped + return _wrapper return _decorator else: # pragma: not testing # We aren't using real PyContracts, so just define our decorators as @@ -100,44 +109,6 @@ return "%d-%d" % (start, end) -def format_lines(statements, lines): - """Nicely format a list of line numbers. - - Format a list of line numbers for printing by coalescing groups of lines as - long as the lines represent consecutive statements. This will coalesce - even if there are gaps between statements. - - For example, if `statements` is [1,2,3,4,5,10,11,12,13,14] and - `lines` is [1,2,5,10,11,13,14] then the result will be "1-2, 5-11, 13-14". - - Both `lines` and `statements` can be any iterable. All of the elements of - `lines` must be in `statements`, and all of the values must be positive - integers. - - """ - statements = sorted(statements) - lines = sorted(lines) - - pairs = [] - start = None - lidx = 0 - for stmt in statements: - if lidx >= len(lines): - break - if stmt == lines[lidx]: - lidx += 1 - if not start: - start = stmt - end = stmt - elif start: - pairs.append((start, end)) - start = None - if start: - pairs.append((start, end)) - ret = ', '.join(map(nice_pair, pairs)) - return ret - - def expensive(fn): """A decorator to indicate that a method shouldn't be called more than once. @@ -148,13 +119,12 @@ if env.TESTING: attr = "_once_" + fn.__name__ - def _wrapped(self): - """Inner function that checks the cache.""" + def _wrapper(self): if hasattr(self, attr): raise AssertionError("Shouldn't have called %s more than once" % fn.__name__) setattr(self, attr, True) return fn(self) - return _wrapped + return _wrapper else: return fn # pragma: not testing @@ -181,6 +151,20 @@ raise +def ensure_dir(directory): + """Make sure the directory exists. + + If `directory` is None or empty, do nothing. + """ + if directory and not os.path.isdir(directory): + os.makedirs(directory) + + +def ensure_dir_for_file(path): + """Make sure the directory for the path exists.""" + ensure_dir(os.path.dirname(path)) + + def output_encoding(outfile=None): """Determine the encoding to use for output written to `outfile` or stdout.""" if outfile is None: @@ -193,6 +177,26 @@ return encoding +def filename_suffix(suffix): + """Compute a filename suffix for a data file. + + If `suffix` is a string or None, simply return it. If `suffix` is True, + then build a suffix incorporating the hostname, process id, and a random + number. + + Returns a string or None. + + """ + if suffix is True: + # If data_suffix was a simple true value, then make a suffix with + # plenty of distinguishing information. We do this here in + # `save()` at the last minute so that the pid will be correct even + # if the process forks. + dice = random.Random(os.urandom(8)).randint(0, 999999) + suffix = "%s.%s.%06d" % (socket.gethostname(), os.getpid(), dice) + return suffix + + class Hasher(object): """Hashes Python data into md5.""" def __init__(self): @@ -226,6 +230,7 @@ continue self.update(k) self.update(a) + self.md5.update(b'.') def hexdigest(self): """Retrieve the hex digest of the hash.""" @@ -249,14 +254,67 @@ ) -class SimpleRepr(object): - """A mixin implementing a simple __repr__.""" +class DefaultValue(object): + """A sentinel object to use for unusual default-value needs. + + Construct with a string that will be used as the repr, for display in help + and Sphinx output. + + """ + def __init__(self, display_as): + self.display_as = display_as + def __repr__(self): - return "<{klass} @{id:x} {attrs}>".format( - klass=self.__class__.__name__, - id=id(self) & 0xFFFFFF, - attrs=" ".join("{}={!r}".format(k, v) for k, v in self.__dict__.items()), - ) + return self.display_as + + +def substitute_variables(text, variables): + """Substitute ``${VAR}`` variables in `text` with their values. + + Variables in the text can take a number of shell-inspired forms:: + + $VAR + ${VAR} + ${VAR?} strict: an error if VAR isn't defined. + ${VAR-missing} defaulted: "missing" if VAR isn't defined. + $$ just a dollar sign. + + `variables` is a dictionary of variable values. + + Returns the resulting text with values substituted. + + """ + dollar_pattern = r"""(?x) # Use extended regex syntax + \$ # A dollar sign, + (?: # then + (?P<dollar>\$) | # a dollar sign, or + (?P<word1>\w+) | # a plain word, or + { # a {-wrapped + (?P<word2>\w+) # word, + (?: + (?P<strict>\?) | # with a strict marker + -(?P<defval>[^}]*) # or a default value + )? # maybe. + } + ) + """ + + def dollar_replace(match): + """Called for each $replacement.""" + # Only one of the groups will have matched, just get its text. + word = next(g for g in match.group('dollar', 'word1', 'word2') if g) + if word == "$": + return "$" + elif word in variables: + return variables[word] + elif match.group('strict'): + msg = "Variable {} is undefined: {!r}".format(word, text) + raise CoverageException(msg) + else: + return match.group('defval') + + text = re.sub(dollar_pattern, dollar_replace, text) + return text class BaseCoverageException(Exception): @@ -265,7 +323,7 @@ class CoverageException(BaseCoverageException): - """A run-of-the-mill exception specific to coverage.py.""" + """An exception raised by a coverage.py function.""" pass