summaryrefslogtreecommitdiff
path: root/coverage/misc.py
diff options
context:
space:
mode:
Diffstat (limited to 'coverage/misc.py')
-rw-r--r--coverage/misc.py105
1 files changed, 86 insertions, 19 deletions
diff --git a/coverage/misc.py b/coverage/misc.py
index b99fbb81..5d330c6d 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -1,32 +1,73 @@
-"""Miscellaneous stuff for Coverage."""
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
+
+"""Miscellaneous stuff for coverage.py."""
import errno
import hashlib
import inspect
+import locale
import os
+import sys
+import types
from coverage import env
-from coverage.backward import string_class, to_bytes, unicode_class
+from coverage.backward import to_bytes, unicode_class
+
+ISOLATED_MODULES = {}
+
+
+def isolate_module(mod):
+ """Copy a module so that we are isolated from aggressive mocking.
+
+ If a test suite mocks os.path.exists (for example), and then we need to use
+ it during the test, everything will get tangled up if we use their mock.
+ Making a copy of the module when we import it will isolate coverage.py from
+ those complications.
+ """
+ if mod not in ISOLATED_MODULES:
+ new_mod = types.ModuleType(mod.__name__)
+ ISOLATED_MODULES[mod] = new_mod
+ for name in dir(mod):
+ value = getattr(mod, name)
+ if isinstance(value, types.ModuleType):
+ value = isolate_module(value)
+ setattr(new_mod, name, value)
+ return ISOLATED_MODULES[mod]
+
+os = isolate_module(os)
# Use PyContracts for assertion testing on parameters and returns, but only if
# we are running our own test suite.
if env.TESTING:
from contracts import contract # pylint: disable=unused-import
- from contracts import new_contract
+ from contracts import new_contract as raw_new_contract
+
+ def new_contract(*args, **kwargs):
+ """A proxy for contracts.new_contract that doesn't mind happening twice."""
+ try:
+ return raw_new_contract(*args, **kwargs)
+ except ValueError:
+ # During meta-coverage, this module is imported twice, and
+ # PyContracts doesn't like redefining contracts. It's OK.
+ pass
# Define contract words that PyContract doesn't have.
new_contract('bytes', lambda v: isinstance(v, bytes))
if env.PY3:
new_contract('unicode', lambda v: isinstance(v, unicode_class))
-
-else:
+else: # pragma: not covered
# We aren't using real PyContracts, so just define a no-op decorator as a
# stunt double.
def contract(**unused):
"""Dummy no-op implementation of `contract`."""
return lambda func: func
+ def new_contract(*args_unused, **kwargs_unused):
+ """Dummy no-op implementation of `new_contract`."""
+ pass
+
def nice_pair(pair):
"""Make a nice string representation of a pair of numbers.
@@ -76,19 +117,24 @@ def format_lines(statements, lines):
def expensive(fn):
- """A decorator to cache the result of an expensive operation.
+ """A decorator to indicate that a method shouldn't be called more than once.
- Only applies to methods with no arguments.
+ Normally, this does nothing. During testing, this raises an exception if
+ called more than once.
"""
- attr = "_cache_" + fn.__name__
-
- def _wrapped(self):
- """Inner fn that checks the cache."""
- if not hasattr(self, attr):
- setattr(self, attr, fn(self))
- return getattr(self, attr)
- return _wrapped
+ if env.TESTING:
+ attr = "_once_" + fn.__name__
+
+ def _wrapped(self):
+ """Inner function that checks the cache."""
+ 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
+ else:
+ return fn # pragma: not covered
def bool_or_none(b):
@@ -113,6 +159,18 @@ def file_be_gone(path):
raise
+def output_encoding(outfile=None):
+ """Determine the encoding to use for output written to `outfile` or stdout."""
+ if outfile is None:
+ outfile = sys.stdout
+ encoding = (
+ getattr(outfile, "encoding", None) or
+ getattr(sys.__stdout__, "encoding", None) or
+ locale.getpreferredencoding()
+ )
+ return encoding
+
+
class Hasher(object):
"""Hashes Python data into md5."""
def __init__(self):
@@ -121,8 +179,8 @@ class Hasher(object):
def update(self, v):
"""Add `v` to the hash, recursively if needed."""
self.md5.update(to_bytes(str(type(v))))
- if isinstance(v, string_class):
- self.md5.update(to_bytes(v))
+ if isinstance(v, unicode_class):
+ self.md5.update(v.encode('utf8'))
elif isinstance(v, bytes):
self.md5.update(v)
elif v is None:
@@ -152,7 +210,6 @@ class Hasher(object):
return self.md5.hexdigest()
-# TODO: abc?
def _needs_to_implement(that, func_name):
"""Helper to raise NotImplementedError in interface stubs."""
if hasattr(that, "_coverage_plugin_name"):
@@ -170,8 +227,18 @@ def _needs_to_implement(that, func_name):
)
+class SimpleRepr(object):
+ """A mixin implementing a simple __repr__."""
+ 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()),
+ )
+
+
class CoverageException(Exception):
- """An exception specific to Coverage."""
+ """An exception specific to coverage.py."""
pass