diff options
37 files changed, 380 insertions, 351 deletions
diff --git a/.travis.yml b/.travis.yml index 79ae11e0..ac189e57 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,17 @@ # Tell Travis what to do # https://travis-ci.com/nedbat/coveragepy +dist: xenial language: python cache: pip python: - '2.7' - - '3.4' - '3.5' - '3.6' - - 'pypy' - - 'pypy3.5' + - '3.7' + - 'pypy2.7-6.0' + - 'pypy3.5-6.0' env: matrix: @@ -23,7 +24,6 @@ install: script: - tox - - pip freeze - if [[ $COVERAGE_COVERAGE == 'yes' ]]; then python igor.py combine_html; fi - if [[ $COVERAGE_COVERAGE == 'yes' ]]; then pip install codecov; fi - if [[ $COVERAGE_COVERAGE == 'yes' ]]; then codecov -X gcov --file coverage.xml; fi diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 93f27153..50a8fba8 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -101,6 +101,7 @@ Scott Belden Sigve Tjora Stan Hu Stefan Behnel +Stephan Richter Stephen Finucane Steve Leonard Steve Peak @@ -51,10 +51,12 @@ test: PYTEST_SMOKE_ARGS = -n 6 -m "not expensive" --maxfail=3 $(ARGS) smoke: - COVERAGE_NO_PYTRACER=1 tox -q -e py27,py34 -- $(PYTEST_SMOKE_ARGS) + # Run tests with the C tracer in the lowest supported Python versions. + COVERAGE_NO_PYTRACER=1 tox -q -e py27,py35 -- $(PYTEST_SMOKE_ARGS) pysmoke: - COVERAGE_NO_CTRACER=1 tox -q -e py27,py34 -- $(PYTEST_SMOKE_ARGS) + # Run tests with the Python tracer in the lowest supported Python versions. + COVERAGE_NO_CTRACER=1 tox -q -e py27,py35 -- $(PYTEST_SMOKE_ARGS) metacov: COVERAGE_COVERAGE=yes tox $(ARGS) @@ -35,7 +35,7 @@ library to determine which lines are executable, and which have been executed. Coverage.py runs on many versions of Python: * CPython 2.7. -* CPython 3.4 through pre-alpha 3.8. +* CPython 3.5 through pre-alpha 3.8. * PyPy2 6.0 and PyPy3 6.0. * Jython 2.7.1, though not for reporting. * IronPython 2.7.7, though not for reporting. diff --git a/appveyor.yml b/appveyor.yml index 1131efe1..f4a41b51 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -34,18 +34,6 @@ environment: PYTHON_VERSION: "2.7.15" PYTHON_ARCH: "64" - - JOB: "3.4 32-bit" - TOXENV: "py34" - PYTHON: "C:\\Python34" - PYTHON_VERSION: "3.4.4" - PYTHON_ARCH: "32" - - - JOB: "3.4 64-bit" - TOXENV: "py34" - PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4.4" - PYTHON_ARCH: "64" - - JOB: "3.5 32-bit" TOXENV: "py35" PYTHON: "C:\\Python35" @@ -122,8 +110,8 @@ install: # Install requirements. - "%CMD_IN_ENV% pip install -r requirements/ci.pip" - # Make a python3.4.bat file in the current directory so that tox will find it - # and python3.4 will mean what we want it to. + # Make a pythonX.Y.bat file in the current directory so that tox will find it + # and pythonX.Y will mean what we want it to. - "python -c \"import os; open('python{}.{}.bat'.format(*os.environ['TOXENV'][2:]), 'w').write('@{}\\\\python \\x25*\\n'.format(os.environ['PYTHON']))\"" build_script: diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c index 7d639112..d497a94d 100644 --- a/coverage/ctracer/tracer.c +++ b/coverage/ctracer/tracer.c @@ -541,7 +541,7 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) /* Make the frame right in case settrace(gettrace()) happens. */ Py_INCREF(self); - My_XSETREF(frame->f_trace, (PyObject*)self); + Py_XSETREF(frame->f_trace, (PyObject*)self); /* A call event is really a "start frame" event, and can happen for * re-entering a generator also. f_lasti is -1 for a true call, and a diff --git a/coverage/ctracer/util.h b/coverage/ctracer/util.h index 96d2e51c..cb8aceb9 100644 --- a/coverage/ctracer/util.h +++ b/coverage/ctracer/util.h @@ -44,14 +44,6 @@ #endif /* Py3k */ -// Undocumented, and not in 2.6, so our own copy of it. -#define My_XSETREF(op, op2) \ - do { \ - PyObject *_py_tmp = (PyObject *)(op); \ - (op) = (op2); \ - Py_XDECREF(_py_tmp); \ - } while (0) - /* The values returned to indicate ok or error. */ #define RET_OK 0 #define RET_ERROR -1 diff --git a/coverage/env.py b/coverage/env.py index d97b193c..83b4be65 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -28,6 +28,39 @@ PY3 = PYVERSION >= (3, 0) class PYBEHAVIOR(object): """Flags indicating this Python's behavior.""" + # Is "if __debug__" optimized away? + optimize_if_debug = (not PYPY) + + # If "if not __debug__" optimized away? + optimize_if_not_debug = (not PYPY) and (PYVERSION >= (3, 7, 0, 'alpha', 4)) + + # Do we have yield-from? + yield_from = (PYVERSION >= (3, 3)) + + # Do we have PEP 420 namespace packages? + namespaces_pep420 = (PYVERSION >= (3, 3)) + + # Do .pyc files have the source file size recorded in them? + size_in_pyc = (PYVERSION >= (3, 3)) + + # Do we have async and await syntax? + async_syntax = (PYVERSION >= (3, 5)) + + # PEP 448 defined additional unpacking generalizations + unpackings_pep448 = (PYVERSION >= (3, 5)) + + # Can co_lnotab have negative deltas? + negative_lnotab = (PYVERSION >= (3, 6)) + + # Do .pyc files conform to PEP 552? Hash-based pyc's. + hashed_pyc_pep552 = (PYVERSION >= (3, 7, 0, 'alpha', 4)) + + # Python 3.7.0b3 changed the behavior of the sys.path[0] entry for -m. It + # used to be an empty string (meaning the current directory). It changed + # to be the actual path to the current directory, so that os.chdir wouldn't + # affect the outcome. + actual_syspath0_dash_m = (PYVERSION >= (3, 7, 0, 'beta', 3)) + # When a break/continue/return statement in a try block jumps to a finally # block, does the finally block do the break/continue/return (pre-3.8), or # does the finally jump back to the break/continue/return (3.8) to do the diff --git a/coverage/execfile.py b/coverage/execfile.py index 4fc6a85f..97997b06 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -124,11 +124,7 @@ class PyRunner(object): should_update_sys_path = True if self.as_module: - # Python 3.7.0b3 changed the behavior of the sys.path[0] entry for -m. It - # used to be an empty string (meaning the current directory). It changed - # to be the actual path to the current directory, so that os.chdir wouldn't - # affect the outcome. - if env.PYVERSION >= (3, 7, 0, 'beta', 3): + if env.PYBEHAVIOR.actual_syspath0_dash_m: path0 = os.getcwd() else: path0 = "" @@ -290,7 +286,7 @@ def make_code_from_pyc(filename): raise NoCode("Bad magic number in .pyc file") date_based = True - if env.PYVERSION >= (3, 7, 0, 'alpha', 4): + if env.PYBEHAVIOR.hashed_pyc_pep552: flags = struct.unpack('<L', fpyc.read(4))[0] hash_based = flags & 0x01 if hash_based: @@ -299,7 +295,7 @@ def make_code_from_pyc(filename): if date_based: # Skip the junk in the header that we don't need. fpyc.read(4) # Skip the moddate. - if env.PYVERSION >= (3, 3): + if env.PYBEHAVIOR.size_in_pyc: # 3.3 added another long to the header (size), skip it. fpyc.read(4) diff --git a/coverage/files.py b/coverage/files.py index b328f653..d9495912 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -59,6 +59,7 @@ def canonical_filename(filename): """ if filename not in CANONICAL_FILENAME_CACHE: + cf = filename if not os.path.isabs(filename): for path in [os.curdir] + sys.path: if path is None: @@ -69,9 +70,9 @@ def canonical_filename(filename): except UnicodeError: exists = False if exists: - filename = f + cf = f break - cf = abs_file(filename) + cf = abs_file(cf) CANONICAL_FILENAME_CACHE[filename] = cf return CANONICAL_FILENAME_CACHE[filename] diff --git a/coverage/htmlfiles/keybd_closed.png b/coverage/htmlfiles/keybd_closed.png Binary files differindex db114023..db114023 100755..100644 --- a/coverage/htmlfiles/keybd_closed.png +++ b/coverage/htmlfiles/keybd_closed.png diff --git a/coverage/htmlfiles/keybd_open.png b/coverage/htmlfiles/keybd_open.png Binary files differindex db114023..db114023 100755..100644 --- a/coverage/htmlfiles/keybd_open.png +++ b/coverage/htmlfiles/keybd_open.png diff --git a/coverage/parser.py b/coverage/parser.py index 1c19f69e..6ae99fe4 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -409,7 +409,7 @@ class ByteParser(object): yield (byte_num, line_num) last_line_num = line_num byte_num += byte_incr - if env.PYVERSION >= (3, 6) and line_incr >= 0x80: + if env.PYBEHAVIOR.negative_lnotab and line_incr >= 0x80: line_incr -= 0x100 line_num += line_incr if line_num != last_line_num: diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 0219a8a2..bf8c1e43 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -545,7 +545,7 @@ class CoverageSqliteData(SimpleReprMixin): if file_id is None: return None else: - query = "select lineno from line where file_id = ?" + query = "select distinct lineno from line where file_id = ?" data = [file_id] if context is not None: query += " and context_id = ?" @@ -560,7 +560,7 @@ class CoverageSqliteData(SimpleReprMixin): if file_id is None: return None else: - query = "select fromno, tono from arc where file_id = ?" + query = "select distinct fromno, tono from arc where file_id = ?" data = [file_id] if context is not None: query += " and context_id = ?" diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py index 6c07337a..8ecdc24a 100644 --- a/coverage/xmlreport.py +++ b/coverage/xmlreport.py @@ -6,7 +6,6 @@ import os import os.path -import re import sys import time import xml.dom.minidom @@ -225,16 +224,4 @@ def serialize_xml(dom): out = dom.toprettyxml() if env.PY2: out = out.encode("utf8") - # In Python 3.8, minidom lost the sorting of attributes: https://bugs.python.org/issue34160 - # For the limited kinds of XML we produce, this re-sorts them. - if env.PYVERSION >= (3, 8): - rx_attr = r' [\w-]+="[^"]*"' - rx_attrs = r'(' + rx_attr + ')+' - fixed_lines = [] - for line in out.splitlines(True): - hollow_line = re.sub(rx_attrs, u"☺", line) - attrs = sorted(re.findall(rx_attr, line)) - new_line = hollow_line.replace(u"☺", "".join(attrs)) - fixed_lines.append(new_line) - out = "".join(fixed_lines) return out diff --git a/doc/cmd.rst b/doc/cmd.rst index 86a858e4..1344d20e 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -207,7 +207,7 @@ can include a path to another directory. By default, each run of your program starts with an empty data set. If you need to run your program multiple times to get complete data (for example, because you need to supply disjoint options), you can accumulate data across runs with -the ``-a`` flag on the **run** command. +the ``--append`` flag on the **run** command. To erase the collected data, use the **erase** command:: @@ -482,7 +482,8 @@ to log: * ``multiproc``: log the start and stop of multiprocessing processes. -* ``pid``: annotate all warnings and debug output with the process id. +* ``pid``: annotate all warnings and debug output with the process and thread + ids. * ``plugin``: print information about plugin operations. diff --git a/doc/index.rst b/doc/index.rst index 7ed08189..b06e805d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -31,7 +31,7 @@ not. The latest version is coverage.py 5.0a4, released November 25, 2018. It is supported on: - * Python versions 2.7, 3.4, 3.5, 3.6, 3.7, and pre-alpha 3.8. + * Python versions 2.7, 3.5, 3.6, 3.7, and pre-alpha 3.8. * PyPy2 6.0 and PyPy3 6.0. @@ -40,7 +40,7 @@ not. * IronPython 2.7.7, though only for running code, not reporting. **This is a pre-release build. The usual warnings about possible bugs - apply.** The latest stable version is coverage.py 4.5.2, `described here`_. + apply.** The latest stable version is coverage.py 4.5.3, `described here`_. .. _described here: http://coverage.readthedocs.io/ diff --git a/doc/requirements.pip b/doc/requirements.pip index 6aa6e6e3..de417d05 100644 --- a/doc/requirements.pip +++ b/doc/requirements.pip @@ -4,6 +4,6 @@ doc8==0.8.0 pyenchant==2.0.0 -sphinx==1.8.3 +sphinx==1.8.4 sphinxcontrib-spelling==4.2.0 -sphinx_rtd_theme==0.4.2 +sphinx_rtd_theme==0.4.3 @@ -1,5 +1,6 @@ * Release checklist +- Check that the current virtualenv matches the current coverage branch. - Version number in coverage/version.py version_info = (4, 0, 2, 'alpha', 1) version_info = (4, 0, 2, 'beta', 1) @@ -23,10 +24,9 @@ - Generate new sample_html to get the latest, incl footer version number: make clean pip install -e . - pip install nose cd ~/cog/trunk rm -rf htmlcov - coverage run --branch --source=cogapp -m nose cogapp/test_cogapp.py:CogTestsInMemory + coverage run --branch --source=cogapp -m pytest -k CogTestsInMemory coverage combine coverage html - IF PRE-RELEASE: @@ -45,7 +45,7 @@ $ make publish - Kits: - Start fresh: - - $ make clean + - $ make sterile - Source kit and wheels: - $ make kit wheel - Linux wheels: @@ -60,6 +60,7 @@ - $ make kit_upload - Tag the tree - git tag coverage-3.0.1 + - git push - git push --tags - Bump version: - coverage/version.py diff --git a/lab/set_env.py b/lab/set_env.py deleted file mode 100755 index fe0a4d49..00000000 --- a/lab/set_env.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python3 -# -# Run this like: -# -# $ $(lab/set_env.py) -# - -import functools -import glob -import itertools -import os -import re -import sys - -# Some other environment variables that could be useful: -# $set_env.py: PYTEST_ADDOPTS - Extra arguments to pytest. - -pstderr = functools.partial(print, file=sys.stderr) - -SETTINGS = [] - -def find_settings(): - line_pattern = r"\$set_env.py: (\w+) - (.*)" - globs = "*/*.py *.py" - - filenames = itertools.chain.from_iterable(glob.glob(g) for g in globs.split()) - files = 0 - for filename in filenames: - files += 1 - with open(filename) as f: - for line in f: - m = re.search(line_pattern, line) - if m: - SETTINGS.append(m.groups()) - SETTINGS.sort() - pstderr("Read {} files".format(files)) - -def read_them(): - values = {} - for name, _ in SETTINGS: - values[name] = os.environ.get(name) - return values - -def show_them(values): - for i, (name, description) in enumerate(SETTINGS, start=1): - value = values[name] - if value is None: - eq = ' ' - value = '' - else: - eq = '=' - value = repr(value) - pstderr("{:2d}: {:>30s} {} {:12s} {}".format(i, name, eq, value, description)) - -def set_by_num(values, n, value): - setting_name = SETTINGS[int(n)-1][0] - values[setting_name] = value - -PROMPT = "(# value | x # | q) ::> " - -def get_new_values(values): - show = True - while True: - if show: - show_them(values) - show = False - pstderr("") - pstderr(PROMPT, end='') - sys.stderr.flush() - try: - cmd = input("").strip().split() - except EOFError: - pstderr("\n") - break - if not cmd: - continue - if cmd[0] == 'q': - break - if cmd[0] == 'x': - if len(cmd) < 2: - pstderr("Need numbers of entries to delete") - continue - try: - nums = map(int, cmd[1:]) - except ValueError: - pstderr("Need numbers of entries to delete") - continue - else: - for num in nums: - set_by_num(values, num, None) - else: - try: - num = int(cmd[0]) - except ValueError: - pstderr("Don't understand option {!r}".format(cmd[0])) - continue - else: - if len(cmd) >= 2: - set_by_num(values, num, " ".join(cmd[1:])) - else: - pstderr("Need a value to set") - continue - show = True - - return values - -def as_exports(values): - exports = [] - for name, value in values.items(): - if value is None: - exports.append("export -n {}".format(name)) - else: - exports.append("export {}={!r}".format(name, value)) - return "eval " + "; ".join(exports) - -def main(): - find_settings() - print(as_exports(get_new_values(read_them()))) - -if __name__ == '__main__': - main() diff --git a/requirements/dev.pip b/requirements/dev.pip index 415dcda2..ef467537 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -4,7 +4,7 @@ # Requirements for doing local development work on coverage.py. # https://requires.io/github/nedbat/coveragepy/requirements/ -pip==18.1 +pip==19.0.3 virtualenv==16.1.0 # PyPI requirements for running tests. @@ -19,4 +19,4 @@ readme_renderer==24.0 # for kitting. requests==2.21.0 -twine==1.12.1 +twine==1.13.0 diff --git a/requirements/pytest.pip b/requirements/pytest.pip index 6ec37a72..0274ec05 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -3,12 +3,12 @@ # The pytest specifics used by coverage.py -pytest==4.0.2 +pytest==4.3.0 pluggy>=0.7 # pytest needs this, but pip doesn't understand -pytest-xdist==1.25.0 -flaky==3.4.0 +pytest-xdist==1.26.1 +flaky==3.5.3 mock==2.0.0 -PyContracts==1.8.7 +PyContracts==1.8.12 # Our testing mixins unittest-mixins==1.6 diff --git a/requirements/tox.pip b/requirements/tox.pip index 0ba0e97c..81fa10dd 100644 --- a/requirements/tox.pip +++ b/requirements/tox.pip @@ -2,6 +2,6 @@ # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # The version of tox used by coverage.py -tox==3.6.1 +tox==3.7.0 # Adds env recreation on requirements file changes. tox-battery==0.5.1 diff --git a/requirements/wheel.pip b/requirements/wheel.pip index 5c766d59..56ced495 100644 --- a/requirements/wheel.pip +++ b/requirements/wheel.pip @@ -2,6 +2,7 @@ # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Things needed to make wheels for coverage.py -setuptools==40.6.3 + +setuptools==40.8.0 # pin so auditwheel will work: https://github.com/pypa/auditwheel/issues/102 wheel==0.31.1 @@ -28,7 +28,6 @@ Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 -Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 @@ -103,12 +102,20 @@ setup_args = dict( author_email='ned@nedbatchelder.com', description=doc, long_description=long_description, + long_description_content_type='text/x-rst', keywords='code coverage testing', license='Apache 2.0', classifiers=classifier_list, url="https://github.com/nedbat/coveragepy", - - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4", + project_urls={ + 'Documentation': __url__, + 'Funding': ( + 'https://tidelift.com/subscription/pkg/pypi-coverage' + '?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=pypi' + ), + 'Issues': 'https://github.com/nedbat/coveragepy/issues', + }, + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4", ) # A replacement for the build_ext command which raises a single exception diff --git a/tests/conftest.py b/tests/conftest.py index c883ef7b..d0572dca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,9 @@ import pytest from coverage import env +# Pytest can take additional options: +# $set_env.py: PYTEST_ADDOPTS - Extra arguments to pytest. + @pytest.fixture(autouse=True) def set_warnings(): """Enable DeprecationWarnings during all tests.""" diff --git a/tests/goldtest.py b/tests/goldtest.py index 4c6c3c96..b5e32f5f 100644 --- a/tests/goldtest.py +++ b/tests/goldtest.py @@ -10,6 +10,7 @@ import os import os.path import re import sys +import xml.etree.ElementTree from unittest_mixins import change_dir # pylint: disable=unused-import @@ -79,12 +80,19 @@ def compare( # ourselves. text_diff = [] for f in diff_files: + expected_file = os.path.join(expected_dir, f) - actual_file = os.path.join(actual_dir, f) with open(expected_file, READ_MODE) as fobj: expected = fobj.read() + if expected_file.endswith(".xml"): + expected = canonicalize_xml(expected) + + actual_file = os.path.join(actual_dir, f) with open(actual_file, READ_MODE) as fobj: actual = fobj.read() + if actual_file.endswith(".xml"): + actual = canonicalize_xml(actual) + if scrubs: expected = scrub(expected, scrubs) actual = scrub(actual, scrubs) @@ -102,6 +110,15 @@ def compare( assert not actual_only, "Files in %s only: %s" % (actual_dir, actual_only) +def canonicalize_xml(xtext): + """Canonicalize some XML text.""" + root = xml.etree.ElementTree.fromstring(xtext) + for node in root.iter(): + node.attrib = dict(sorted(node.items())) + xtext = xml.etree.ElementTree.tostring(root) + return xtext.decode('utf8') + + def contains(filename, *strlist): """Check that the file contains all of a list of strings. diff --git a/tests/test_api.py b/tests/test_api.py index 2f6f7a2f..755a89a2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -495,7 +495,7 @@ class NamespaceModuleTest(UsingModulesMixin, CoverageTest): def setUp(self): super(NamespaceModuleTest, self).setUp() - if env.PYVERSION < (3, 3): + if not env.PYBEHAVIOR.namespaces_pep420: self.skipTest("Python before 3.3 doesn't have namespace packages") def test_explicit_namespace_module(self): diff --git a/tests/test_arcs.py b/tests/test_arcs.py index d3430af2..cbbac64a 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -1056,7 +1056,7 @@ class YieldTest(CoverageTest): self.assertEqual(self.stdout(), "20\n12\n") def test_yield_from(self): - if env.PYVERSION < (3, 3): + if not env.PYBEHAVIOR.yield_from: self.skipTest("Python before 3.3 doesn't have 'yield from'") self.check_coverage("""\ def gen(inp): @@ -1159,8 +1159,8 @@ class OptimizedIfTest(CoverageTest): arcz=".1 1C CE EF F.", ) - def test_constant_if(self): - if env.PYPY: + def test_if_debug(self): + if not env.PYBEHAVIOR.optimize_if_debug: self.skipTest("PyPy doesn't optimize away 'if __debug__:'") # CPython optimizes away "if __debug__:" self.check_coverage("""\ @@ -1173,13 +1173,17 @@ class OptimizedIfTest(CoverageTest): """, arcz=".1 12 24 41 26 61 1.", ) + + def test_if_not_debug(self): # Before 3.7, no Python optimized away "if not __debug__:" - if env.PYVERSION < (3, 7, 0, 'alpha', 4): - arcz = ".1 12 23 31 34 41 26 61 1." - arcz_missing = "34 41" - else: + if not env.PYBEHAVIOR.optimize_if_debug: + self.skipTest("PyPy doesn't optimize away 'if __debug__:'") + elif env.PYBEHAVIOR.optimize_if_not_debug: arcz = ".1 12 23 31 26 61 1." arcz_missing = "" + else: + arcz = ".1 12 23 31 34 41 26 61 1." + arcz_missing = "34 41" self.check_coverage("""\ for value in [True, False]: if value: @@ -1230,7 +1234,7 @@ class MiscArcTest(CoverageTest): ) def test_unpacked_literals(self): - if env.PYVERSION < (3, 5): + if not env.PYBEHAVIOR.unpackings_pep448: self.skipTest("Don't have unpacked literals until 3.5") self.check_coverage("""\ d = { @@ -1492,7 +1496,7 @@ class AsyncTest(CoverageTest): """Tests of the new async and await keywords in Python 3.5""" def setUp(self): - if env.PYVERSION < (3, 5): + if not env.PYBEHAVIOR.async_syntax: self.skipTest("Async features are new in Python 3.5") super(AsyncTest, self).setUp() diff --git a/tests/test_data.py b/tests/test_data.py index bda73810..3f96288f 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -199,6 +199,22 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): covdata.add_run_info(count=17) self.assertEqual(covdata.run_infos(), [{"hello": "there", "count": 17}]) + def test_no_duplicate_lines(self): + covdata = CoverageData() + covdata.set_context("context1") + covdata.add_lines(LINES_1) + covdata.set_context("context2") + covdata.add_lines(LINES_1) + self.assertEqual(covdata.lines('a.py'), A_PY_LINES_1) + + def test_no_duplicate_arcs(self): + covdata = CoverageData() + covdata.set_context("context1") + covdata.add_arcs(ARCS_3) + covdata.set_context("context2") + covdata.add_arcs(ARCS_3) + self.assertEqual(covdata.arcs('x.py'), X_PY_ARCS_3) + def test_no_arcs_vs_unmeasured_file(self): covdata = CoverageData() covdata.add_arcs(ARCS_3) diff --git a/tests/test_files.py b/tests/test_files.py index b4490ea6..e271da1b 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -57,6 +57,19 @@ class FilesTest(CoverageTest): rel = os.path.join('sub', trick, 'file1.py') self.assertEqual(files.relative_filename(abs_file(rel)), rel) + def test_canonical_filename_ensure_cache_hit(self): + self.make_file("sub/proj1/file1.py") + d = actual_path(self.abs_path("sub/proj1")) + self.chdir(d) + files.set_relative_directory() + canonical_path = files.canonical_filename('sub/proj1/file1.py') + self.assertEqual(canonical_path, self.abs_path('file1.py')) + # After the filename has been converted, it should be in the cache. + self.assertIn('sub/proj1/file1.py', files.CANONICAL_FILENAME_CACHE) + self.assertEqual( + files.canonical_filename('sub/proj1/file1.py'), + self.abs_path('file1.py')) + @pytest.mark.parametrize("original, flat", [ (u"a/b/c.py", u"a_b_c_py"), diff --git a/tests/test_plugins.py b/tests/test_plugins.py index d3365a6d..14d07c1a 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -4,6 +4,7 @@ """Tests for plugins.""" import os.path +from xml.etree import ElementTree import coverage from coverage import env @@ -455,14 +456,25 @@ class GoodFileTracerTest(FileTracerTest): total = cov.xml_report(include=["*.html"], omit=["uni*.html"]) self.assertAlmostEqual(total, 36.36, places=2) - with open("coverage.xml") as fxml: - xml = fxml.read() - - for snip in [ - 'filename="bar_4.html" line-rate="0.5" name="bar_4.html"', - 'filename="foo_7.html" line-rate="0.2857" name="foo_7.html"', - ]: - self.assertIn(snip, xml) + dom = ElementTree.parse("coverage.xml") + classes = {} + for elt in dom.findall(".//class"): + classes[elt.get('name')] = elt + + assert classes['bar_4.html'].attrib == { + 'branch-rate': '1', + 'complexity': '0', + 'filename': 'bar_4.html', + 'line-rate': '0.5', + 'name': 'bar_4.html', + } + assert classes['foo_7.html'].attrib == { + 'branch-rate': '1', + 'complexity': '0', + 'filename': 'foo_7.html', + 'line-rate': '0.2857', + 'name': 'foo_7.html', + } def test_defer_to_python(self): # A plugin that measures, but then wants built-in python reporting. diff --git a/tests/test_process.py b/tests/test_process.py index a1e71f1c..f234a9ef 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -11,6 +11,7 @@ import os.path import re import sys import textwrap +from xml.etree import ElementTree import pytest @@ -1266,12 +1267,18 @@ class UnicodeFilePathsTest(CoverageTest): 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 - ) + self.assertIn(b' filename="\xc3\xa2/accented.py"', xml) + self.assertIn(b' name="accented.py"', xml) + + dom = ElementTree.parse("coverage.xml") + elts = dom.findall(u".//package[@name='â']") + assert len(elts) == 1 + assert elts[0].attrib == { + "branch-rate": u"0", + "complexity": u"0", + "line-rate": u"1", + "name": u"â", + } report_expected = ( u"Name Stmts Miss Cover\n" diff --git a/tests/test_templite.py b/tests/test_templite.py index 16942db8..3b1e38af 100644 --- a/tests/test_templite.py +++ b/tests/test_templite.py @@ -252,13 +252,23 @@ class TempliteTest(CoverageTest): "@{% for n in nums -%}\n" " {% for a in abc -%}\n" " {# this disappears completely -#}\n" - " {{a -}}\n" + " {{a-}}\n" " {{n -}}\n" + " {{n -}}\n" " {% endfor %}\n" "{% endfor %}!\n", {'nums': [0, 1, 2], 'abc': ['a', 'b', 'c']}, - "@a0b0c0\na1b1c1\na2b2c2\n!\n" + "@a00b00c00\na11b11c11\na22b22c22\n!\n" ) + self.try_render( + "@{% for n in nums -%}\n" + " {{n -}}\n" + " x\n" + "{% endfor %}!\n", + {'nums': [0, 1, 2]}, + "@0x\n1x\n2x\n!\n" + ) + self.try_render(" hello ", {}, " hello ") def test_non_ascii(self): self.try_render( @@ -269,8 +279,8 @@ class TempliteTest(CoverageTest): def test_exception_during_evaluation(self): # TypeError: Couldn't evaluate {{ foo.bar.baz }}: - msg = "Couldn't evaluate None.bar" - with self.assertRaisesRegex(TempliteValueError, msg): + regex = "^Couldn't evaluate None.bar$" + with self.assertRaisesRegex(TempliteValueError, regex): self.try_render( "Hey {{foo.bar.baz}} there", {'foo': None}, "Hey ??? there" ) diff --git a/tests/test_xml.py b/tests/test_xml.py index 185e6ad1..09ab2f85 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -7,6 +7,7 @@ import os import os.path import re +from xml.etree import ElementTree import coverage from coverage.backward import import_local_file @@ -14,7 +15,6 @@ from coverage.files import abs_file from tests.coveragetest import CoverageTest from tests.goldtest import change_dir, compare, gold_path -from tests.helpers import re_line, re_lines class XmlTestHelpers(CoverageTest): @@ -30,7 +30,7 @@ class XmlTestHelpers(CoverageTest): self.make_file("sub/__init__.py") self.make_file("sub/doit.py", "print('doit!')") self.make_file("main.py", "import sub.doit") - cov = coverage.Coverage() + cov = coverage.Coverage(source=["."]) self.start_import_stop(cov, "main") return cov @@ -59,10 +59,36 @@ class XmlTestHelpers(CoverageTest): filename = here("f{0}.py".format(i)) self.make_file(filename, "# {0}\n".format(filename)) - def assert_source(self, xml, src): + def assert_source(self, xmldom, src): """Assert that the XML has a <source> element with `src`.""" src = abs_file(src) - self.assertRegex(xml, r'<source>\s*{0}\s*</source>'.format(re.escape(src))) + elts = xmldom.findall(".//sources/source") + assert any(elt.text == src for elt in elts) + + +class XmlTestHelpersTest(XmlTestHelpers, CoverageTest): + """Tests of methods in XmlTestHelpers.""" + + def test_assert_source(self): + dom = ElementTree.fromstring("""\ + <doc> + <src>foo</src> + <sources> + <source>{cwd}something</source> + <source>{cwd}another</source> + </sources> + </doc> + """.format(cwd=abs_file(".")+os.sep)) + + self.assert_source(dom, "something") + self.assert_source(dom, "another") + + with self.assertRaises(AssertionError): + self.assert_source(dom, "hello") + with self.assertRaises(AssertionError): + self.assert_source(dom, "foo") + with self.assertRaises(AssertionError): + self.assert_source(dom, "thing") class XmlReportTest(XmlTestHelpers, CoverageTest): @@ -110,25 +136,28 @@ class XmlReportTest(XmlTestHelpers, CoverageTest): def test_filename_format_showing_everything(self): cov = self.run_doit() - cov.xml_report(outfile="-") - xml = self.stdout() - doit_line = re_line(xml, "class.*doit") - self.assertIn('filename="sub/doit.py"', doit_line) + cov.xml_report() + dom = ElementTree.parse("coverage.xml") + elts = dom.findall(".//class[@name='doit.py']") + assert len(elts) == 1 + assert elts[0].get('filename') == "sub/doit.py" def test_filename_format_including_filename(self): cov = self.run_doit() - cov.xml_report(["sub/doit.py"], outfile="-") - xml = self.stdout() - doit_line = re_line(xml, "class.*doit") - self.assertIn('filename="sub/doit.py"', doit_line) + cov.xml_report(["sub/doit.py"]) + dom = ElementTree.parse("coverage.xml") + elts = dom.findall(".//class[@name='doit.py']") + assert len(elts) == 1 + assert elts[0].get('filename') == "sub/doit.py" def test_filename_format_including_module(self): cov = self.run_doit() import sub.doit # pylint: disable=import-error - cov.xml_report([sub.doit], outfile="-") - xml = self.stdout() - doit_line = re_line(xml, "class.*doit") - self.assertIn('filename="sub/doit.py"', doit_line) + cov.xml_report([sub.doit]) + dom = ElementTree.parse("coverage.xml") + elts = dom.findall(".//class[@name='doit.py']") + assert len(elts) == 1 + assert elts[0].get('filename') == "sub/doit.py" def test_reporting_on_nothing(self): # Used to raise a zero division error: @@ -136,28 +165,31 @@ class XmlReportTest(XmlTestHelpers, CoverageTest): self.make_file("empty.py", "") cov = coverage.Coverage() empty = self.start_import_stop(cov, "empty") - cov.xml_report([empty], outfile="-") - xml = self.stdout() - empty_line = re_line(xml, "class.*empty") - self.assertIn('filename="empty.py"', empty_line) - self.assertIn('line-rate="1"', empty_line) + cov.xml_report([empty]) + dom = ElementTree.parse("coverage.xml") + elts = dom.findall(".//class[@name='empty.py']") + assert len(elts) == 1 + assert elts[0].get('filename') == "empty.py" + assert elts[0].get('line-rate') == '1' def test_empty_file_is_100_not_0(self): # https://bitbucket.org/ned/coveragepy/issue/345 cov = self.run_doit() - cov.xml_report(outfile="-") - xml = self.stdout() - init_line = re_line(xml, 'filename="sub/__init__.py"') - self.assertIn('line-rate="1"', init_line) + cov.xml_report() + dom = ElementTree.parse("coverage.xml") + elts = dom.findall(".//class[@name='__init__.py']") + assert len(elts) == 1 + assert elts[0].get('line-rate') == '1' def test_curdir_source(self): # With no source= option, the XML report should explain that the source # is in the current directory. cov = self.run_doit() - cov.xml_report(outfile="-") - xml = self.stdout() - self.assert_source(xml, ".") - self.assertEqual(xml.count('<source>'), 1) + cov.xml_report() + dom = ElementTree.parse("coverage.xml") + self.assert_source(dom, ".") + sources = dom.findall(".//source") + assert len(sources) == 1 def test_deep_source(self): # When using source=, the XML report needs to mention those directories @@ -170,21 +202,33 @@ class XmlReportTest(XmlTestHelpers, CoverageTest): mod_foo = import_local_file("foo", "src/main/foo.py") # pragma: nested mod_bar = import_local_file("bar", "also/over/there/bar.py") # pragma: nested cov.stop() # pragma: nested - cov.xml_report([mod_foo, mod_bar], outfile="-") - xml = self.stdout() - - self.assert_source(xml, "src/main") - self.assert_source(xml, "also/over/there") - self.assertEqual(xml.count('<source>'), 2) - - self.assertIn( - '<class branch-rate="0" complexity="0" filename="foo.py" line-rate="1" name="foo.py">', - xml - ) - self.assertIn( - '<class branch-rate="0" complexity="0" filename="bar.py" line-rate="1" name="bar.py">', - xml - ) + cov.xml_report([mod_foo, mod_bar]) + dom = ElementTree.parse("coverage.xml") + + self.assert_source(dom, "src/main") + self.assert_source(dom, "also/over/there") + sources = dom.findall(".//source") + assert len(sources) == 2 + + foo_class = dom.findall(".//class[@name='foo.py']") + assert len(foo_class) == 1 + assert foo_class[0].attrib == { + 'branch-rate': '0', + 'complexity': '0', + 'filename': 'foo.py', + 'line-rate': '1', + 'name': 'foo.py', + } + + bar_class = dom.findall(".//class[@name='bar.py']") + assert len(bar_class) == 1 + assert bar_class[0].attrib == { + 'branch-rate': '0', + 'complexity': '0', + 'filename': 'bar.py', + 'line-rate': '1', + 'name': 'bar.py', + } def test_nonascii_directory(self): # https://bitbucket.org/ned/coveragepy/issues/573/cant-generate-xml-report-if-some-source @@ -195,22 +239,33 @@ class XmlReportTest(XmlTestHelpers, CoverageTest): cov.xml_report() +def unbackslash(v): + """Find strings in `v`, and replace backslashes with slashes throughout.""" + if isinstance(v, (tuple, list)): + return [unbackslash(vv) for vv in v] + elif isinstance(v, dict): + return {k: unbackslash(vv) for k, vv in v.items()} + else: + assert isinstance(v, str) + return v.replace("\\", "/") + + class XmlPackageStructureTest(XmlTestHelpers, CoverageTest): """Tests about the package structure reported in the coverage.xml file.""" def package_and_class_tags(self, cov): """Run an XML report on `cov`, and get the package and class tags.""" - self.captured_stdout.truncate(0) - cov.xml_report(outfile="-") - packages_and_classes = re_lines(self.stdout(), r"<package |<class ") - scrubs = r' branch-rate="0"| complexity="0"| line-rate="[\d.]+"' - return clean(packages_and_classes, scrubs) + cov.xml_report() + dom = ElementTree.parse("coverage.xml") + for node in dom.iter(): + if node.tag in ('package', 'class'): + yield (node.tag, {a:v for a,v in node.items() if a in ('name', 'filename')}) def assert_package_and_class_tags(self, cov, result): """Check the XML package and class tags from `cov` match `result`.""" - self.assertMultiLineEqual( - self.package_and_class_tags(cov), - clean(result) + self.assertEqual( + unbackslash(list(self.package_and_class_tags(cov))), + unbackslash(result), ) def test_package_names(self): @@ -220,18 +275,18 @@ class XmlPackageStructureTest(XmlTestHelpers, CoverageTest): """) cov = coverage.Coverage(source=".") self.start_import_stop(cov, "main") - self.assert_package_and_class_tags(cov, """\ - <package name="."> - <class filename="main.py" name="main.py"> - <package name="d0"> - <class filename="d0/__init__.py" name="__init__.py"> - <class filename="d0/f0.py" name="f0.py"> - <package name="d0.d0"> - <class filename="d0/d0/__init__.py" name="__init__.py"> - <class filename="d0/d0/f0.py" name="f0.py"> - """) - - def test_package_depth(self): + self.assert_package_and_class_tags(cov, [ + ('package', {'name': "."}), + ('class', {'filename': "main.py", 'name': "main.py"}), + ('package', {'name': "d0"}), + ('class', {'filename': "d0/__init__.py", 'name': "__init__.py"}), + ('class', {'filename': "d0/f0.py", 'name': "f0.py"}), + ('package', {'name': "d0.d0"}), + ('class', {'filename': "d0/d0/__init__.py", 'name': "__init__.py"}), + ('class', {'filename': "d0/d0/f0.py", 'name': "f0.py"}), + ]) + + def test_package_depth_1(self): self.make_tree(width=1, depth=4) self.make_file("main.py", """\ from d0.d0 import f0 @@ -240,46 +295,62 @@ class XmlPackageStructureTest(XmlTestHelpers, CoverageTest): self.start_import_stop(cov, "main") cov.set_option("xml:package_depth", 1) - self.assert_package_and_class_tags(cov, """\ - <package name="."> - <class filename="main.py" name="main.py"> - <package name="d0"> - <class filename="d0/__init__.py" name="__init__.py"> - <class filename="d0/d0/__init__.py" name="d0/__init__.py"> - <class filename="d0/d0/d0/__init__.py" name="d0/d0/__init__.py"> - <class filename="d0/d0/d0/f0.py" name="d0/d0/f0.py"> - <class filename="d0/d0/f0.py" name="d0/f0.py"> - <class filename="d0/f0.py" name="f0.py"> + self.assert_package_and_class_tags(cov, [ + ('package', {'name': "."}), + ('class', {'filename': "main.py", 'name': "main.py"}), + ('package', {'name': "d0"}), + ('class', {'filename': "d0/__init__.py", 'name': "__init__.py"}), + ('class', {'filename': "d0/d0/__init__.py", 'name': "d0/__init__.py"}), + ('class', {'filename': "d0/d0/d0/__init__.py", 'name': "d0/d0/__init__.py"}), + ('class', {'filename': "d0/d0/d0/f0.py", 'name': "d0/d0/f0.py"}), + ('class', {'filename': "d0/d0/f0.py", 'name': "d0/f0.py"}), + ('class', {'filename': "d0/f0.py", 'name': "f0.py"}), + ]) + + def test_package_depth_2(self): + self.make_tree(width=1, depth=4) + self.make_file("main.py", """\ + from d0.d0 import f0 """) + cov = coverage.Coverage(source=".") + self.start_import_stop(cov, "main") cov.set_option("xml:package_depth", 2) - self.assert_package_and_class_tags(cov, """\ - <package name="."> - <class filename="main.py" name="main.py"> - <package name="d0"> - <class filename="d0/__init__.py" name="__init__.py"> - <class filename="d0/f0.py" name="f0.py"> - <package name="d0.d0"> - <class filename="d0/d0/__init__.py" name="__init__.py"> - <class filename="d0/d0/d0/__init__.py" name="d0/__init__.py"> - <class filename="d0/d0/d0/f0.py" name="d0/f0.py"> - <class filename="d0/d0/f0.py" name="f0.py"> + self.assert_package_and_class_tags(cov, [ + ('package', {'name': "."}), + ('class', {'filename': "main.py", 'name': "main.py"}), + ('package', {'name': "d0"}), + ('class', {'filename': "d0/__init__.py", 'name': "__init__.py"}), + ('class', {'filename': "d0/f0.py", 'name': "f0.py"}), + ('package', {'name': "d0.d0"}), + ('class', {'filename': "d0/d0/__init__.py", 'name': "__init__.py"}), + ('class', {'filename': "d0/d0/d0/__init__.py", 'name': "d0/__init__.py"}), + ('class', {'filename': "d0/d0/d0/f0.py", 'name': "d0/f0.py"}), + ('class', {'filename': "d0/d0/f0.py", 'name': "f0.py"}), + ]) + + def test_package_depth_3(self): + self.make_tree(width=1, depth=4) + self.make_file("main.py", """\ + from d0.d0 import f0 """) + cov = coverage.Coverage(source=".") + self.start_import_stop(cov, "main") cov.set_option("xml:package_depth", 3) - self.assert_package_and_class_tags(cov, """\ - <package name="."> - <class filename="main.py" name="main.py"> - <package name="d0"> - <class filename="d0/__init__.py" name="__init__.py"> - <class filename="d0/f0.py" name="f0.py"> - <package name="d0.d0"> - <class filename="d0/d0/__init__.py" name="__init__.py"> - <class filename="d0/d0/f0.py" name="f0.py"> - <package name="d0.d0.d0"> - <class filename="d0/d0/d0/__init__.py" name="__init__.py"> - <class filename="d0/d0/d0/f0.py" name="f0.py"> - """) + self.assert_package_and_class_tags(cov, [ + ('package', {'name': "."}), + ('class', {'filename': "main.py", 'name': "main.py"}), + ('package', {'name': "d0"}), + ('class', {'filename': "d0/__init__.py", 'name': "__init__.py"}), + ('class', {'filename': "d0/f0.py", 'name': "f0.py"}), + ('package', {'name': "d0.d0"}), + ('class', {'filename': "d0/d0/__init__.py", 'name': "__init__.py"}), + ('class', {'filename': "d0/d0/f0.py", 'name': "f0.py"}), + ('package', {'name': "d0.d0.d0"}), + ('class', {'filename': "d0/d0/d0/__init__.py", 'name': "__init__.py"}), + ('class', {'filename': "d0/d0/d0/f0.py", 'name': "f0.py"}), + ]) def test_source_prefix(self): # https://bitbucket.org/ned/coveragepy/issues/465 @@ -287,26 +358,13 @@ class XmlPackageStructureTest(XmlTestHelpers, CoverageTest): self.make_file("src/mod.py", "print(17)") cov = coverage.Coverage(source=["src"]) self.start_import_stop(cov, "mod", modfile="src/mod.py") - self.assert_package_and_class_tags(cov, """\ - <package name="."> - <class filename="mod.py" name="mod.py"> - """) - xml = self.stdout() - self.assert_source(xml, "src") - - -def clean(text, scrub=None): - """Clean text to prepare it for comparison. - - Remove text matching `scrub`, and leading whitespace. Convert backslashes - to forward slashes. - """ - if scrub: - text = re.sub(scrub, "", text) - text = re.sub(r"(?m)^\s+", "", text) - text = re.sub(r"\\", "/", text) - return text + self.assert_package_and_class_tags(cov, [ + ('package', {'name': "."}), + ('class', {'filename': "mod.py", 'name': "mod.py"}), + ]) + dom = ElementTree.parse("coverage.xml") + self.assert_source(dom, "src") def compare_xml(expected, actual, **kwargs): @@ -2,7 +2,7 @@ # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt [tox] -envlist = py{27,34,35,36,37,38}, pypy{2,3}, doc, lint +envlist = py{27,35,36,37,38}, pypy{2,3}, doc, lint skip_missing_interpreters = {env:COVERAGE_SKIP_MISSING_INTERPRETERS:True} toxworkdir = {env:TOXWORKDIR:.tox} @@ -13,12 +13,12 @@ deps = # Check here for what might be out of date: # https://requires.io/github/nedbat/coveragepy/requirements/ -r requirements/pytest.pip - pip==18.1 - setuptools==40.6.3 + pip==19.0.3 + setuptools==40.8.0 # gevent 1.3 causes a failure: https://github.com/nedbat/coveragepy/issues/663 - py{27,34,35,36}: gevent==1.2.2 - py{27,34,35,36,37,38}: eventlet==0.24.1 - py{27,34,35,36,37,38}: greenlet==0.4.15 + py{27,35,36}: gevent==1.2.2 + py{27,35,36,37,38}: eventlet==0.24.1 + py{27,35,36,37,38}: greenlet==0.4.15 # Windows can't update the pip version with pip running, so use Python # to install things. @@ -85,7 +85,7 @@ setenv = commands = python -m tabnanny {env:LINTABLE} python igor.py check_eol - check-manifest --ignore 'lab*,perf*,doc/sample_html*,.treerc' + check-manifest --ignore 'lab*,perf*,doc/sample_html*,.treerc,.github*' python setup.py -q sdist bdist_wheel twine check dist/* python -m pylint --notes= {env:LINTABLE} @@ -94,8 +94,8 @@ commands = #2.7: py27, lint python = 2.7: py27 - 3.4: py34 3.5: py35 3.6: py36 + 3.7: py37 pypy: pypy pypy3.5: pypy3 diff --git a/tox_wheels.ini b/tox_wheels.ini index 7381cafb..a4854605 100644 --- a/tox_wheels.ini +++ b/tox_wheels.ini @@ -2,7 +2,7 @@ # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt [tox] -envlist = py{27,34,35,36,37,sys} +envlist = py{27,35,36,37,38,sys} toxworkdir = {toxinidir}/.tox_kits [testenv] |