summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.hgtags1
-rw-r--r--CHANGES.rst29
-rw-r--r--README.rst7
-rw-r--r--ci/download_appveyor.py4
-rw-r--r--coverage/control.py5
-rw-r--r--coverage/ctracer/tracer.c44
-rw-r--r--coverage/files.py34
-rw-r--r--coverage/misc.py14
-rw-r--r--coverage/parser.py4
-rw-r--r--coverage/phystokens.py30
-rw-r--r--coverage/python.py4
-rw-r--r--coverage/summary.py55
-rw-r--r--coverage/version.py2
-rw-r--r--coverage/xmlreport.py6
-rw-r--r--doc/changes.rst23
-rw-r--r--doc/conf.py2
-rw-r--r--doc/index.rst10
-rw-r--r--doc/sample_html/cogapp___init___py.html4
-rw-r--r--doc/sample_html/cogapp___main___py.html4
-rw-r--r--doc/sample_html/cogapp_backward_py.html4
-rw-r--r--doc/sample_html/cogapp_cogapp_py.html4
-rw-r--r--doc/sample_html/cogapp_makefiles_py.html4
-rw-r--r--doc/sample_html/cogapp_test_cogapp_py.html4
-rw-r--r--doc/sample_html/cogapp_test_makefiles_py.html4
-rw-r--r--doc/sample_html/cogapp_test_whiteutils_py.html4
-rw-r--r--doc/sample_html/cogapp_whiteutils_py.html4
-rw-r--r--doc/sample_html/index.html4
-rw-r--r--doc/sample_html/status.json2
-rw-r--r--howto.txt15
-rw-r--r--igor.py2
-rw-r--r--lab/bug397.py56
-rw-r--r--lab/cover-plugin.txt54
-rw-r--r--lab/parser.py4
-rw-r--r--lab/show_pyc.py87
-rw-r--r--requirements/dev.pip2
-rw-r--r--tests/coveragetest.py10
-rw-r--r--tests/helpers.py22
-rw-r--r--tests/test_files.py2
-rw-r--r--tests/test_oddball.py40
-rw-r--r--tests/test_phystokens.py30
-rw-r--r--tests/test_process.py77
-rw-r--r--tests/test_python.py2
-rw-r--r--tests/test_summary.py29
-rw-r--r--tox.ini12
44 files changed, 564 insertions, 196 deletions
diff --git a/.hgtags b/.hgtags
index df4642bb..61a3021a 100644
--- a/.hgtags
+++ b/.hgtags
@@ -47,3 +47,4 @@ eec8e928880df1beafdf7d4bea87f784375b35d7 coverage-4.0a3
82b0c5a85a7eb2156eebaa6b81b1f62fb4fe51b5 coverage-4.0b3
3c3e507a247eb35251083b9528a99e50831c960f coverage-4.0
8e727dc12de10fb8a302b04a8f2af3e00587889e coverage-4.0.1
+7428dab9307da4660878436fe71b696ca2048cf2 coverage-4.0.2
diff --git a/CHANGES.rst b/CHANGES.rst
index bc398530..a599352a 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -6,6 +6,34 @@ Change history for Coverage.py
==============================
+Version 4.0.2, in progress
+--------------------------
+
+- Fixed an unusual edge case of detecting source encodings, described in
+ `issue 443`_.
+
+.. _issue 443: https://bitbucket.org/ned/coveragepy/issues/443/coverage-gets-confused-when-encoding
+
+
+Version 4.0.2 --- 4 November 2015
+---------------------------------
+
+- More work on supporting unusually encoded source. Fixed `issue 431`_.
+
+- Files or directories with non-ASCII characters are now handled properly,
+ fixing `issue 432`_.
+
+- Setting a trace function with sys.settrace was broken by a change in 4.0.1,
+ as reported in `issue 436`_. This is now fixed.
+
+- Officially support PyPy 4.0, which required no changes, just updates to the
+ docs.
+
+.. _issue 431: https://bitbucket.org/ned/coveragepy/issues/431/couldnt-parse-python-file-with-cp1252
+.. _issue 432: https://bitbucket.org/ned/coveragepy/issues/432/path-with-unicode-characters-various
+.. _issue 436: https://bitbucket.org/ned/coveragepy/issues/436/disabled-coverage-ctracer-may-rise-from
+
+
Version 4.0.1 --- 13 October 2015
---------------------------------
@@ -553,7 +581,6 @@ Version 3.6b2 --- 23 December 2012
file name, coverage.py no longer associates it with the directory name,
fixing `issue 221`_.
-.. _issue 80: https://bitbucket.org/ned/coveragepy/issue/80/is-there-a-duck-typing-way-to-know-we-cant
.. _issue 221: https://bitbucket.org/ned/coveragepy/issue/221/coveragepy-incompatible-with-pyratemp
diff --git a/README.rst b/README.rst
index 4f11cb39..66a8f5e9 100644
--- a/README.rst
+++ b/README.rst
@@ -15,7 +15,8 @@ Coverage.py measures code coverage, typically during test execution. It uses
the code analysis tools and tracing hooks provided in the Python standard
library to determine which lines are executable, and which have been executed.
-Coverage.py runs on CPython 2.6, 2.7, 3.3, 3.4 or 3.5, PyPy 2.6, and PyPy3 2.4.
+Coverage.py runs on CPython 2.6, 2.7, 3.3, 3.4 and 3.5; PyPy 2.4, 2.6 and 4.0;
+and PyPy3 2.4.
Documentation is on `Read the Docs <http://coverage.readthedocs.org>`_.
Code repository and issue tracker are on `Bitbucket <http://bitbucket.org/ned/coveragepy>`_,
@@ -25,8 +26,8 @@ with a mirrored repository on `GitHub <https://github.com/nedbat/coveragepy>`_.
support, --skip-covered, HTML filtering, and more than 50 issues closed.
-Quick Start
------------
+Getting Started
+---------------
See the `quick start <http://coverage.readthedocs.org/#quick-start>`_
section of the docs.
diff --git a/ci/download_appveyor.py b/ci/download_appveyor.py
index a4ef8d53..f640b41a 100644
--- a/ci/download_appveyor.py
+++ b/ci/download_appveyor.py
@@ -17,7 +17,7 @@ def make_auth_headers():
token = f.read().strip()
headers = {
- 'Authorization': 'Bearer {}'.format(token),
+ 'Authorization': 'Bearer {0}'.format(token),
}
return headers
@@ -86,7 +86,7 @@ def unpack_zipfile(filename):
with open(filename, 'rb') as fzip:
z = zipfile.ZipFile(fzip)
for name in z.namelist():
- print " extracting {}".format(name)
+ print " extracting {0}".format(name)
ensure_dirs(name)
z.extract(name)
diff --git a/coverage/control.py b/coverage/control.py
index b7ba055d..0a5ccae6 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -1047,7 +1047,10 @@ class Coverage(object):
output_dir = os.path.dirname(self.config.xml_output)
if output_dir and not os.path.isdir(output_dir):
os.makedirs(output_dir)
- outfile = open(self.config.xml_output, "w")
+ open_kwargs = {}
+ if env.PY3:
+ open_kwargs['encoding'] = 'utf8'
+ outfile = open(self.config.xml_output, "w", **open_kwargs)
file_to_close = outfile
try:
reporter = XmlReporter(self, self.config)
diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c
index e5f39d09..dba8a11c 100644
--- a/coverage/ctracer/tracer.c
+++ b/coverage/ctracer/tracer.c
@@ -768,7 +768,7 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse
#endif
#if WHAT_LOG
- if (what <= sizeof(what_sym)/sizeof(const char *)) {
+ if (what <= (int)(sizeof(what_sym)/sizeof(const char *))) {
ascii = MyText_AS_BYTES(frame->f_code->co_filename);
printf("trace: %s @ %s %d\n", what_sym[what], MyBytes_AS_STRING(ascii), frame->f_lineno);
Py_DECREF(ascii);
@@ -859,6 +859,7 @@ CTracer_call(CTracer *self, PyObject *args, PyObject *kwds)
int what;
int orig_lineno;
PyObject *ret = NULL;
+ PyObject * ascii = NULL;
static char *what_names[] = {
"call", "exception", "line", "return",
@@ -866,10 +867,6 @@ CTracer_call(CTracer *self, PyObject *args, PyObject *kwds)
NULL
};
- #if WHAT_LOG
- printf("pytrace\n");
- #endif
-
static char *kwlist[] = {"frame", "event", "arg", "lineno", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!O!O|i:Tracer_call", kwlist,
@@ -880,14 +877,21 @@ CTracer_call(CTracer *self, PyObject *args, PyObject *kwds)
/* In Python, the what argument is a string, we need to find an int
for the C function. */
for (what = 0; what_names[what]; what++) {
- PyObject *ascii = MyText_AS_BYTES(what_str);
- int should_break = !strcmp(MyBytes_AS_STRING(ascii), what_names[what]);
+ int should_break;
+ ascii = MyText_AS_BYTES(what_str);
+ should_break = !strcmp(MyBytes_AS_STRING(ascii), what_names[what]);
Py_DECREF(ascii);
if (should_break) {
break;
}
}
+ #if WHAT_LOG
+ ascii = MyText_AS_BYTES(frame->f_code->co_filename);
+ printf("pytrace: %s @ %s %d\n", what_sym[what], MyBytes_AS_STRING(ascii), frame->f_lineno);
+ Py_DECREF(ascii);
+ #endif
+
/* Save off the frame's lineno, and use the forced one, if provided. */
orig_lineno = frame->f_lineno;
if (lineno > 0) {
@@ -904,8 +908,30 @@ CTracer_call(CTracer *self, PyObject *args, PyObject *kwds)
frame->f_lineno = orig_lineno;
/* For better speed, install ourselves the C way so that future calls go
- directly to CTracer_trace, without this intermediate function. */
- PyEval_SetTrace((Py_tracefunc)CTracer_trace, (PyObject*)self);
+ directly to CTracer_trace, without this intermediate function.
+
+ Only do this if this is a CALL event, since new trace functions only
+ take effect then. If we don't condition it on CALL, then we'll clobber
+ the new trace function before it has a chance to get called. To
+ understand why, there are three internal values to track: frame.f_trace,
+ c_tracefunc, and c_traceobj. They are explained here:
+ http://nedbatchelder.com/text/trace-function.html
+
+ Without the conditional on PyTrace_CALL, this is what happens:
+
+ def func(): # f_trace c_tracefunc c_traceobj
+ # -------------- -------------- --------------
+ # CTracer CTracer.trace CTracer
+ sys.settrace(my_func)
+ # CTracer trampoline my_func
+ # Now Python calls trampoline(CTracer), which calls this function
+ # which calls PyEval_SetTrace below, setting us as the tracer again:
+ # CTracer CTracer.trace CTracer
+ # and it's as if the settrace never happened.
+ */
+ if (what == PyTrace_CALL) {
+ PyEval_SetTrace((Py_tracefunc)CTracer_trace, (PyObject*)self);
+ }
done:
return ret;
diff --git a/coverage/files.py b/coverage/files.py
index 0b5651cb..44997d12 100644
--- a/coverage/files.py
+++ b/coverage/files.py
@@ -13,12 +13,9 @@ import sys
from coverage import env
from coverage.backward import unicode_class
-from coverage.misc import CoverageException, join_regex, isolate_module
+from coverage.misc import contract, CoverageException, join_regex, isolate_module
-RELATIVE_DIR = None
-CANONICAL_FILENAME_CACHE = {}
-
os = isolate_module(os)
@@ -33,10 +30,13 @@ def set_relative_directory():
# avoid duplicating work.
CANONICAL_FILENAME_CACHE = {}
+
def relative_directory():
"""Return the directory that `relative_filename` is relative to."""
return RELATIVE_DIR
+
+@contract(returns='unicode')
def relative_filename(filename):
"""Return the relative form of `filename`.
@@ -47,8 +47,10 @@ def relative_filename(filename):
fnorm = os.path.normcase(filename)
if fnorm.startswith(RELATIVE_DIR):
filename = filename[len(RELATIVE_DIR):]
- return filename
+ return unicode_filename(filename)
+
+@contract(returns='unicode')
def canonical_filename(filename):
"""Return a canonical file name for `filename`.
@@ -126,14 +128,36 @@ else:
return filename
+if env.PY2:
+ @contract(returns='unicode')
+ def unicode_filename(filename):
+ """Return a Unicode version of `filename`."""
+ if isinstance(filename, str):
+ encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
+ filename = filename.decode(encoding, "replace")
+ return filename
+else:
+ @contract(filename='unicode', returns='unicode')
+ def unicode_filename(filename):
+ """Return a Unicode version of `filename`."""
+ return filename
+
+
+@contract(returns='unicode')
def abs_file(filename):
"""Return the absolute normalized form of `filename`."""
path = os.path.expandvars(os.path.expanduser(filename))
path = os.path.abspath(os.path.realpath(path))
path = actual_path(path)
+ path = unicode_filename(path)
return path
+RELATIVE_DIR = None
+CANONICAL_FILENAME_CACHE = None
+set_relative_directory()
+
+
def isabs_anywhere(filename):
"""Is `filename` an absolute path on any OS?"""
return ntpath.isabs(filename) or posixpath.isabs(filename)
diff --git a/coverage/misc.py b/coverage/misc.py
index 36e4fe9c..db6298b6 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -6,7 +6,9 @@
import errno
import hashlib
import inspect
+import locale
import os
+import sys
import types
from coverage import env
@@ -149,6 +151,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):
diff --git a/coverage/parser.py b/coverage/parser.py
index 882c972b..a5e96237 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -211,7 +211,7 @@ class PythonParser(object):
else:
lineno = err.args[1][0] # TokenError
raise NotPython(
- "Couldn't parse '%s' as Python source: '%s' at line %d" % (
+ u"Couldn't parse '%s' as Python source: '%s' at line %d" % (
self.filename, err.args[0], lineno
)
)
@@ -338,7 +338,7 @@ class ByteParser(object):
self.code = compile_unicode(text, filename, "exec")
except SyntaxError as synerr:
raise NotPython(
- "Couldn't parse '%s' as Python source: '%s' at line %d" % (
+ u"Couldn't parse '%s' as Python source: '%s' at line %d" % (
filename, synerr.msg, synerr.lineno
)
)
diff --git a/coverage/phystokens.py b/coverage/phystokens.py
index 7092d39e..b34b1c3b 100644
--- a/coverage/phystokens.py
+++ b/coverage/phystokens.py
@@ -6,6 +6,7 @@
import codecs
import keyword
import re
+import sys
import token
import tokenize
@@ -152,7 +153,7 @@ class CachedTokenizer(object):
generate_tokens = CachedTokenizer().generate_tokens
-COOKIE_RE = re.compile(r"^\s*#.*coding[:=]\s*([-\w.]+)", flags=re.MULTILINE)
+COOKIE_RE = re.compile(r"^[ \t]*#.*coding[:=][ \t]*([-\w.]+)", flags=re.MULTILINE)
@contract(source='bytes')
def _source_encoding_py2(source):
@@ -274,30 +275,21 @@ def compile_unicode(source, filename, mode):
Python 2's compile() builtin has a stupid restriction: if the source string
is Unicode, then it may not have a encoding declaration in it. Why not?
- Who knows!
+ Who knows! It also decodes to utf8, and then tries to interpret those utf8
+ bytes according to the encoding declaration. Why? Who knows!
- This function catches that exception, neuters the coding declaration, and
- compiles it anyway.
+ This function neuters the coding declaration, and compiles it.
"""
- try:
- code = compile(source, filename, mode)
- except SyntaxError as synerr:
- if "coding declaration in unicode string" not in synerr.args[0].lower():
- raise
- source = neuter_encoding_declaration(source)
- code = compile(source, filename, mode)
-
+ source = neuter_encoding_declaration(source)
+ if env.PY2 and isinstance(filename, unicode):
+ filename = filename.encode(sys.getfilesystemencoding(), "replace")
+ code = compile(source, filename, mode)
return code
@contract(source='unicode', returns='unicode')
def neuter_encoding_declaration(source):
- """Return `source`, with any encoding declaration neutered.
-
- This function will only ever be called on `source` that has an encoding
- declaration, so some edge cases can be ignored.
-
- """
- source = COOKIE_RE.sub("# (deleted declaration)", source)
+ """Return `source`, with any encoding declaration neutered."""
+ source = COOKIE_RE.sub("# (deleted declaration)", source, count=1)
return source
diff --git a/coverage/python.py b/coverage/python.py
index 71b50f0c..4f589735 100644
--- a/coverage/python.py
+++ b/coverage/python.py
@@ -95,6 +95,8 @@ class PythonFileReporter(FileReporter):
else:
filename = morf
+ filename = files.unicode_filename(filename)
+
# .pyc files should always refer to a .py instead.
if filename.endswith(('.pyc', '.pyo')):
filename = filename[:-1]
@@ -106,6 +108,7 @@ class PythonFileReporter(FileReporter):
if hasattr(morf, '__name__'):
name = morf.__name__
name = name.replace(".", os.sep) + ".py"
+ name = files.unicode_filename(name)
else:
name = files.relative_filename(filename)
self.relname = name
@@ -115,6 +118,7 @@ class PythonFileReporter(FileReporter):
self._statements = None
self._excluded = None
+ @contract(returns='unicode')
def relative_filename(self):
return self.relname
diff --git a/coverage/summary.py b/coverage/summary.py
index 4dcaa735..5ddbb380 100644
--- a/coverage/summary.py
+++ b/coverage/summary.py
@@ -5,9 +5,10 @@
import sys
+from coverage import env
from coverage.report import Reporter
from coverage.results import Numbers
-from coverage.misc import NotPython, CoverageException
+from coverage.misc import NotPython, CoverageException, output_encoding
class SummaryReporter(Reporter):
@@ -20,38 +21,44 @@ class SummaryReporter(Reporter):
def report(self, morfs, outfile=None):
"""Writes a report summarizing coverage statistics per module.
- `outfile` is a file object to write the summary to.
+ `outfile` is a file object to write the summary to. It must be opened
+ for native strings (bytes on Python 2, Unicode on Python 3).
"""
self.find_file_reporters(morfs)
# Prepare the formatting strings
max_name = max([len(fr.relative_filename()) for fr in self.file_reporters] + [5])
- fmt_name = "%%- %ds " % max_name
- fmt_err = "%s %s: %s\n"
- fmt_skip_covered = "\n%s file%s skipped due to complete coverage.\n"
+ fmt_name = u"%%- %ds " % max_name
+ fmt_err = u"%s %s: %s\n"
+ fmt_skip_covered = u"\n%s file%s skipped due to complete coverage.\n"
- header = (fmt_name % "Name") + " Stmts Miss"
- fmt_coverage = fmt_name + "%6d %6d"
+ header = (fmt_name % "Name") + u" Stmts Miss"
+ fmt_coverage = fmt_name + u"%6d %6d"
if self.branches:
- header += " Branch BrPart"
- fmt_coverage += " %6d %6d"
+ header += u" Branch BrPart"
+ fmt_coverage += u" %6d %6d"
width100 = Numbers.pc_str_width()
- header += "%*s" % (width100+4, "Cover")
- fmt_coverage += "%%%ds%%%%" % (width100+3,)
+ header += u"%*s" % (width100+4, "Cover")
+ fmt_coverage += u"%%%ds%%%%" % (width100+3,)
if self.config.show_missing:
- header += " Missing"
- fmt_coverage += " %s"
- rule = "-" * len(header) + "\n"
- header += "\n"
- fmt_coverage += "\n"
+ header += u" Missing"
+ fmt_coverage += u" %s"
+ rule = u"-" * len(header) + u"\n"
+ header += u"\n"
+ fmt_coverage += u"\n"
- if not outfile:
+ if outfile is None:
outfile = sys.stdout
+ if env.PY2:
+ writeout = lambda u: outfile.write(u.encode(output_encoding()))
+ else:
+ writeout = outfile.write
+
# Write the header
- outfile.write(header)
- outfile.write(rule)
+ writeout(header)
+ writeout(rule)
total = Numbers()
skipped_count = 0
@@ -83,7 +90,7 @@ class SummaryReporter(Reporter):
missing_fmtd += ", "
missing_fmtd += branches_fmtd
args += (missing_fmtd,)
- outfile.write(fmt_coverage % args)
+ writeout(fmt_coverage % args)
except Exception:
report_it = not self.config.ignore_errors
if report_it:
@@ -93,22 +100,22 @@ class SummaryReporter(Reporter):
if typ is NotPython and not fr.should_be_python():
report_it = False
if report_it:
- outfile.write(fmt_err % (fr.relative_filename(), typ.__name__, msg))
+ writeout(fmt_err % (fr.relative_filename(), typ.__name__, msg))
if total.n_files > 1:
- outfile.write(rule)
+ writeout(rule)
args = ("TOTAL", total.n_statements, total.n_missing)
if self.branches:
args += (total.n_branches, total.n_partial_branches)
args += (total.pc_covered_str,)
if self.config.show_missing:
args += ("",)
- outfile.write(fmt_coverage % args)
+ writeout(fmt_coverage % args)
if not total.n_files and not skipped_count:
raise CoverageException("No data to report.")
if self.config.skip_covered and skipped_count:
- outfile.write(fmt_skip_covered % (skipped_count, 's' if skipped_count > 1 else ''))
+ writeout(fmt_skip_covered % (skipped_count, 's' if skipped_count > 1 else ''))
return total.n_statements and total.pc_covered
diff --git a/coverage/version.py b/coverage/version.py
index d1da40bb..98973192 100644
--- a/coverage/version.py
+++ b/coverage/version.py
@@ -5,7 +5,7 @@
# This file is exec'ed in setup.py, don't import anything!
# Same semantics as sys.version_info.
-version_info = (4, 0, 2, 'alpha', 1)
+version_info = (4, 0, 3, 'final', 0)
def _make_version(major, minor, micro, releaselevel, serial):
diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py
index b8f8a9e4..d7c2f44a 100644
--- a/coverage/xmlreport.py
+++ b/coverage/xmlreport.py
@@ -8,6 +8,7 @@ import sys
import time
import xml.dom.minidom
+from coverage import env
from coverage import __url__, __version__, files
from coverage.misc import isolate_module
from coverage.report import Reporter
@@ -116,7 +117,10 @@ class XmlReporter(Reporter):
xcoverage.setAttribute("branch-rate", branch_rate)
# Use the DOM to write the output file.
- outfile.write(self.xml_out.toprettyxml())
+ out = self.xml_out.toprettyxml()
+ if env.PY2:
+ out = out.encode("utf8")
+ outfile.write(out)
# Return the total percentage.
denom = lnum_tot + bnum_tot
diff --git a/doc/changes.rst b/doc/changes.rst
index 5731d3c1..c87ffba4 100644
--- a/doc/changes.rst
+++ b/doc/changes.rst
@@ -35,6 +35,7 @@ Major change history for coverage.py
.. :history: 20150822T092800, updated for 4.0b2
.. :history: 20150919T072700, updated for 4.0
.. :history: 20151013T103000, updated for 4.0.1
+.. :history: 20151104T050900, updated for 4.0.2
These are the major changes for coverage.py. For a more complete change
@@ -44,6 +45,28 @@ history, see the `CHANGES.rst`_ file in the source tree.
.. module:: coverage
+
+.. _changes_402:
+
+Version 4.0.2 --- 4 November 2015
+---------------------------------
+
+- More work on supporting unusually encoded source. Fixed `issue 431`_.
+
+- Files or directories with non-ASCII characters are now handled properly,
+ fixing `issue 432`_.
+
+- Setting a trace function with sys.settrace was broken by a change in 4.0.1,
+ as reported in `issue 436`_. This is now fixed.
+
+- Officially support PyPy 4.0, which required no changes, just updates to the
+ docs.
+
+.. _issue 431: https://bitbucket.org/ned/coveragepy/issues/431/couldnt-parse-python-file-with-cp1252
+.. _issue 432: https://bitbucket.org/ned/coveragepy/issues/432/path-with-unicode-characters-various
+.. _issue 436: https://bitbucket.org/ned/coveragepy/issues/436/disabled-coverage-ctracer-may-rise-from
+
+
.. _changes_401:
Version 4.0.1 --- 13 October 2015
diff --git a/doc/conf.py b/doc/conf.py
index be00403a..f6cbf663 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -58,7 +58,7 @@ copyright = u'2009\N{EN DASH}2015, Ned Batchelder'
# The short X.Y version.
version = '4.0'
# The full version, including alpha/beta/rc tags.
-release = '4.0.1'
+release = '4.0.2'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/doc/index.rst b/doc/index.rst
index 725db71f..119fa7a3 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -41,6 +41,7 @@ Coverage.py
.. :history: 20150822T092900, Updated for 4.0b2
.. :history: 20150918T072700, Updated for 4.0
.. :history: 20151013T103200, Updated for 4.0.1
+.. :history: 20151104T050900, updated for 4.0.2
Coverage.py is a tool for measuring code coverage of Python programs. It
@@ -53,9 +54,12 @@ not.
.. ifconfig:: not prerelease
- The latest version is coverage.py 4.0.1, released 13 October 2015.
- It is supported on Python versions 2.6, 2.7, 3.3, 3.4, and 3.5, as well
- as PyPy 2.4 and 2.6, and PyPy3 2.4.
+ The latest version is coverage.py 4.0.2, released 4 November 2015.
+ It is supported on:
+
+ * Python versions 2.6, 2.7, 3.3, 3.4, and 3.5
+ * PyPy 2.4, 2.6, and 4.0
+ * PyPy3 2.4
.. ifconfig:: prerelease
diff --git a/doc/sample_html/cogapp___init___py.html b/doc/sample_html/cogapp___init___py.html
index c05b2e2a..230f2166 100644
--- a/doc/sample_html/cogapp___init___py.html
+++ b/doc/sample_html/cogapp___init___py.html
@@ -99,8 +99,8 @@
<div id="footer">
<div class="content">
<p>
- <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.1</a>,
- created at 2015-10-13 10:50
+ <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.2</a>,
+ created at 2015-11-04 05:20
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp___main___py.html b/doc/sample_html/cogapp___main___py.html
index 92fbe0d2..2f1d82c0 100644
--- a/doc/sample_html/cogapp___main___py.html
+++ b/doc/sample_html/cogapp___main___py.html
@@ -91,8 +91,8 @@
<div id="footer">
<div class="content">
<p>
- <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.1</a>,
- created at 2015-10-13 10:50
+ <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.2</a>,
+ created at 2015-11-04 05:20
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp_backward_py.html b/doc/sample_html/cogapp_backward_py.html
index 7dfc051d..eda8764f 100644
--- a/doc/sample_html/cogapp_backward_py.html
+++ b/doc/sample_html/cogapp_backward_py.html
@@ -133,8 +133,8 @@
<div id="footer">
<div class="content">
<p>
- <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.1</a>,
- created at 2015-10-13 10:50
+ <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.2</a>,
+ created at 2015-11-04 05:20
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp_cogapp_py.html b/doc/sample_html/cogapp_cogapp_py.html
index e4e0fd04..c7b99af3 100644
--- a/doc/sample_html/cogapp_cogapp_py.html
+++ b/doc/sample_html/cogapp_cogapp_py.html
@@ -1631,8 +1631,8 @@
<div id="footer">
<div class="content">
<p>
- <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.1</a>,
- created at 2015-10-13 10:50
+ <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.2</a>,
+ created at 2015-11-04 05:20
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp_makefiles_py.html b/doc/sample_html/cogapp_makefiles_py.html
index 7b38133f..a2261347 100644
--- a/doc/sample_html/cogapp_makefiles_py.html
+++ b/doc/sample_html/cogapp_makefiles_py.html
@@ -207,8 +207,8 @@
<div id="footer">
<div class="content">
<p>
- <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.1</a>,
- created at 2015-10-13 10:50
+ <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.2</a>,
+ created at 2015-11-04 05:20
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp_test_cogapp_py.html b/doc/sample_html/cogapp_test_cogapp_py.html
index 0327d022..6b5c5709 100644
--- a/doc/sample_html/cogapp_test_cogapp_py.html
+++ b/doc/sample_html/cogapp_test_cogapp_py.html
@@ -4525,8 +4525,8 @@
<div id="footer">
<div class="content">
<p>
- <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.1</a>,
- created at 2015-10-13 10:50
+ <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.2</a>,
+ created at 2015-11-04 05:20
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp_test_makefiles_py.html b/doc/sample_html/cogapp_test_makefiles_py.html
index 15ec673a..cb7c7c40 100644
--- a/doc/sample_html/cogapp_test_makefiles_py.html
+++ b/doc/sample_html/cogapp_test_makefiles_py.html
@@ -265,8 +265,8 @@
<div id="footer">
<div class="content">
<p>
- <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.1</a>,
- created at 2015-10-13 10:50
+ <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.2</a>,
+ created at 2015-11-04 05:20
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp_test_whiteutils_py.html b/doc/sample_html/cogapp_test_whiteutils_py.html
index af4c8cf8..bf75896a 100644
--- a/doc/sample_html/cogapp_test_whiteutils_py.html
+++ b/doc/sample_html/cogapp_test_whiteutils_py.html
@@ -285,8 +285,8 @@
<div id="footer">
<div class="content">
<p>
- <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.1</a>,
- created at 2015-10-13 10:50
+ <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.2</a>,
+ created at 2015-11-04 05:20
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp_whiteutils_py.html b/doc/sample_html/cogapp_whiteutils_py.html
index 02f34d7e..a939299d 100644
--- a/doc/sample_html/cogapp_whiteutils_py.html
+++ b/doc/sample_html/cogapp_whiteutils_py.html
@@ -223,8 +223,8 @@
<div id="footer">
<div class="content">
<p>
- <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.1</a>,
- created at 2015-10-13 10:50
+ <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.2</a>,
+ created at 2015-11-04 05:20
</p>
</div>
</div>
diff --git a/doc/sample_html/index.html b/doc/sample_html/index.html
index ad8bcfdd..4f920b93 100644
--- a/doc/sample_html/index.html
+++ b/doc/sample_html/index.html
@@ -202,8 +202,8 @@
<div id="footer">
<div class="content">
<p>
- <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.1</a>,
- created at 2015-10-13 10:50
+ <a class="nav" href="https://coverage.readthedocs.org">coverage.py v4.0.2</a>,
+ created at 2015-11-04 05:20
</p>
</div>
</div>
diff --git a/doc/sample_html/status.json b/doc/sample_html/status.json
index a0cad73e..614f7b11 100644
--- a/doc/sample_html/status.json
+++ b/doc/sample_html/status.json
@@ -1 +1 @@
-{"files": {"cogapp_test_whiteutils_py": {"index": {"relative_filename": "cogapp/test_whiteutils.py", "html_filename": "cogapp_test_whiteutils_py.html", "nums": [1, 69, 0, 69, 0, 0, 0]}, "hash": "f7a3c04788858b652fcfb29825e1e8d4"}, "cogapp_test_makefiles_py": {"index": {"relative_filename": "cogapp/test_makefiles.py", "html_filename": "cogapp_test_makefiles_py.html", "nums": [1, 55, 0, 55, 6, 0, 6]}, "hash": "ff1f44c04d08ae202f5164e1ba75818e"}, "cogapp_cogapp_py": {"index": {"relative_filename": "cogapp/cogapp.py", "html_filename": "cogapp_cogapp_py.html", "nums": [1, 423, 4, 3, 168, 4, 4]}, "hash": "4c6c4b843da7cd91e690535daa5192c1"}, "cogapp___init___py": {"index": {"relative_filename": "cogapp/__init__.py", "html_filename": "cogapp___init___py.html", "nums": [1, 2, 0, 0, 0, 0, 0]}, "hash": "e8e5f4601febeb4f8cfaedc27cda7ff2"}, "cogapp_backward_py": {"index": {"relative_filename": "cogapp/backward.py", "html_filename": "cogapp_backward_py.html", "nums": [1, 19, 0, 8, 2, 1, 1]}, "hash": "02fc7e84b678eb8cd2df594ecd21792d"}, "cogapp_test_cogapp_py": {"index": {"relative_filename": "cogapp/test_cogapp.py", "html_filename": "cogapp_test_cogapp_py.html", "nums": [1, 704, 6, 0, 6, 1, 1]}, "hash": "8042afebdffb470d9119c676064ad32c"}, "cogapp_makefiles_py": {"index": {"relative_filename": "cogapp/makefiles.py", "html_filename": "cogapp_makefiles_py.html", "nums": [1, 28, 3, 7, 14, 2, 8]}, "hash": "4344b02848c1c9dd0a59c3196640dde0"}, "cogapp_whiteutils_py": {"index": {"relative_filename": "cogapp/whiteutils.py", "html_filename": "cogapp_whiteutils_py.html", "nums": [1, 45, 0, 3, 32, 3, 3]}, "hash": "b898c54cd626b6265d68d6e536ad78cf"}, "cogapp___main___py": {"index": {"relative_filename": "cogapp/__main__.py", "html_filename": "cogapp___main___py.html", "nums": [1, 3, 0, 3, 0, 0, 0]}, "hash": "c846304fff9f9b5f7510a86b60c3c3c6"}}, "version": "4.0.1", "settings": "7d07facd0b8027d5d394bf363d405670", "format": 1} \ No newline at end of file
+{"files": {"cogapp_test_whiteutils_py": {"index": {"relative_filename": "cogapp/test_whiteutils.py", "html_filename": "cogapp_test_whiteutils_py.html", "nums": [1, 69, 0, 69, 0, 0, 0]}, "hash": "f7a3c04788858b652fcfb29825e1e8d4"}, "cogapp_test_makefiles_py": {"index": {"relative_filename": "cogapp/test_makefiles.py", "html_filename": "cogapp_test_makefiles_py.html", "nums": [1, 55, 0, 55, 6, 0, 6]}, "hash": "ff1f44c04d08ae202f5164e1ba75818e"}, "cogapp_cogapp_py": {"index": {"relative_filename": "cogapp/cogapp.py", "html_filename": "cogapp_cogapp_py.html", "nums": [1, 423, 4, 3, 168, 4, 4]}, "hash": "4c6c4b843da7cd91e690535daa5192c1"}, "cogapp___init___py": {"index": {"relative_filename": "cogapp/__init__.py", "html_filename": "cogapp___init___py.html", "nums": [1, 2, 0, 0, 0, 0, 0]}, "hash": "e8e5f4601febeb4f8cfaedc27cda7ff2"}, "cogapp_backward_py": {"index": {"relative_filename": "cogapp/backward.py", "html_filename": "cogapp_backward_py.html", "nums": [1, 19, 0, 8, 2, 1, 1]}, "hash": "02fc7e84b678eb8cd2df594ecd21792d"}, "cogapp_test_cogapp_py": {"index": {"relative_filename": "cogapp/test_cogapp.py", "html_filename": "cogapp_test_cogapp_py.html", "nums": [1, 704, 6, 0, 6, 1, 1]}, "hash": "8042afebdffb470d9119c676064ad32c"}, "cogapp_makefiles_py": {"index": {"relative_filename": "cogapp/makefiles.py", "html_filename": "cogapp_makefiles_py.html", "nums": [1, 28, 3, 7, 14, 2, 8]}, "hash": "4344b02848c1c9dd0a59c3196640dde0"}, "cogapp_whiteutils_py": {"index": {"relative_filename": "cogapp/whiteutils.py", "html_filename": "cogapp_whiteutils_py.html", "nums": [1, 45, 0, 3, 32, 3, 3]}, "hash": "b898c54cd626b6265d68d6e536ad78cf"}, "cogapp___main___py": {"index": {"relative_filename": "cogapp/__main__.py", "html_filename": "cogapp___main___py.html", "nums": [1, 3, 0, 3, 0, 0, 0]}, "hash": "c846304fff9f9b5f7510a86b60c3c3c6"}}, "version": "4.0.2", "settings": "7d07facd0b8027d5d394bf363d405670", "format": 1} \ No newline at end of file
diff --git a/howto.txt b/howto.txt
index 225cf875..2686283b 100644
--- a/howto.txt
+++ b/howto.txt
@@ -6,7 +6,10 @@
- Mac
- Pythons 2.6, 2.7, 3.3, 3.4, 3.5
- Version number in coverage/version.py
- - 3.1a1, 3.1b1, 3.1c1, 3.1
+ version_info = (4, 0, 2, 'alpha', 1)
+ version_info = (4, 0, 2, 'beta', 1)
+ version_info = (4, 0, 2, 'candidate', 1)
+ version_info = (4, 0, 2, 'final', 0)
- Python version number in classifiers in setup.py
- Copyright date in NOTICE.txt
- Update CHANGES.rst, including release date.
@@ -16,7 +19,7 @@
- Version and date in doc/index.rst
- Version and copyright date in doc/conf.py
- Don't forget the man page: doc/python-coverage.1.txt
- - done with changes to source files, check them in.
+ - Done with changes to source files, check them in.
- Generate new sample_html to get the latest, incl footer version number:
pip install -e .
cd ~/cog/trunk
@@ -30,6 +33,8 @@
- ELSE:
rm -f ~/coverage/trunk/doc/sample_html/*.*
cp -r htmlcov/ ~/coverage/trunk/doc/sample_html/
+ cd ~/coverage/trunk
+ check in the new sample html
- IF BETA:
- Build and publish docs:
$ make publishbeta
@@ -43,7 +48,7 @@
- $ make wheel
- Windows kits
- $ hg push
- - wait about 35 minutes for Appveyor to build kits.
+ - wait about an hour for Appveyor to build kits.
- $ make download_appveyor
- examine the dist directory, and remove anything that looks malformed.
- Update PyPi:
@@ -57,11 +62,11 @@
- Update nedbatchelder.com
- Blog post?
- Update readthedocs
- - Coverage / versions
+ - visit https://readthedocs.org/projects/coverage/versions/
- find the latest tag in the inactive list, edit it, make it active.
- IF NOT BETA:
- visit https://readthedocs.org/dashboard/coverage/advanced/
- - change the default version
+ - change the default version to the new version
- Update bitbucket:
- Issue tracker should get new version number in picker.
# Note: don't delete old version numbers: it marks changes on the tickets
diff --git a/igor.py b/igor.py
index 4ef952b3..409fdc91 100644
--- a/igor.py
+++ b/igor.py
@@ -214,11 +214,13 @@ def do_zip_mods():
assert [ord(c) for c in text] == ords
print(u"All OK with {encoding}")
""")
+ # These encodings should match the list in tests/test_python.py
details = [
(u'utf8', u'ⓗⓔⓛⓛⓞ, ⓦⓞⓡⓛⓓ'),
(u'gb2312', u'你好,世界'),
(u'hebrew', u'שלום, עולם'),
(u'shift_jis', u'こんにちは世界'),
+ (u'cp1252', u'“hi”'),
]
for encoding, text in details:
filename = 'encoded_{0}.py'.format(encoding)
diff --git a/lab/bug397.py b/lab/bug397.py
new file mode 100644
index 00000000..4d72e908
--- /dev/null
+++ b/lab/bug397.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+"""
+Run this file two ways under coverage and see that the times are the same:
+
+ $ coverage run lab/bug397.py slow
+ Runtime per example: 130.96 +/- 3.70 us
+ $ coverage run lab/bug397.py fast
+ Runtime per example: 131.34 +/- 4.48 us
+
+Written by David MacIver as part of
+https://bitbucket.org/ned/coveragepy/issues/397/stopping-and-resuming-coverage-with
+
+"""
+from __future__ import print_function
+
+import sys
+import random
+import time
+import math
+
+if sys.argv[1] == "slow":
+ sys.settrace(sys.gettrace())
+
+random.seed(1)
+
+
+def hash_str(s):
+ h = 0
+ for c in s:
+ h = (h * 31 + ord(c)) & (2 ** 64 - 1)
+ return h
+
+data = [
+ hex(random.getrandbits(1024)) for _ in range(500)
+]
+
+N_SAMPLES = 100
+
+
+def mean(xs):
+ xs = list(xs)
+ return sum(xs) / len(xs)
+
+
+def sd(xs):
+ return math.sqrt(mean(x ** 2 for x in xs) - mean(xs) ** 2)
+
+
+if __name__ == '__main__':
+ timing = []
+ for _ in range(N_SAMPLES):
+ start = time.time()
+ for d in data:
+ hash_str(d)
+ timing.append(1000000 * (time.time() - start) / len(data))
+ print("Runtime per example:", "%.2f +/- %.2f us" % (mean(timing), sd(timing)))
diff --git a/lab/cover-plugin.txt b/lab/cover-plugin.txt
deleted file mode 100644
index 927fff81..00000000
--- a/lab/cover-plugin.txt
+++ /dev/null
@@ -1,54 +0,0 @@
-== nose cover plugin flow ==
-
-- configure:
- set self.coverPackages: list of names of packages
-
-- begin:
- self.skipModules = sys.modules.keys()[:]
- set coverage.exclude (why?)
- coverage.start
-
-
-- report:
- coverage.stop()
- modules = [ module
- for name, module in sys.modules.items()
- if self.wantModuleCoverage(name, module) ]
- coverage.report(modules)
-
-- wantModuleCoverage(name, module):
- if self.coverPackages:
- for package in self.coverPackages:
- want_it = False
- if name.startswith(package):
- if self.coverTests:
- want_it = True
- else:
- want_it = not self.conf.testMatch.search(name)
- if want_it:
- return True
- if name in self.skipModules:
- return False
-
- if self.conf.testMatch.search(name) and not self.coverTests:
- return False
-
- return not self.coverPackages
-
-- wantFile:
-
-
-
-
-source, include, omit:
-
- # self.source is a list of canonical directories for the packages.
- # canon_dir is the canonical directory containing the source file.
-
- if self.source:
- for s in self.source:
- if is_contained(s, canon_dir):
- break
- else:
- # This file wasn't in any source.
- return False
diff --git a/lab/parser.py b/lab/parser.py
index 1343f4ce..97c81d89 100644
--- a/lab/parser.py
+++ b/lab/parser.py
@@ -65,9 +65,9 @@ class ParserMain(object):
if options.histogram:
total = sum(opcode_counts.values())
- print("{} total opcodes".format(total))
+ print("{0} total opcodes".format(total))
for opcode, number in opcode_counts.most_common():
- print("{:20s} {:6d} {:.1%}".format(opcode, number, number/total))
+ print("{0:20s} {1:6d} {2:.1%}".format(opcode, number, number/total))
def one_file(self, options, filename):
diff --git a/lab/show_pyc.py b/lab/show_pyc.py
index 147c6ff8..4eaa5131 100644
--- a/lab/show_pyc.py
+++ b/lab/show_pyc.py
@@ -1,15 +1,22 @@
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
-import dis, marshal, struct, sys, time, types
+import binascii
+import dis
+import marshal
+import struct
+import sys
+import time
+import types
+
def show_pyc_file(fname):
f = open(fname, "rb")
magic = f.read(4)
moddate = f.read(4)
modtime = time.asctime(time.localtime(struct.unpack('<L', moddate)[0]))
- print "magic %s" % (magic.encode('hex'))
- print "moddate %s (%s)" % (moddate.encode('hex'), modtime)
+ print("magic %s" % (binascii.hexlify(magic)))
+ print("moddate %s (%s)" % (binascii.hexlify(moddate), modtime))
code = marshal.load(f)
show_code(code)
@@ -21,38 +28,68 @@ def show_py_text(text, fname="<string>"):
code = compile(text, fname, "exec")
show_code(code)
-def show_code(code, indent=''):
- print "%scode" % indent
+CO_FLAGS = [
+ ('CO_OPTIMIZED', 0x00001),
+ ('CO_NEWLOCALS', 0x00002),
+ ('CO_VARARGS', 0x00004),
+ ('CO_VARKEYWORDS', 0x00008),
+ ('CO_NESTED', 0x00010),
+ ('CO_GENERATOR', 0x00020),
+ ('CO_NOFREE', 0x00040),
+ ('CO_COROUTINE', 0x00080),
+ ('CO_ITERABLE_COROUTINE', 0x00100),
+ ('CO_GENERATOR_ALLOWED', 0x01000),
+ ('CO_FUTURE_DIVISION', 0x02000),
+ ('CO_FUTURE_ABSOLUTE_IMPORT', 0x04000),
+ ('CO_FUTURE_WITH_STATEMENT', 0x08000),
+ ('CO_FUTURE_PRINT_FUNCTION', 0x10000),
+ ('CO_FUTURE_UNICODE_LITERALS', 0x20000),
+ ('CO_FUTURE_BARRY_AS_BDFL', 0x40000),
+ ('CO_FUTURE_GENERATOR_STOP', 0x80000),
+]
+
+def show_code(code, indent='', number=None):
+ label = ""
+ if number is not None:
+ label = "%d: " % number
+ print("%s%scode" % (indent, label))
indent += ' '
- print "%sargcount %d" % (indent, code.co_argcount)
- print "%snlocals %d" % (indent, code.co_nlocals)
- print "%sstacksize %d" % (indent, code.co_stacksize)
- print "%sflags %04x" % (indent, code.co_flags)
+ print("%sname %r" % (indent, code.co_name))
+ print("%sargcount %d" % (indent, code.co_argcount))
+ print("%snlocals %d" % (indent, code.co_nlocals))
+ print("%sstacksize %d" % (indent, code.co_stacksize))
+ print("%sflags %04x: %s" % (indent, code.co_flags, flag_words(code.co_flags, CO_FLAGS)))
show_hex("code", code.co_code, indent=indent)
dis.disassemble(code)
- print "%sconsts" % indent
- for const in code.co_consts:
+ print("%sconsts" % indent)
+ for i, const in enumerate(code.co_consts):
if type(const) == types.CodeType:
- show_code(const, indent+' ')
+ show_code(const, indent+' ', number=i)
else:
- print " %s%r" % (indent, const)
- print "%snames %r" % (indent, code.co_names)
- print "%svarnames %r" % (indent, code.co_varnames)
- print "%sfreevars %r" % (indent, code.co_freevars)
- print "%scellvars %r" % (indent, code.co_cellvars)
- print "%sfilename %r" % (indent, code.co_filename)
- print "%sname %r" % (indent, code.co_name)
- print "%sfirstlineno %d" % (indent, code.co_firstlineno)
+ print(" %s%d: %r" % (indent, i, const))
+ print("%snames %r" % (indent, code.co_names))
+ print("%svarnames %r" % (indent, code.co_varnames))
+ print("%sfreevars %r" % (indent, code.co_freevars))
+ print("%scellvars %r" % (indent, code.co_cellvars))
+ print("%sfilename %r" % (indent, code.co_filename))
+ print("%sfirstlineno %d" % (indent, code.co_firstlineno))
show_hex("lnotab", code.co_lnotab, indent=indent)
def show_hex(label, h, indent):
- h = h.encode('hex')
+ h = binascii.hexlify(h)
if len(h) < 60:
- print "%s%s %s" % (indent, label, h)
+ print("%s%s %s" % (indent, label, h.decode('ascii')))
else:
- print "%s%s" % (indent, label)
+ print("%s%s" % (indent, label))
for i in range(0, len(h), 60):
- print "%s %s" % (indent, h[i:i+60])
+ print("%s %s" % (indent, h[i:i+60].decode('ascii')))
+
+def flag_words(flags, flag_defs):
+ words = []
+ for word, flag in flag_defs:
+ if flag & flags:
+ words.append(word)
+ return ", ".join(words)
def show_file(fname):
if fname.endswith('pyc'):
@@ -60,7 +97,7 @@ def show_file(fname):
elif fname.endswith('py'):
show_py_file(fname)
else:
- print "Odd file:", fname
+ print("Odd file:", fname)
def main(args):
if args[0] == '-c':
diff --git a/requirements/dev.pip b/requirements/dev.pip
index 668ae351..503601ff 100644
--- a/requirements/dev.pip
+++ b/requirements/dev.pip
@@ -17,4 +17,4 @@ pylint==1.4.4
# for kitting.
requests==2.8.1
-twine==1.6.3
+twine==1.6.4
diff --git a/tests/coveragetest.py b/tests/coveragetest.py
index 8357f101..8b54c819 100644
--- a/tests/coveragetest.py
+++ b/tests/coveragetest.py
@@ -56,6 +56,11 @@ class CoverageTest(
def setUp(self):
super(CoverageTest, self).setUp()
+ # Attributes for getting info about what happened.
+ self.last_command_status = None
+ self.last_command_output = None
+ self.last_module_name = None
+
if _TEST_NAME_FILE: # pragma: debugging
with open(_TEST_NAME_FILE, "w") as f:
f.write("%s_%s" % (
@@ -112,8 +117,9 @@ class CoverageTest(
return mod
def get_module_name(self):
- """Return the module name to use for this test run."""
- return 'coverage_test_' + str(random.random())[2:]
+ """Return a random module name to use for this test run."""
+ self.last_module_name = 'coverage_test_' + str(random.random())[2:]
+ return self.last_module_name
# Map chars to numbers for arcz_to_arcs
_arcz_map = {'.': -1}
diff --git a/tests/helpers.py b/tests/helpers.py
index aa094bc1..f4bff2b0 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -3,7 +3,12 @@
"""Helpers for coverage.py tests."""
+import os
import subprocess
+import sys
+
+from coverage import env
+from coverage.misc import output_encoding
def run_command(cmd):
@@ -12,8 +17,21 @@ def run_command(cmd):
Returns the exit status code and the combined stdout and stderr.
"""
+ if env.PY2 and isinstance(cmd, unicode):
+ cmd = cmd.encode(sys.getfilesystemencoding())
+
+ # In some strange cases (PyPy3 in a virtualenv!?) the stdout encoding of
+ # the subprocess is set incorrectly to ascii. Use an environment variable
+ # to force the encoding to be the same as ours.
+ sub_env = dict(os.environ)
+ encoding = output_encoding()
+ if encoding:
+ sub_env['PYTHONIOENCODING'] = encoding
+
proc = subprocess.Popen(
- cmd, shell=True,
+ cmd,
+ shell=True,
+ env=sub_env,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
@@ -22,7 +40,7 @@ def run_command(cmd):
# Get the output, and canonicalize it to strings with newlines.
if not isinstance(output, str):
- output = output.decode('utf-8')
+ output = output.decode(output_encoding())
output = output.replace('\r', '')
return status, output
diff --git a/tests/test_files.py b/tests/test_files.py
index e3d33285..e7353235 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -27,7 +27,7 @@ class FilesTest(CoverageTest):
def test_simple(self):
self.make_file("hello.py")
files.set_relative_directory()
- self.assertEqual(files.relative_filename("hello.py"), "hello.py")
+ self.assertEqual(files.relative_filename(u"hello.py"), u"hello.py")
a = self.abs_path("hello.py")
self.assertNotEqual(a, "hello.py")
self.assertEqual(files.relative_filename(a), "hello.py")
diff --git a/tests/test_oddball.py b/tests/test_oddball.py
index 8f9e9707..87c65b0e 100644
--- a/tests/test_oddball.py
+++ b/tests/test_oddball.py
@@ -410,6 +410,46 @@ class GettraceTest(CoverageTest):
''',
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "")
+ def test_setting_new_trace_function(self):
+ # https://bitbucket.org/ned/coveragepy/issues/436/disabled-coverage-ctracer-may-rise-from
+ self.check_coverage('''\
+ import sys
+
+ def tracer(frame, event, arg):
+ print("%s: %s @ %d" % (event, frame.f_code.co_filename, frame.f_lineno))
+ return tracer
+
+ def begin():
+ sys.settrace(tracer)
+
+ def collect():
+ t = sys.gettrace()
+ assert t is tracer, t
+
+ def test_unsets_trace():
+ begin()
+ collect()
+
+ old = sys.gettrace()
+ test_unsets_trace()
+ sys.settrace(old)
+ ''',
+ lines=[1, 3, 4, 5, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20],
+ missing="4-5, 11-12",
+ )
+
+ out = self.stdout().replace(self.last_module_name, "coverage_test")
+
+ self.assertEqual(
+ out,
+ (
+ "call: coverage_test.py @ 10\n"
+ "line: coverage_test.py @ 11\n"
+ "line: coverage_test.py @ 12\n"
+ "return: coverage_test.py @ 12\n"
+ ),
+ )
+
class ExecTest(CoverageTest):
"""Tests of exec."""
diff --git a/tests/test_phystokens.py b/tests/test_phystokens.py
index b4a106fd..e28fb176 100644
--- a/tests/test_phystokens.py
+++ b/tests/test_phystokens.py
@@ -8,7 +8,7 @@ import re
from coverage import env
from coverage.phystokens import source_token_lines, source_encoding
-from coverage.phystokens import neuter_encoding_declaration
+from coverage.phystokens import neuter_encoding_declaration, compile_unicode
from coverage.python import get_python_source
from tests.coveragetest import CoverageTest
@@ -102,6 +102,7 @@ ENCODING_DECLARATION_SOURCES = [
b"#!/usr/bin/python\n# vim: set fileencoding=cp850:\n",
b"# This Python file uses this encoding: cp850\n",
b"# This file uses a different encoding:\n# coding: cp850\n",
+ b"\n# coding=cp850\n\n",
]
class SourceEncodingTest(CoverageTest):
@@ -126,11 +127,6 @@ class SourceEncodingTest(CoverageTest):
source = b'def parse(src, encoding=None):\n pass'
self.assertEqual(source_encoding(source), DEF_ENCODING)
- def test_detect_source_encoding_on_second_line(self):
- # A coding declaration should be found despite a first blank line.
- source = b"\n# coding=cp850\n\n"
- self.assertEqual(source_encoding(source), 'cp850')
-
def test_dont_detect_source_encoding_on_third_line(self):
# A coding declaration doesn't count on the third line.
source = b"\n\n# coding=cp850\n\n"
@@ -160,8 +156,30 @@ class NeuterEncodingDeclarationTest(CoverageTest):
for source in ENCODING_DECLARATION_SOURCES:
neutered = neuter_encoding_declaration(source.decode("ascii"))
neutered = neutered.encode("ascii")
+
+ # The neutered source should have the same number of lines.
+ source_lines = source.splitlines()
+ neutered_lines = neutered.splitlines()
+ self.assertEqual(len(source_lines), len(neutered_lines))
+
+ # Only one of the lines should be different.
+ lines_different = sum(
+ int(nline != sline) for nline, sline in zip(neutered_lines, source_lines)
+ )
+ self.assertEqual(lines_different, 1)
+
self.assertEqual(
source_encoding(neutered),
DEF_ENCODING,
"Wrong encoding in %r" % neutered
)
+
+
+class CompileUnicodeTest(CoverageTest):
+ """Tests of compiling Unicode strings."""
+
+ run_in_temp_dir = False
+
+ def test_cp1252(self):
+ uni = u"""# coding: cp1252\n# \u201C curly \u201D\n"""
+ compile_unicode(uni, "<string>", "exec")
diff --git a/tests/test_process.py b/tests/test_process.py
index 4902f7c0..a69fd356 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -13,6 +13,7 @@ import textwrap
import coverage
from coverage import env, CoverageData
+from coverage.misc import output_encoding
from tests.coveragetest import CoverageTest
@@ -884,6 +885,82 @@ class FailUnderEmptyFilesTest(CoverageTest):
self.assertEqual(st, 2)
+class UnicodeFilePathsTest(CoverageTest):
+ """Tests of using non-ascii characters in the names of files."""
+
+ def test_accented_dot_py(self):
+ # Make a file with a non-ascii character in the filename.
+ self.make_file(u"h\xe2t.py", "print('accented')")
+ out = self.run_command(u"coverage run h\xe2t.py")
+ self.assertEqual(out, "accented\n")
+
+ # The HTML report uses ascii-encoded HTML entities.
+ out = self.run_command("coverage html")
+ self.assertEqual(out, "")
+ self.assert_exists(u"htmlcov/h\xe2t_py.html")
+ with open("htmlcov/index.html") as indexf:
+ index = indexf.read()
+ self.assertIn('<a href="h&#226;t_py.html">h&#226;t.py</a>', index)
+
+ # The XML report is always UTF8-encoded.
+ out = self.run_command("coverage xml")
+ self.assertEqual(out, "")
+ with open("coverage.xml", "rb") as xmlf:
+ xml = xmlf.read()
+ self.assertIn(u' filename="h\xe2t.py"'.encode('utf8'), xml)
+ self.assertIn(u' name="h\xe2t.py"'.encode('utf8'), xml)
+
+ report_expected = (
+ u"Name Stmts Miss Cover\n"
+ u"----------------------------\n"
+ u"h\xe2t.py 1 0 100%\n"
+ )
+
+ if env.PY2:
+ report_expected = report_expected.encode(output_encoding())
+
+ out = self.run_command("coverage report")
+ self.assertEqual(out, report_expected)
+
+ def test_accented_directory(self):
+ # Make a file with a non-ascii character in the directory name.
+ self.make_file(u"\xe2/accented.py", "print('accented')")
+ out = self.run_command(u"coverage run \xe2/accented.py")
+ self.assertEqual(out, "accented\n")
+
+ # The HTML report uses ascii-encoded HTML entities.
+ out = self.run_command("coverage html")
+ self.assertEqual(out, "")
+ self.assert_exists(u"htmlcov/\xe2_accented_py.html")
+ with open("htmlcov/index.html") as indexf:
+ index = indexf.read()
+ self.assertIn('<a href="&#226;_accented_py.html">&#226;%saccented.py</a>' % os.sep, index)
+
+ # The XML report is always UTF8-encoded.
+ out = self.run_command("coverage xml")
+ self.assertEqual(out, "")
+ with open("coverage.xml", "rb") as xmlf:
+ xml = xmlf.read()
+ self.assertIn(u' filename="\xe2/accented.py"'.encode('utf8'), xml)
+ self.assertIn(u' name="accented.py"'.encode('utf8'), xml)
+ self.assertIn(
+ u'<package branch-rate="0" complexity="0" line-rate="1" name="\xe2">'.encode('utf8'),
+ xml
+ )
+
+ report_expected = (
+ u"Name Stmts Miss Cover\n"
+ u"-----------------------------------\n"
+ u"\xe2%saccented.py 1 0 100%%\n" % os.sep
+ )
+
+ if env.PY2:
+ report_expected = report_expected.encode(output_encoding())
+
+ out = self.run_command("coverage report")
+ self.assertEqual(out, report_expected)
+
+
def possible_pth_dirs():
"""Produce a sequence of directories for trying to write .pth files."""
# First look through sys.path, and we find a .pth file, then it's a good
diff --git a/tests/test_python.py b/tests/test_python.py
index e510e786..ee1e1f95 100644
--- a/tests/test_python.py
+++ b/tests/test_python.py
@@ -20,7 +20,7 @@ class GetZipBytesTest(CoverageTest):
# See igor.py, do_zipmods, for the text of these files.
zip_file = "tests/zipmods.zip"
sys.path.append(zip_file) # So we can import the files.
- for encoding in ["utf8", "gb2312", "hebrew", "shift_jis"]:
+ for encoding in ["utf8", "gb2312", "hebrew", "shift_jis", "cp1252"]:
filename = zip_file + "/encoded_" + encoding + ".py"
filename = filename.replace("/", os.sep)
zip_data = get_zip_bytes(filename)
diff --git a/tests/test_summary.py b/tests/test_summary.py
index cf55130a..56c0b831 100644
--- a/tests/test_summary.py
+++ b/tests/test_summary.py
@@ -1,3 +1,4 @@
+# coding: utf8
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
@@ -13,6 +14,7 @@ import sys
import coverage
from coverage import env
from coverage.backward import StringIO
+from coverage.misc import output_encoding
from tests.coveragetest import CoverageTest
@@ -404,6 +406,33 @@ class SummaryTest(CoverageTest):
"mycode.py NotPython: Couldn't parse 'mycode.py' as Python source: 'error' at line 1"
)
+ def test_accenteddotpy_not_python(self):
+ # We run a .py file with a non-ascii name, and when reporting, we can't
+ # parse it as Python. We should get an error message in the report.
+
+ self.make_file(u"accented\xe2.py", "print('accented')")
+ self.run_command(u"coverage run accented\xe2.py")
+ self.make_file(u"accented\xe2.py", "This isn't python at all!")
+ report = self.report_from_command(u"coverage report accented\xe2.py")
+
+ # Name Stmts Miss Cover
+ # ----------------------------
+ # xxxx NotPython: Couldn't parse '...' as Python source: 'invalid syntax' at line 1
+ # No data to report.
+
+ last = self.squeezed_lines(report)[-2]
+ # The actual file name varies run to run.
+ last = re.sub(r"parse '.*(accented.*?\.py)", r"parse '\1", last)
+ # The actual error message varies version to version
+ last = re.sub(r": '.*' at", ": 'error' at", last)
+ expected = (
+ u"accented\xe2.py NotPython: "
+ u"Couldn't parse 'accented\xe2.py' as Python source: 'error' at line 1"
+ )
+ if env.PY2:
+ expected = expected.encode(output_encoding())
+ self.assertEqual(last, expected)
+
def test_dotpy_not_python_ignored(self):
# We run a .py file, and when reporting, we can't parse it as Python,
# but we've said to ignore errors, so there's no error reported.
diff --git a/tox.ini b/tox.ini
index 21602252..009fa79b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,7 +2,7 @@
# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
[tox]
-envlist = py{26,27,33,34,35}, pypy{24,26,3_24}, doc
+envlist = py{26,27,33,34,35}, pypy{24,26,40,3_24}, doc
skip_missing_interpreters = True
[testenv]
@@ -14,13 +14,13 @@ deps =
mock==1.3.0
PyContracts==1.7.6
py26: unittest2==1.1.0
- py{26,27,33,34}: gevent==1.1b5
+ py{26,27,33,34,35}: gevent==1.1b6
py{26,27,33,34}: eventlet==0.17.4
py{26,27,33,34,35}: greenlet==0.4.9
passenv = *
setenv =
- pypy,pypy{24,26,3_24}: COVERAGE_NO_EXTENSION=no C extensions under PyPy
+ pypy,pypy{24,26,40,3_24}: COVERAGE_NO_EXTENSION=no C extension under PyPy
commands =
python setup.py --quiet clean develop
@@ -49,15 +49,19 @@ basepython = pypy2.6
[testenv:pypy3_24]
basepython = pypy3-2.4
+[testenv:pypy40]
+basepython = pypy4.0
+
[testenv:doc]
# Build the docs so we know if they are successful. We build twice: once with
# -q to get all warnings, and once with -QW to get a success/fail status
# return.
-basepython = python2
deps = -rdoc/requirements.pip
commands =
sphinx-build -aEnq doc doc/_build/html
sphinx-build -aEnQW doc doc/_build/html
+ rst2html.py --strict CHANGES.rst doc/_build/trash
+ rst2html.py --strict README.rst doc/_build/trash
# Yes, pep8 will read its settings from tox.ini!
[pep8]