diff -Nru path.py-11.0.1/CHANGES.rst path.py-11.5.0/CHANGES.rst --- path.py-11.0.1/CHANGES.rst 2018-03-26 18:33:23.000000000 +0000 +++ path.py-11.5.0/CHANGES.rst 2018-10-02 19:48:33.000000000 +0000 @@ -1,3 +1,68 @@ +11.5.0 +------ + +- #156: Re-wrote the handling of pattern matches for + ``listdir``, ``walk``, and related methods, allowing + the pattern to be a more complex object. This approach + drastically simplifies the code and obviates the + ``CaseInsensitivePattern`` and ``FastPath`` classes. + Now the main ``Path`` class should be as performant + as ``FastPath`` and case-insensitive matches can be + readily constructed using the new + ``path.matchers.CaseInsensitive`` class. + +11.4.1 +------ + +- #153: Skip intermittently failing performance test on + Python 2. + +11.4.0 +------ + +- #130: Path.py now supports non-decodable filenames on + Linux and Python 2, leveraging the + `backports.os `_ + package (as an optional dependency). Currently, only + ``listdir`` is patched, but other ``os`` primitives may + be patched similarly in the ``patch_for_linux_python2`` + function. + +- #141: For merge_tree, instead of relying on the deprecated + distutils module, implement merge_tree explicitly. The + ``update`` parameter is deprecated, instead superseded + by a ``copy_function`` parameter and an ``only_newer`` + wrapper for any copy function. + +11.3.0 +------ + +- #151: No longer use two techniques for splitting lines. + Instead, unconditionally rely on io.open for universal + newlines support and always use splitlines. + +11.2.0 +------ + +- #146: Rely on `importlib_metadata + `_ instead of + setuptools/pkg_resources to load the version of the module. + Added tests ensuring a <100ms import time for the ``path`` + module. This change adds an explicit dependency on the + importlib_metadata package, but the project still supports + copying of the ``path.py`` module without any dependencies. + +11.1.0 +------ + +- #143, #144: Add iglob method. +- #142, #145: Rename ``tempdir`` to ``TempDir`` and declare + it as part of ``__all__``. Retain ``tempdir`` for compatibility + for now. +- #145: ``TempDir.__enter__`` no longer returns the ``TempDir`` + instance, but instead returns a ``Path`` instance, suitable for + entering to change the current working directory. + 11.0.1 ------ @@ -99,7 +164,8 @@ --- - Refreshed project metadata based on `jaraco's project - skeleton _. + skeleton `_. + - Releases are now automatically published via Travis-CI. - #111: More aggressively trap errors when importing ``pkg_resources``. diff -Nru path.py-11.0.1/debian/changelog path.py-11.5.0/debian/changelog --- path.py-11.0.1/debian/changelog 2018-04-23 06:19:55.000000000 +0000 +++ path.py-11.5.0/debian/changelog 2018-11-23 07:55:24.000000000 +0000 @@ -1,8 +1,49 @@ +path.py (11.5.0-3) unstable; urgency=medium + + * Team upload. + * Fix autopkgtests, replace them with autodep8 Python tests. + + -- Ondřej Nový Fri, 23 Nov 2018 08:55:24 +0100 + +path.py (11.5.0-2) unstable; urgency=medium + + * Modify how the upstream test suite gets run (Closes: #911488). + + -- Julien Puydt Mon, 22 Oct 2018 10:19:28 +0200 + +path.py (11.5.0-1) unstable; urgency=medium + + * New upstream release. + * Drop upstreamed patch (to remove dep on distutils). + * Add patch to revert an upstream change in version detection + (using upstream's code would require a new package!) + + -- Julien Puydt Fri, 19 Oct 2018 17:20:04 +0200 + +path.py (11.3.0-1) experimental; urgency=medium + + [ Ondřej Nový ] + * d/changelog: Remove trailing whitespaces + * d/control: Remove ancient X-Python-Version field + * d/control: Remove ancient X-Python3-Version field + + [ Matthias Klose ] + * Add a patch to remove dep on python3-distutils + (Closes: #896271) + + [ Julien Puydt ] + * Bump std-ver to 4.2.1. + * Use my debian.org mail address. + * New upstream release. + * Replace Matthias' patch for distutils with an upstream one. + + -- Julien Puydt Tue, 18 Sep 2018 22:44:24 +0200 + path.py (11.0.1-2) unstable; urgency=medium * Refresh packaging: - Bump std-ver to 4.1.4. - - Bump dh compat to 11. + - Bump dh compat to 11. * Add tests for import in autopkgtest. * Add missing depends for python3-path (Closes: #896271). diff -Nru path.py-11.0.1/debian/control path.py-11.5.0/debian/control --- path.py-11.0.1/debian/control 2018-04-23 06:19:55.000000000 +0000 +++ path.py-11.5.0/debian/control 2018-11-23 07:35:57.000000000 +0000 @@ -1,9 +1,9 @@ Source: path.py Maintainer: Debian Python Modules Team -Uploaders: Julien Puydt +Uploaders: Julien Puydt Section: python Priority: optional -Standards-Version: 4.1.4 +Standards-Version: 4.2.1 Homepage: https://github.com/jaraco/path.py Build-Depends: debhelper (>= 11), dh-python (>= 2.20160609~), @@ -15,10 +15,9 @@ python3-pytest, python3-setuptools, python3-setuptools-scm -X-Python-Version: >= 2.7 -X-Python3-Version: >= 3.2 Vcs-Git: https://salsa.debian.org/python-team/modules/path.py.git Vcs-Browser: https://salsa.debian.org/python-team/modules/path.py +Testsuite: autopkgtest-pkg-python Package: python-path Architecture: all @@ -31,7 +30,7 @@ Package: python3-path Architecture: all -Depends: ${misc:Depends}, ${python3:Depends}, python3-distutils +Depends: ${misc:Depends}, ${python3:Depends} Description: module wrapper for os.path for Python 3 path.py implements a path objects as first-class entities, allowing common operations on files to be invoked on those path objects directly. diff -Nru path.py-11.0.1/debian/copyright path.py-11.5.0/debian/copyright --- path.py-11.0.1/debian/copyright 2018-04-23 06:19:55.000000000 +0000 +++ path.py-11.5.0/debian/copyright 2018-11-23 07:33:32.000000000 +0000 @@ -7,7 +7,7 @@ License: Expat Files: debian/* -Copyright: 2015-2017 Julien Puydt +Copyright: 2015-2018 Julien Puydt License: Expat License: Expat diff -Nru path.py-11.0.1/debian/patches/missing_flake8.patch path.py-11.5.0/debian/patches/missing_flake8.patch --- path.py-11.0.1/debian/patches/missing_flake8.patch 2018-04-23 06:19:55.000000000 +0000 +++ path.py-11.5.0/debian/patches/missing_flake8.patch 2018-11-23 07:33:32.000000000 +0000 @@ -5,9 +5,11 @@ --- a/pytest.ini +++ b/pytest.ini -@@ -1,4 +1,4 @@ +@@ -1,6 +1,6 @@ [pytest] norecursedirs=dist build .tox .eggs -addopts=--doctest-modules --flake8 +addopts=--doctest-modules doctest_optionflags=ALLOW_UNICODE ELLIPSIS + filterwarnings= + ignore:Possible nested set::pycodestyle:113 diff -Nru path.py-11.0.1/debian/patches/series path.py-11.5.0/debian/patches/series --- path.py-11.0.1/debian/patches/series 2018-04-23 06:19:55.000000000 +0000 +++ path.py-11.5.0/debian/patches/series 2018-11-23 07:33:32.000000000 +0000 @@ -1 +1,2 @@ missing_flake8.patch +version_with_pkgresources.patch diff -Nru path.py-11.0.1/debian/patches/version_with_pkgresources.patch path.py-11.5.0/debian/patches/version_with_pkgresources.patch --- path.py-11.0.1/debian/patches/version_with_pkgresources.patch 1970-01-01 00:00:00.000000000 +0000 +++ path.py-11.5.0/debian/patches/version_with_pkgresources.patch 2018-11-23 07:33:32.000000000 +0000 @@ -0,0 +1,20 @@ +Author: Julien Puydt +Description: don't require another package just to find out about the version +Forwarded: no + +--- a/path.py ++++ b/path.py +@@ -103,12 +103,7 @@ + U_NL_END = re.compile(r'(?:{0})$'.format(U_NEWLINE.pattern)) + + +-try: +- import importlib_metadata +- __version__ = importlib_metadata.version('path.py') +-except Exception: +- __version__ = 'unknown' +- ++__version__ = @VERSION@ + + class TreeWalkWarning(Warning): + pass diff -Nru path.py-11.0.1/debian/rules path.py-11.5.0/debian/rules --- path.py-11.0.1/debian/rules 2018-04-23 06:19:55.000000000 +0000 +++ path.py-11.5.0/debian/rules 2018-11-23 07:33:32.000000000 +0000 @@ -1,13 +1,20 @@ #!/usr/bin/make -f -#export DH_VERBOSE=1 +include /usr/share/dpkg/pkg-info.mk + export PYBUILD_NAME=path export LC_ALL=C.UTF-8 -export PYBUILD_TEST_ARGS=test_path.py +export PYBUILD_TEST_ARGS_python3=test_path.py +# it's not even clear why those tests should succeed... +export PYBUILD_TEST_ARGS_python2=$(PYBUILD_TEST_ARGS_python3) -k "not test_listdir_other_encoding" %: dh $@ --with python2,python3 --buildsystem=pybuild +override_dh_auto_build: + sed -i path.py -e "s/@VERSION@/'$(DEB_VERSION_UPSTREAM)'/g" + dh_auto_build + override_dh_install: dh_install rm debian/python*-path/usr/lib/python*/dist-packages/test_path.py diff -Nru path.py-11.0.1/debian/tests/control path.py-11.5.0/debian/tests/control --- path.py-11.0.1/debian/tests/control 2018-04-23 06:19:55.000000000 +0000 +++ path.py-11.5.0/debian/tests/control 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -Tests: import2 -Depends: python-path - -Tests: import3 -Depends: python3-path - -Tests: upstream2 -Depends: python-path, python-pytest, python-setuptools, python-packaging - -Tests: upstream3 -Depends: python3-path, python3-pytest, python3-setuptools, python3-packaging diff -Nru path.py-11.0.1/debian/tests/control.autodep8 path.py-11.5.0/debian/tests/control.autodep8 --- path.py-11.0.1/debian/tests/control.autodep8 1970-01-01 00:00:00.000000000 +0000 +++ path.py-11.5.0/debian/tests/control.autodep8 2018-11-23 07:35:38.000000000 +0000 @@ -0,0 +1,5 @@ +Tests: upstream2 +Depends: python-path, python-pytest, python-setuptools, python-packaging + +Tests: upstream3 +Depends: python3-path, python3-pytest, python3-setuptools, python3-packaging diff -Nru path.py-11.0.1/debian/tests/import2 path.py-11.5.0/debian/tests/import2 --- path.py-11.0.1/debian/tests/import2 2018-04-23 06:19:55.000000000 +0000 +++ path.py-11.5.0/debian/tests/import2 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -#!/bin/sh -set -e -python2 -c "import path" diff -Nru path.py-11.0.1/debian/tests/import3 path.py-11.5.0/debian/tests/import3 --- path.py-11.0.1/debian/tests/import3 2018-04-23 06:19:55.000000000 +0000 +++ path.py-11.5.0/debian/tests/import3 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -#!/bin/sh -set -e -python3 -c "import path" diff -Nru path.py-11.0.1/debian/tests/upstream2 path.py-11.5.0/debian/tests/upstream2 --- path.py-11.0.1/debian/tests/upstream2 2018-04-23 06:19:55.000000000 +0000 +++ path.py-11.5.0/debian/tests/upstream2 2018-11-23 07:33:32.000000000 +0000 @@ -1,2 +1,3 @@ #!/bin/sh +mv path.py path.py.bak python2 test_path.py diff -Nru path.py-11.0.1/debian/tests/upstream3 path.py-11.5.0/debian/tests/upstream3 --- path.py-11.0.1/debian/tests/upstream3 2018-04-23 06:19:55.000000000 +0000 +++ path.py-11.5.0/debian/tests/upstream3 2018-11-23 07:33:32.000000000 +0000 @@ -1,2 +1,3 @@ #!/bin/sh +mv path.py path.py.bak python3 test_path.py diff -Nru path.py-11.0.1/Dockerfile path.py-11.5.0/Dockerfile --- path.py-11.0.1/Dockerfile 1970-01-01 00:00:00.000000000 +0000 +++ path.py-11.5.0/Dockerfile 2018-10-02 19:48:33.000000000 +0000 @@ -0,0 +1,9 @@ +from ubuntu:bionic +RUN apt update +RUN apt install -y python python-pip git +RUN python -m pip install tox +RUN mkdir /app +ENV LANG=C.UTF-8 +WORKDIR /app +COPY . . +CMD tox diff -Nru path.py-11.0.1/.dockerignore path.py-11.5.0/.dockerignore --- path.py-11.0.1/.dockerignore 1970-01-01 00:00:00.000000000 +0000 +++ path.py-11.5.0/.dockerignore 2018-10-02 19:48:33.000000000 +0000 @@ -0,0 +1 @@ +.tox diff -Nru path.py-11.0.1/docs/conf.py path.py-11.5.0/docs/conf.py --- path.py-11.0.1/docs/conf.py 2018-03-26 18:33:23.000000000 +0000 +++ path.py-11.5.0/docs/conf.py 2018-10-02 19:48:33.000000000 +0000 @@ -10,7 +10,7 @@ pygments_style = 'sphinx' html_theme = 'alabaster' -html_static_path = ['_static'] +html_static_path = [] htmlhelp_basename = 'pathpydoc' templates_path = ['_templates'] exclude_patterns = ['_build'] @@ -26,7 +26,7 @@ ), replace=[ dict( - pattern=r'(Issue )?#(?P\d+)', + pattern=r'(Issue #|\B#)(?P\d+)', url='{package_url}/issues/{issue}', ), dict( diff -Nru path.py-11.0.1/.flake8 path.py-11.5.0/.flake8 --- path.py-11.0.1/.flake8 2018-03-26 18:33:23.000000000 +0000 +++ path.py-11.5.0/.flake8 2018-10-02 19:48:33.000000000 +0000 @@ -1,2 +1,8 @@ [flake8] -ignore = W191,W503 +ignore = + # Allow tabs for indentation + W191 + # W503 violates spec https://github.com/PyCQA/pycodestyle/issues/513 + W503 + # W504 has issues https://github.com/OCA/maintainer-quality-tools/issues/545 + W504 diff -Nru path.py-11.0.1/path.py path.py-11.5.0/path.py --- path.py-11.0.1/path.py 2018-03-26 18:33:23.000000000 +0000 +++ path.py-11.5.0/path.py 2018-10-02 19:48:33.000000000 +0000 @@ -1,25 +1,3 @@ -# -# Copyright (c) 2010 Mikhail Gusarov -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# - """ path.py - An object representing a path to a file or directory. @@ -29,8 +7,18 @@ from path import Path d = Path('/home/guido/bin') + + # Globbing for f in d.files('*.py'): f.chmod(0o755) + + # Changing the working directory: + with Path("somewhere"): + # cwd in now `somewhere` + ... + + # Concatenate paths with / + foo_txt = Path("bar") / "foo.txt" """ from __future__ import unicode_literals @@ -49,9 +37,10 @@ import re import contextlib import io -import distutils.dir_util import importlib import itertools +import platform +import ntpath try: import win32security @@ -84,6 +73,9 @@ text_type = __builtin__.unicode getcwdu = os.getcwdu map = itertools.imap + filter = itertools.ifilter + FileNotFoundError = OSError + itertools.filterfalse = itertools.ifilterfalse @contextlib.contextmanager @@ -100,7 +92,7 @@ ############################################################################## -__all__ = ['Path', 'CaseInsensitivePattern'] +__all__ = ['Path', 'TempDir', 'CaseInsensitivePattern'] LINESEPS = ['\r\n', '\r', '\n'] @@ -112,8 +104,8 @@ try: - import pkg_resources - __version__ = pkg_resources.require('path.py')[0].version + import importlib_metadata + __version__ = importlib_metadata.version('path.py') except Exception: __version__ = 'unknown' @@ -163,6 +155,60 @@ ) +class matchers(object): + # TODO: make this class a module + + @staticmethod + def load(param): + """ + If the supplied parameter is a string, assum it's a simple + pattern. + """ + return ( + matchers.Pattern(param) if isinstance(param, string_types) + else param if param is not None + else matchers.Null() + ) + + class Base(object): + pass + + class Null(Base): + def __call__(self, path): + return True + + class Pattern(Base): + def __init__(self, pattern): + self.pattern = pattern + + def get_pattern(self, normcase): + try: + return self._pattern + except AttributeError: + pass + self._pattern = normcase(self.pattern) + return self._pattern + + def __call__(self, path): + normcase = getattr(self, 'normcase', path.module.normcase) + pattern = self.get_pattern(normcase) + return fnmatch.fnmatchcase(normcase(path.name), pattern) + + class CaseInsensitive(Pattern): + """ + A Pattern with a ``'normcase'`` property, suitable for passing to + :meth:`listdir`, :meth:`dirs`, :meth:`files`, :meth:`walk`, + :meth:`walkdirs`, or :meth:`walkfiles` to match case-insensitive. + + For example, to get all files ending in .py, .Py, .pY, or .PY in the + current directory:: + + from path import Path, matchers + Path('.').files(matchers.CaseInsensitive('*.py')) + """ + normcase = staticmethod(ntpath.normcase) + + class Path(text_type): """ Represents a filesystem path. @@ -525,7 +571,7 @@ # --- Listing, searching, walking, and matching - def listdir(self, pattern=None): + def listdir(self, match=None): """ D.listdir() -> List of items in this directory. Use :meth:`files` or :meth:`dirs` instead if you want a listing @@ -533,46 +579,39 @@ The elements of the list are Path objects. - With the optional `pattern` argument, this only lists - items whose names match the given pattern. + With the optional `match` argument, a callable, + only return items whose names match the given pattern. .. seealso:: :meth:`files`, :meth:`dirs` """ - if pattern is None: - pattern = '*' - return [ - self / child - for child in os.listdir(self) - if self._next_class(child).fnmatch(pattern) - ] + match = matchers.load(match) + return list(filter(match, ( + self / child for child in os.listdir(self) + ))) - def dirs(self, pattern=None): + def dirs(self, *args, **kwargs): """ D.dirs() -> List of this directory's subdirectories. The elements of the list are Path objects. This does not walk recursively into subdirectories (but see :meth:`walkdirs`). - With the optional `pattern` argument, this only lists - directories whose names match the given pattern. For - example, ``d.dirs('build-*')``. + Accepts parameters to :meth:`listdir`. """ - return [p for p in self.listdir(pattern) if p.isdir()] + return [p for p in self.listdir(*args, **kwargs) if p.isdir()] - def files(self, pattern=None): + def files(self, *args, **kwargs): """ D.files() -> List of the files in this directory. The elements of the list are Path objects. This does not walk into subdirectories (see :meth:`walkfiles`). - With the optional `pattern` argument, this only lists files - whose names match the given pattern. For example, - ``d.files('*.pyc')``. + Accepts parameters to :meth:`listdir`. """ - return [p for p in self.listdir(pattern) if p.isfile()] + return [p for p in self.listdir(*args, **kwargs) if p.isfile()] - def walk(self, pattern=None, errors='strict'): + def walk(self, match=None, errors='strict'): """ D.walk() -> iterator over files and subdirs, recursively. The iterator yields Path objects naming each child item of @@ -602,6 +641,8 @@ raise ValueError("invalid errors parameter") errors = vars(Handlers).get(errors, errors) + match = matchers.load(match) + try: childList = self.listdir() except Exception: @@ -612,7 +653,7 @@ return for child in childList: - if pattern is None or child.fnmatch(pattern): + if match(child): yield child try: isdir = child.isdir() @@ -624,92 +665,26 @@ isdir = False if isdir: - for item in child.walk(pattern, errors): + for item in child.walk(errors=errors, match=match): yield item - def walkdirs(self, pattern=None, errors='strict'): + def walkdirs(self, *args, **kwargs): """ D.walkdirs() -> iterator over subdirs, recursively. - - With the optional `pattern` argument, this yields only - directories whose names match the given pattern. For - example, ``mydir.walkdirs('*test')`` yields only directories - with names ending in ``'test'``. - - The `errors=` keyword argument controls behavior when an - error occurs. The default is ``'strict'``, which causes an - exception. The other allowed values are ``'warn'`` (which - reports the error via :func:`warnings.warn()`), and ``'ignore'``. """ - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - try: - dirs = self.dirs() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - - for child in dirs: - if pattern is None or child.fnmatch(pattern): - yield child - for subsubdir in child.walkdirs(pattern, errors): - yield subsubdir + return ( + item + for item in self.walk(*args, **kwargs) + if item.isdir() + ) - def walkfiles(self, pattern=None, errors='strict'): + def walkfiles(self, *args, **kwargs): """ D.walkfiles() -> iterator over files in D, recursively. - - The optional argument `pattern` limits the results to files - with names that match the pattern. For example, - ``mydir.walkfiles('*.tmp')`` yields only files with the ``.tmp`` - extension. """ - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - try: - childList = self.listdir() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - - for child in childList: - try: - isfile = child.isfile() - isdir = not isfile and child.isdir() - except Exception: - if errors == 'ignore': - continue - elif errors == 'warn': - warnings.warn( - "Unable to access '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - continue - else: - raise - - if isfile: - if pattern is None or child.fnmatch(pattern): - yield child - elif isdir: - for f in child.walkfiles(pattern, errors): - yield f + return ( + item + for item in self.walk(*args, **kwargs) + if item.isfile() + ) def fnmatch(self, pattern, normcase=None): """ Return ``True`` if `self.name` matches the given `pattern`. @@ -739,10 +714,32 @@ of all the files users have in their :file:`bin` directories. .. seealso:: :func:`glob.glob` + + .. note:: Glob is **not** recursive, even when using ``**``. + To do recursive globbing see :func:`walk`, + :func:`walkdirs` or :func:`walkfiles`. """ cls = self._next_class return [cls(s) for s in glob.glob(self / pattern)] + def iglob(self, pattern): + """ Return an iterator of Path objects that match the pattern. + + `pattern` - a path relative to this directory, with wildcards. + + For example, ``Path('/users').iglob('*/bin/*')`` returns an + iterator of all the files users have in their :file:`bin` + directories. + + .. seealso:: :func:`glob.iglob` + + .. note:: Glob is **not** recursive, even when using ``**``. + To do recursive globbing see :func:`walk`, + :func:`walkdirs` or :func:`walkfiles`. + """ + cls = self._next_class + return (cls(s) for s in glob.iglob(self / pattern)) + # # --- Reading or writing an entire file at once. @@ -891,15 +888,9 @@ translated to ``'\n'``. If ``False``, newline characters are stripped off. Default is ``True``. - This uses ``'U'`` mode. - .. seealso:: :meth:`text` """ - if encoding is None and retain: - with self.open('U') as f: - return f.readlines() - else: - return self.text(encoding, errors).splitlines(retain) + return self.text(encoding, errors).splitlines(retain) def write_lines(self, lines, encoding=None, errors='strict', linesep=os.linesep, append=False): @@ -1289,9 +1280,8 @@ file does not exist. """ try: self.unlink() - except OSError: - _, e, _ = sys.exc_info() - if e.errno != errno.ENOENT: + except FileNotFoundError as exc: + if PY2 and exc.errno != errno.ENOENT: raise return self @@ -1385,34 +1375,60 @@ cd = chdir - def merge_tree(self, dst, symlinks=False, *args, **kwargs): + def merge_tree( + self, dst, symlinks=False, + # * + update=False, + copy_function=shutil.copy2, + ignore=lambda dir, contents: []): """ Copy entire contents of self to dst, overwriting existing contents in dst with those in self. - If the additional keyword `update` is True, each - `src` will only be copied if `dst` does not exist, - or `src` is newer than `dst`. - - Note that the technique employed stages the files in a temporary - directory first, so this function is not suitable for merging - trees with large files, especially if the temporary directory - is not capable of storing a copy of the entire source tree. - """ - update = kwargs.pop('update', False) - with tempdir() as _temp_dir: - # first copy the tree to a stage directory to support - # the parameters and behavior of copytree. - stage = _temp_dir / str(hash(self)) - self.copytree(stage, symlinks, *args, **kwargs) - # now copy everything from the stage directory using - # the semantics of dir_util.copy_tree - distutils.dir_util.copy_tree( - stage, - dst, - preserve_symlinks=symlinks, - update=update, + Pass ``symlinks=True`` to copy symbolic links as links. + + Accepts a ``copy_function``, similar to copytree. + + To avoid overwriting newer files, supply a copy function + wrapped in ``only_newer``. For example:: + + src.merge_tree(dst, copy_function=only_newer(shutil.copy2)) + """ + dst = self._next_class(dst) + dst.makedirs_p() + + if update: + warnings.warn( + "Update is deprecated; " + "use copy_function=only_newer(shutil.copy2)", + DeprecationWarning, + stacklevel=2, ) + copy_function = only_newer(copy_function) + + sources = self.listdir() + _ignored = ignore(self, [item.name for item in sources]) + + def ignored(item): + return item.name in _ignored + + for source in itertools.filterfalse(ignored, sources): + dest = dst / source.name + if symlinks and source.islink(): + target = source.readlink() + target.symlink(dest) + elif source.isdir(): + source.merge_tree( + dest, + symlinks=symlinks, + update=update, + copy_function=copy_function, + ignore=ignore, + ) + else: + copy_function(source, dest) + + self.copystat(dst) # # --- Special stuff from os @@ -1547,6 +1563,23 @@ return functools.partial(SpecialResolver, cls) +def only_newer(copy_func): + """ + Wrap a copy function (like shutil.copy2) to return + the dst if it's newer than the source. + """ + @functools.wraps(copy_func) + def wrapper(src, dst, *args, **kwargs): + is_newer_dst = ( + dst.exists() + and dst.getmtime() >= src.getmtime() + ) + if is_newer_dst: + return dst + return copy_func(src, dst, *args, **kwargs) + return wrapper + + class SpecialResolver(object): class ResolverScope: def __init__(self, paths, scope): @@ -1615,15 +1648,15 @@ ) -class tempdir(Path): +class TempDir(Path): """ A temporary directory via :func:`tempfile.mkdtemp`, and constructed with the same parameters that you can use as a context manager. - Example: + Example:: - with tempdir() as d: + with TempDir() as d: # do stuff with the Path object "d" # here the directory is deleted automatically @@ -1638,19 +1671,27 @@ def __new__(cls, *args, **kwargs): dirname = tempfile.mkdtemp(*args, **kwargs) - return super(tempdir, cls).__new__(cls, dirname) + return super(TempDir, cls).__new__(cls, dirname) def __init__(self, *args, **kwargs): pass def __enter__(self): - return self + # TempDir should return a Path version of itself and not itself + # so that a second context manager does not create a second + # temporary directory, but rather changes CWD to the location + # of the temporary directory. + return self._next_class(self) def __exit__(self, exc_type, exc_value, traceback): if not exc_value: self.rmtree() +# For backwards compatibility. +tempdir = TempDir + + def _multi_permission_mask(mode): """ Support multiple, comma-separated Unix chmod symbolic modes. @@ -1725,205 +1766,56 @@ return functools.partial(op_map[op], mask) -class CaseInsensitivePattern(text_type): - """ - A string with a ``'normcase'`` property, suitable for passing to - :meth:`listdir`, :meth:`dirs`, :meth:`files`, :meth:`walk`, - :meth:`walkdirs`, or :meth:`walkfiles` to match case-insensitive. - - For example, to get all files ending in .py, .Py, .pY, or .PY in the - current directory:: +class CaseInsensitivePattern(matchers.CaseInsensitive): + def __init__(self, value): + warnings.warn( + "Use matchers.CaseInsensitive instead", + DeprecationWarning, + stacklevel=2, + ) + super(CaseInsensitivePattern, self).__init__(value) - from path import Path, CaseInsensitivePattern as ci - Path('.').files(ci('*.py')) - """ - @property - def normcase(self): - return __import__('ntpath').normcase +class FastPath(Path): + def __init__(self, *args, **kwargs): + warnings.warn( + "Use Path, as FastPath no longer holds any advantage", + DeprecationWarning, + stacklevel=2, + ) + super(FastPath, self).__init__(*args, **kwargs) -class FastPath(Path): +def patch_for_linux_python2(): """ - Performance optimized version of Path for use - on embedded platforms and other systems with limited - CPU. See #115 and #116 for background. + As reported in #130, when Linux users create filenames + not in the file system encoding, it creates problems on + Python 2. This function attempts to patch the os module + to make it behave more like that on Python 3. """ + if not PY2 or platform.system() != 'Linux': + return - def listdir(self, pattern=None): - children = os.listdir(self) - if pattern is None: - return [self / child for child in children] - - pattern, normcase = self.__prepare(pattern) - return [ - self / child - for child in children - if self._next_class(child).__fnmatch(pattern, normcase) - ] - - def walk(self, pattern=None, errors='strict'): - class Handlers: - def strict(msg): - raise - - def warn(msg): - warnings.warn(msg, TreeWalkWarning) - - def ignore(msg): - pass - - if not callable(errors) and errors not in vars(Handlers): - raise ValueError("invalid errors parameter") - errors = vars(Handlers).get(errors, errors) - - if pattern: - pattern, normcase = self.__prepare(pattern) - else: - normcase = None - - return self.__walk(pattern, normcase, errors) - - def __walk(self, pattern, normcase, errors): - """ Prepared version of walk """ - try: - childList = self.listdir() - except Exception: - exc = sys.exc_info()[1] - tmpl = "Unable to list directory '%(self)s': %(exc)s" - msg = tmpl % locals() - errors(msg) - return - - for child in childList: - if pattern is None or child.__fnmatch(pattern, normcase): - yield child - try: - isdir = child.isdir() - except Exception: - exc = sys.exc_info()[1] - tmpl = "Unable to access '%(child)s': %(exc)s" - msg = tmpl % locals() - errors(msg) - isdir = False - - if isdir: - for item in child.__walk(pattern, normcase, errors): - yield item - - def walkdirs(self, pattern=None, errors='strict'): - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - if pattern: - pattern, normcase = self.__prepare(pattern) - else: - normcase = None - - return self.__walkdirs(pattern, normcase, errors) - - def __walkdirs(self, pattern, normcase, errors): - """ Prepared version of walkdirs """ - try: - dirs = self.dirs() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - - for child in dirs: - if pattern is None or child.__fnmatch(pattern, normcase): - yield child - for subsubdir in child.__walkdirs(pattern, normcase, errors): - yield subsubdir - - def walkfiles(self, pattern=None, errors='strict'): - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - if pattern: - pattern, normcase = self.__prepare(pattern) - else: - normcase = None + try: + import backports.os + except ImportError: + return - return self.__walkfiles(pattern, normcase, errors) + class OS: + """ + The proxy to the os module + """ + def __init__(self, wrapped): + self._orig = wrapped - def __walkfiles(self, pattern, normcase, errors): - """ Prepared version of walkfiles """ - try: - childList = self.listdir() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise + def __getattr__(self, name): + return getattr(self._orig, name) - for child in childList: - try: - isfile = child.isfile() - isdir = not isfile and child.isdir() - except Exception: - if errors == 'ignore': - continue - elif errors == 'warn': - warnings.warn( - "Unable to access '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - continue - else: - raise - - if isfile: - if pattern is None or child.__fnmatch(pattern, normcase): - yield child - elif isdir: - for f in child.__walkfiles(pattern, normcase, errors): - yield f - - def __fnmatch(self, pattern, normcase): - """ Return ``True`` if `self.name` matches the given `pattern`, - prepared version. - `pattern` - A filename pattern with wildcards, - for example ``'*.py'``. The pattern is expected to be normcase'd - already. - `normcase` - A function used to normalize the pattern and - filename before matching. - .. seealso:: :func:`Path.fnmatch` - """ - return fnmatch.fnmatchcase(normcase(self.name), pattern) + def listdir(self, *args, **kwargs): + items = self._orig.listdir(*args, **kwargs) + return list(map(backports.os.fsdecode, items)) - def __prepare(self, pattern, normcase=None): - """ Prepares a fmatch_pattern for use with ``FastPath.__fnmatch`. - `pattern` - A filename pattern with wildcards, - for example ``'*.py'``. If the pattern contains a `normcase` - attribute, it is applied to the name and path prior to comparison. - `normcase` - (optional) A function used to normalize the pattern and - filename before matching. Defaults to :meth:`self.module`, - which defaults to :meth:`os.path.normcase`. - .. seealso:: :func:`FastPath.__fnmatch` - """ - if not normcase: - normcase = getattr(pattern, 'normcase', self.module.normcase) - pattern = normcase(pattern) - return pattern, normcase + globals().update(os=OS(os)) - def fnmatch(self, pattern, normcase=None): - if not pattern: - raise ValueError("No pattern provided") - pattern, normcase = self.__prepare(pattern, normcase) - return self.__fnmatch(pattern, normcase) +patch_for_linux_python2() diff -Nru path.py-11.0.1/pyproject.toml path.py-11.5.0/pyproject.toml --- path.py-11.0.1/pyproject.toml 1970-01-01 00:00:00.000000000 +0000 +++ path.py-11.5.0/pyproject.toml 2018-10-02 19:48:33.000000000 +0000 @@ -0,0 +1,2 @@ +[build-system] +requires = ["setuptools>=30.3", "wheel", "setuptools_scm>=1.15"] diff -Nru path.py-11.0.1/pytest.ini path.py-11.5.0/pytest.ini --- path.py-11.0.1/pytest.ini 2018-03-26 18:33:23.000000000 +0000 +++ path.py-11.5.0/pytest.ini 2018-10-02 19:48:33.000000000 +0000 @@ -2,3 +2,5 @@ norecursedirs=dist build .tox .eggs addopts=--doctest-modules --flake8 doctest_optionflags=ALLOW_UNICODE ELLIPSIS +filterwarnings= + ignore:Possible nested set::pycodestyle:113 diff -Nru path.py-11.0.1/README.rst path.py-11.5.0/README.rst --- path.py-11.0.1/README.rst 2018-03-26 18:33:23.000000000 +0000 +++ path.py-11.5.0/README.rst 2018-10-02 19:48:33.000000000 +0000 @@ -6,8 +6,8 @@ .. image:: https://img.shields.io/travis/jaraco/path.py/master.svg :target: https://travis-ci.org/jaraco/path.py -.. image:: https://img.shields.io/appveyor/ci/jaraco/path.py/master.svg - :target: https://ci.appveyor.com/project/jaraco/path.py/branch/master +.. image:: https://img.shields.io/appveyor/ci/jaraco/path-py/master.svg + :target: https://ci.appveyor.com/project/jaraco/path-py/branch/master .. image:: https://readthedocs.org/projects/pathpy/badge/?version=latest :target: https://pathpy.readthedocs.io/en/latest/?badge=latest @@ -23,6 +23,18 @@ for f in d.files('*.py'): f.chmod(0o755) + # Globbing + for f in d.files('*.py'): + f.chmod(0o755) + + # Changing the working directory: + with Path("somewhere"): + # cwd in now `somewhere` + ... + + # Concatenate paths with / + foo_txt = Path("bar") / "foo.txt" + ``path.py`` is `hosted at Github `_. Find `the documentation here `_. @@ -64,7 +76,9 @@ objects may be passed directly to other APIs that expect simple text representations of paths, whereas with ``pathlib``, one must first cast values to strings before passing them to - APIs unaware of ``pathlib``. + APIs unaware of ``pathlib``. This shortcoming was `addressed + by PEP 519 `_, + in Python 3.6. - ``path.py`` goes beyond exposing basic functionality of a path and exposes commonly-used behaviors on a path, providing methods like ``rmtree`` (from shlib) and ``remove_p`` (remove @@ -72,6 +86,16 @@ - As a PyPI-hosted package, ``path.py`` is free to iterate faster than a stdlib package. Contributions are welcome and encouraged. +- ``path.py`` provides a uniform abstraction over its Path object, + freeing the implementer to subclass it readily. One cannot + subclass a ``pathlib.Path`` to add functionality, but must + subclass ``Path``, ``PosixPath``, and ``WindowsPath``, even + if one only wishes to add a ``__dict__`` to the subclass + instances. ``path.py`` instead allows the ``Path.module`` + object to be overridden by subclasses, defaulting to the + ``os.path``. Even advanced uses of ``path.Path`` that + subclass the model do not need to be concerned with + OS-specific nuances. Alternatives ============ diff -Nru path.py-11.0.1/setup.cfg path.py-11.5.0/setup.cfg --- path.py-11.0.1/setup.cfg 2018-03-26 18:33:23.000000000 +0000 +++ path.py-11.5.0/setup.cfg 2018-10-02 19:48:33.000000000 +0000 @@ -1,5 +1,4 @@ [aliases] -release = dists upload dists = clean --all sdist bdist_wheel [bdist_wheel] @@ -7,3 +6,4 @@ [metadata] license_file = LICENSE +long_description = file:README.rst diff -Nru path.py-11.0.1/setup.py path.py-11.5.0/setup.py --- path.py-11.0.1/setup.py 2018-03-26 18:33:23.000000000 +0000 +++ path.py-11.5.0/setup.py 2018-10-02 19:48:33.000000000 +0000 @@ -2,13 +2,8 @@ # Project skeleton maintained at https://github.com/jaraco/skeleton -import io - import setuptools -with io.open('README.rst', encoding='utf-8') as readme: - long_description = readme.read() - name = 'path.py' description = 'A module wrapper for os.path' nspkg_technique = 'native' @@ -25,16 +20,17 @@ maintainer="Jason R. Coombs", maintainer_email="jaraco@jaraco.com", description=description or name, - long_description=long_description, url="https://github.com/jaraco/" + name, py_modules=['path', 'test_path'], python_requires='>=2.7,!=3.1,!=3.2,!=3.3', install_requires=[ + 'importlib_metadata>=0.5', + 'backports.os; python_version=="2.7" and sys_platform=="linux2"', ], extras_require={ 'testing': [ # upstream - 'pytest>=2.8', + 'pytest>=3.5,!=3.7.3', 'pytest-sugar>=0.9.1', 'collective.checkdocs', 'pytest-flake8', diff -Nru path.py-11.0.1/test_path.py path.py-11.5.0/test_path.py --- path.py-11.0.1/test_path.py 2018-03-26 18:33:23.000000000 +0000 +++ path.py-11.5.0/test_path.py 2018-10-02 19:48:33.000000000 +0000 @@ -22,19 +22,23 @@ import sys import shutil import time +import types import ntpath import posixpath import textwrap import platform import importlib import operator +import datetime +import subprocess +import re import pytest import packaging.version import path -from path import tempdir -from path import CaseInsensitivePattern as ci +from path import TempDir +from path import matchers from path import SpecialResolver from path import Multi @@ -46,7 +50,7 @@ return choices[os.name] -@pytest.fixture(autouse=True, params=[path.Path, path.FastPath]) +@pytest.fixture(autouse=True, params=[path.Path]) def path_class(request, monkeypatch): """ Invoke tests on any number of Path classes. @@ -232,6 +236,30 @@ assert res2 == 'foo/bar' +class TestPerformance: + @pytest.mark.skipif( + path.PY2, + reason="Tests fail frequently on Python 2; see #153") + def test_import_time(self, monkeypatch): + """ + Import of path.py should take less than 100ms. + + Run tests in a subprocess to isolate from test suite overhead. + """ + cmd = [ + sys.executable, + '-m', 'timeit', + '-n', '1', + '-r', '1', + 'import path', + ] + res = subprocess.check_output(cmd, universal_newlines=True) + dur = re.search(r'(\d+) msec per loop', res).group(1) + limit = datetime.timedelta(milliseconds=100) + duration = datetime.timedelta(milliseconds=int(dur)) + assert duration < limit + + class TestSelfReturn: """ Some methods don't necessarily return any value (e.g. makedirs, @@ -271,7 +299,7 @@ class TestScratchDir: """ - Tests that run in a temporary directory (does not test tempdir class) + Tests that run in a temporary directory (does not test TempDir class) """ def test_context_manager(self, tmpdir): """Can be used as context manager for chdir.""" @@ -358,6 +386,11 @@ assert d.glob('*') == [af] assert d.glob('*.html') == [] assert d.glob('testfile') == [] + + # .iglob matches .glob but as an iterator. + assert list(d.iglob('*')) == d.glob('*') + assert isinstance(d.iglob('*'), types.GeneratorType) + finally: af.remove() @@ -380,10 +413,6 @@ pass @pytest.mark.xfail( - platform.system() == 'Linux' and path.PY2, - reason="Can't decode bytes in FS. See #121", - ) - @pytest.mark.xfail( mac_version('10.13'), reason="macOS disallows invalid encodings", ) @@ -686,7 +715,7 @@ test('UTF-16') def test_chunks(self, tmpdir): - p = (tempdir() / 'test.txt').touch() + p = (TempDir() / 'test.txt').touch() txt = "0123456789" size = 5 p.write_text(txt) @@ -700,13 +729,13 @@ reason="samefile not present", ) def test_samefile(self, tmpdir): - f1 = (tempdir() / '1.txt').touch() + f1 = (TempDir() / '1.txt').touch() f1.write_text('foo') - f2 = (tempdir() / '2.txt').touch() + f2 = (TempDir() / '2.txt').touch() f1.write_text('foo') - f3 = (tempdir() / '3.txt').touch() + f3 = (TempDir() / '3.txt').touch() f1.write_text('bar') - f4 = (tempdir() / '4.txt') + f4 = (TempDir() / '4.txt') f1.copyfile(f4) assert os.path.samefile(f1, f2) == f1.samefile(f2) @@ -825,6 +854,20 @@ assert self.subdir_b.isdir() assert self.subdir_b.listdir() == [self.subdir_b / self.test_file.name] + def test_only_newer(self): + """ + merge_tree should accept a copy_function in which only + newer files are copied and older files do not overwrite + newer copies in the dest. + """ + target = self.subdir_b / 'testfile.txt' + target.write_text('this is newer') + self.subdir_a.merge_tree( + self.subdir_b, + copy_function=path.only_newer(shutil.copy2), + ) + assert target.text() == 'this is newer' + class TestChdir: def test_chdir_or_cd(self, tmpdir): @@ -872,7 +915,7 @@ """ One should be able to readily construct a temporary directory """ - d = tempdir() + d = TempDir() assert isinstance(d, path.Path) assert d.exists() assert d.isdir() @@ -881,24 +924,24 @@ def test_next_class(self): """ - It should be possible to invoke operations on a tempdir and get + It should be possible to invoke operations on a TempDir and get Path classes. """ - d = tempdir() + d = TempDir() sub = d / 'subdir' assert isinstance(sub, path.Path) d.rmdir() def test_context_manager(self): """ - One should be able to use a tempdir object as a context, which will + One should be able to use a TempDir object as a context, which will clean up the contents after. """ - d = tempdir() + d = TempDir() res = d.__enter__() - assert res is d + assert res == path.Path(d) (d / 'somefile.txt').touch() - assert not isinstance(d / 'somefile.txt', tempdir) + assert not isinstance(d / 'somefile.txt', TempDir) d.__exit__(None, None, None) assert not d.exists() @@ -906,10 +949,10 @@ """ The context manager will not clean up if an exception occurs. """ - d = tempdir() + d = TempDir() d.__enter__() (d / 'somefile.txt').touch() - assert not isinstance(d / 'somefile.txt', tempdir) + assert not isinstance(d / 'somefile.txt', TempDir) d.__exit__(TypeError, TypeError('foo'), None) assert d.exists() @@ -919,7 +962,7 @@ provide a temporry directory that will be deleted after that. """ - with tempdir() as d: + with TempDir() as d: assert d.isdir() assert not d.isdir() @@ -993,10 +1036,10 @@ p = Path(tmpdir) (p / 'sub').mkdir() (p / 'File').touch() - assert p.listdir(ci('S*')) == [p / 'sub'] - assert p.listdir(ci('f*')) == [p / 'File'] - assert p.files(ci('S*')) == [] - assert p.dirs(ci('f*')) == [] + assert p.listdir(matchers.CaseInsensitive('S*')) == [p / 'sub'] + assert p.listdir(matchers.CaseInsensitive('f*')) == [p / 'File'] + assert p.files(matchers.CaseInsensitive('S*')) == [] + assert p.dirs(matchers.CaseInsensitive('f*')) == [] def test_walk_case_insensitive(self, tmpdir): p = Path(tmpdir) @@ -1005,7 +1048,7 @@ (p / 'sub1' / 'foo' / 'bar.Txt').touch() (p / 'sub2' / 'foo' / 'bar.TXT').touch() (p / 'sub2' / 'foo' / 'bar.txt.bz2').touch() - files = list(p.walkfiles(ci('*.txt'))) + files = list(p.walkfiles(matchers.CaseInsensitive('*.txt'))) assert len(files) == 2 assert p / 'sub2' / 'foo' / 'bar.TXT' in files assert p / 'sub1' / 'foo' / 'bar.Txt' in files @@ -1193,5 +1236,23 @@ assert path == input -if __name__ == '__main__': - pytest.main() +@pytest.mark.xfail('path.PY2', reason="Python 2 has no __future__") +def test_no_dependencies(): + """ + Path.py guarantees that the path module can be + transplanted into an environment without any dependencies. + """ + cmd = [ + sys.executable, + '-S', + '-c', 'import path', + ] + subprocess.check_call(cmd) + + +def test_version(): + """ + Under normal circumstances, path should present a + __version__. + """ + assert re.match(r'\d+\.\d+.*', path.__version__) diff -Nru path.py-11.0.1/tox.ini path.py-11.5.0/tox.ini --- path.py-11.0.1/tox.ini 2018-03-26 18:33:23.000000000 +0000 +++ path.py-11.5.0/tox.ini 2018-10-02 19:48:33.000000000 +0000 @@ -5,10 +5,8 @@ [testenv] deps = setuptools>=31.0.1 - # workaround for yaml/pyyaml#126 - # git+https://github.com/yaml/pyyaml@master#egg=pyyaml;python_version=="3.7" commands = - py.test {posargs} + pytest {posargs} python setup.py checkdocs usedevelop = True extras = testing diff -Nru path.py-11.0.1/.travis.yml path.py-11.5.0/.travis.yml --- path.py-11.0.1/.travis.yml 2018-03-26 18:33:23.000000000 +0000 +++ path.py-11.5.0/.travis.yml 2018-10-02 19:48:33.000000000 +0000 @@ -1,11 +1,11 @@ -dist: trusty +dist: xenial sudo: false language: python python: - 2.7 -- 3.4 -- &latest_py3 3.6 +- 3.6 +- &latest_py3 3.7 jobs: fast_finish: true @@ -25,11 +25,15 @@ secure: fggUs33qP6DB+j/q7KGScfohgGq7OwsW5BMW6ZZvSlq+9pnNDZxSVrfCw0wb9vdq/Hb9nH4Of+wDoyh+Ul6GN28GRX7qj1HTjbc65nhRp9aA1Ib9Y3KJwGR8k5gPJZmx/zKP0r7COSXsOdXDkVSJ/UjCfuKhcsSHpi0lAYG6BSA= distributions: dists skip_cleanup: true - skip_upload_docs: true cache: pip install: - pip install tox tox-venv +before_script: + # Disable IPv6. Ref travis-ci/travis-ci#8361 + - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then + sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; + fi script: tox