Merge pull request #3594 from pytest-dev/interal-pathlib

[WIP] port cache plugin internals to pathlib
This commit is contained in:
Bruno Oliveira 2018-06-25 11:09:36 -03:00 committed by GitHub
commit 5b186cd609
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 98 additions and 79 deletions

View File

@ -14,7 +14,7 @@ environment:
- TOXENV: "py34" - TOXENV: "py34"
- TOXENV: "py35" - TOXENV: "py35"
- TOXENV: "py36" - TOXENV: "py36"
- TOXENV: "pypy" # - TOXENV: "pypy" reenable when we are able to provide a scandir wheel or build scandir
- TOXENV: "py27-pexpect" - TOXENV: "py27-pexpect"
- TOXENV: "py27-xdist" - TOXENV: "py27-xdist"
- TOXENV: "py27-trial" - TOXENV: "py27-trial"

View File

@ -73,15 +73,19 @@ def main():
environment_marker_support_level = get_environment_marker_support_level() environment_marker_support_level = get_environment_marker_support_level()
if environment_marker_support_level >= 2: if environment_marker_support_level >= 2:
install_requires.append('funcsigs;python_version<"3.0"') install_requires.append('funcsigs;python_version<"3.0"')
install_requires.append('pathlib2;python_version<"3.6"')
install_requires.append('colorama;sys_platform=="win32"') install_requires.append('colorama;sys_platform=="win32"')
elif environment_marker_support_level == 1: elif environment_marker_support_level == 1:
extras_require[':python_version<"3.0"'] = ["funcsigs"] extras_require[':python_version<"3.0"'] = ["funcsigs"]
extras_require[':python_version<"3.6"'] = ["pathlib2"]
extras_require[':sys_platform=="win32"'] = ["colorama"] extras_require[':sys_platform=="win32"'] = ["colorama"]
else: else:
if sys.platform == "win32": if sys.platform == "win32":
install_requires.append("colorama") install_requires.append("colorama")
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
install_requires.append("funcsigs") install_requires.append("funcsigs")
if sys.version_info < (3, 6):
install_requires.append("pathlib2")
setup( setup(
name="pytest", name="pytest",

View File

@ -5,40 +5,51 @@ the name cache was not chosen to ensure pluggy automatically
ignores the external pytest-cache ignores the external pytest-cache
""" """
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
from collections import OrderedDict from collections import OrderedDict
import py import py
import six import six
import attr
import pytest import pytest
import json import json
import os import shutil
from os.path import sep as _sep, altsep as _altsep
from textwrap import dedent from . import paths
from .compat import _PY2 as PY2, Path
README_CONTENT = u"""\
# pytest cache directory #
This directory contains data from the pytest's cache plugin,
which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
**Do not** commit this to version control.
See [the docs](https://docs.pytest.org/en/latest/cache.html) for more information.
"""
@attr.s
class Cache(object): class Cache(object):
def __init__(self, config): _cachedir = attr.ib(repr=False)
self.config = config _warn = attr.ib(repr=False)
self._cachedir = Cache.cache_dir_from_config(config)
self.trace = config.trace.root.get("cache") @classmethod
if config.getoption("cacheclear"): def for_config(cls, config):
self.trace("clearing cachedir") cachedir = cls.cache_dir_from_config(config)
if self._cachedir.check(): if config.getoption("cacheclear") and cachedir.exists():
self._cachedir.remove() shutil.rmtree(str(cachedir))
self._cachedir.mkdir() cachedir.mkdir()
return cls(cachedir, config.warn)
@staticmethod @staticmethod
def cache_dir_from_config(config): def cache_dir_from_config(config):
cache_dir = config.getini("cache_dir") return paths.resolve_from_str(config.getini("cache_dir"), config.rootdir)
cache_dir = os.path.expanduser(cache_dir)
cache_dir = os.path.expandvars(cache_dir) def warn(self, fmt, **args):
if os.path.isabs(cache_dir): self._warn(code="I9", message=fmt.format(**args) if args else fmt)
return py.path.local(cache_dir)
else:
return config.rootdir.join(cache_dir)
def makedir(self, name): def makedir(self, name):
""" return a directory path object with the given name. If the """ return a directory path object with the given name. If the
@ -50,12 +61,15 @@ class Cache(object):
Make sure the name contains your plugin or application Make sure the name contains your plugin or application
identifiers to prevent clashes with other cache users. identifiers to prevent clashes with other cache users.
""" """
if _sep in name or _altsep is not None and _altsep in name: name = Path(name)
if len(name.parts) > 1:
raise ValueError("name is not allowed to contain path separators") raise ValueError("name is not allowed to contain path separators")
return self._cachedir.ensure_dir("d", name) res = self._cachedir.joinpath("d", name)
res.mkdir(exist_ok=True, parents=True)
return py.path.local(res)
def _getvaluepath(self, key): def _getvaluepath(self, key):
return self._cachedir.join("v", *key.split("/")) return self._cachedir.joinpath("v", Path(key))
def get(self, key, default): def get(self, key, default):
""" return cached value for the given key. If no value """ return cached value for the given key. If no value
@ -69,12 +83,10 @@ class Cache(object):
""" """
path = self._getvaluepath(key) path = self._getvaluepath(key)
if path.check():
try: try:
with path.open("r") as f: with path.open("r") as f:
return json.load(f) return json.load(f)
except ValueError: except (ValueError, IOError, OSError):
self.trace("cache-invalid at %s" % (path,))
return default return default
def set(self, key, value): def set(self, key, value):
@ -88,42 +100,25 @@ class Cache(object):
""" """
path = self._getvaluepath(key) path = self._getvaluepath(key)
try: try:
path.dirpath().ensure_dir() path.parent.mkdir(exist_ok=True, parents=True)
except (py.error.EEXIST, py.error.EACCES): except (IOError, OSError):
self.config.warn( self.warn("could not create cache path {path}", path=path)
code="I9", message="could not create cache path %s" % (path,)
)
return return
try: try:
f = path.open("w") f = path.open("wb" if PY2 else "w")
except py.error.ENOTDIR: except (IOError, OSError):
self.config.warn( self.warn("cache could not write path {path}", path=path)
code="I9", message="cache could not write path %s" % (path,)
)
else: else:
with f: with f:
self.trace("cache-write %s: %r" % (key, value))
json.dump(value, f, indent=2, sort_keys=True) json.dump(value, f, indent=2, sort_keys=True)
self._ensure_readme() self._ensure_readme()
def _ensure_readme(self): def _ensure_readme(self):
content_readme = dedent( if self._cachedir.is_dir():
"""\ readme_path = self._cachedir / "README.md"
# pytest cache directory # if not readme_path.is_file():
readme_path.write_text(README_CONTENT)
This directory contains data from the pytest's cache plugin,
which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
**Do not** commit this to version control.
See [the docs](https://docs.pytest.org/en/latest/cache.html) for more information.
"""
)
if self._cachedir.check(dir=True):
readme_path = self._cachedir.join("README.md")
if not readme_path.check(file=True):
readme_path.write(content_readme)
class LFPlugin(object): class LFPlugin(object):
@ -297,7 +292,7 @@ def pytest_cmdline_main(config):
@pytest.hookimpl(tryfirst=True) @pytest.hookimpl(tryfirst=True)
def pytest_configure(config): def pytest_configure(config):
config.cache = Cache(config) config.cache = Cache.for_config(config)
config.pluginmanager.register(LFPlugin(config), "lfplugin") config.pluginmanager.register(LFPlugin(config), "lfplugin")
config.pluginmanager.register(NFPlugin(config), "nfplugin") config.pluginmanager.register(NFPlugin(config), "nfplugin")
@ -320,41 +315,40 @@ def cache(request):
def pytest_report_header(config): def pytest_report_header(config):
if config.option.verbose: if config.option.verbose:
relpath = py.path.local().bestrelpath(config.cache._cachedir) relpath = config.cache._cachedir.relative_to(config.rootdir)
return "cachedir: %s" % relpath return "cachedir: {}".format(relpath)
def cacheshow(config, session): def cacheshow(config, session):
from pprint import pprint from pprint import pformat
tw = py.io.TerminalWriter() tw = py.io.TerminalWriter()
tw.line("cachedir: " + str(config.cache._cachedir)) tw.line("cachedir: " + str(config.cache._cachedir))
if not config.cache._cachedir.check(): if not config.cache._cachedir.is_dir():
tw.line("cache is empty") tw.line("cache is empty")
return 0 return 0
dummy = object() dummy = object()
basedir = config.cache._cachedir basedir = config.cache._cachedir
vdir = basedir.join("v") vdir = basedir / "v"
tw.sep("-", "cache values") tw.sep("-", "cache values")
for valpath in sorted(vdir.visit(lambda x: x.isfile())): for valpath in sorted(x for x in vdir.rglob("*") if x.is_file()):
key = valpath.relto(vdir).replace(valpath.sep, "/") key = valpath.relative_to(vdir)
val = config.cache.get(key, dummy) val = config.cache.get(key, dummy)
if val is dummy: if val is dummy:
tw.line("%s contains unreadable content, " "will be ignored" % key) tw.line("%s contains unreadable content, " "will be ignored" % key)
else: else:
tw.line("%s contains:" % key) tw.line("%s contains:" % key)
stream = py.io.TextIO() for line in pformat(val).splitlines():
pprint(val, stream=stream)
for line in stream.getvalue().splitlines():
tw.line(" " + line) tw.line(" " + line)
ddir = basedir.join("d") ddir = basedir / "d"
if ddir.isdir() and ddir.listdir(): if ddir.is_dir():
contents = sorted(ddir.rglob("*"))
tw.sep("-", "cache directories") tw.sep("-", "cache directories")
for p in sorted(basedir.join("d").visit()): for p in contents:
# if p.check(dir=1): # if p.check(dir=1):
# print("%s/" % p.relto(basedir)) # print("%s/" % p.relto(basedir))
if p.isfile(): if p.is_file():
key = p.relto(basedir) key = p.relative_to(basedir)
tw.line("%s is a file of length %d" % (key, p.size())) tw.line("{} is a file of length {:d}".format(key, p.stat().st_size))
return 0 return 0

View File

@ -22,6 +22,7 @@ except ImportError: # pragma: no cover
# Only available in Python 3.4+ or as a backport # Only available in Python 3.4+ or as a backport
enum = None enum = None
__all__ = ["Path"]
_PY3 = sys.version_info > (3, 0) _PY3 = sys.version_info > (3, 0)
_PY2 = not _PY3 _PY2 = not _PY3
@ -32,7 +33,6 @@ if _PY3:
else: else:
from funcsigs import signature, Parameter as Parameter from funcsigs import signature, Parameter as Parameter
NoneType = type(None) NoneType = type(None)
NOTSET = object() NOTSET = object()
@ -40,6 +40,12 @@ PY35 = sys.version_info[:2] >= (3, 5)
PY36 = sys.version_info[:2] >= (3, 6) PY36 = sys.version_info[:2] >= (3, 6)
MODULE_NOT_FOUND_ERROR = "ModuleNotFoundError" if PY36 else "ImportError" MODULE_NOT_FOUND_ERROR = "ModuleNotFoundError" if PY36 else "ImportError"
if PY36:
from pathlib import Path
else:
from pathlib2 import Path
if _PY3: if _PY3:
from collections.abc import MutableMapping as MappingMixin # noqa from collections.abc import MutableMapping as MappingMixin # noqa
from collections.abc import Mapping, Sequence # noqa from collections.abc import Mapping, Sequence # noqa

13
src/_pytest/paths.py Normal file
View File

@ -0,0 +1,13 @@
from .compat import Path
from os.path import expanduser, expandvars, isabs
def resolve_from_str(input, root):
assert not isinstance(input, Path), "would break on py2"
root = Path(root)
input = expanduser(input)
input = expandvars(input)
if isabs(input):
return Path(input)
else:
return root.joinpath(input)

View File

@ -1,5 +1,7 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import sys import sys
import py import py
import _pytest import _pytest
import pytest import pytest
@ -26,7 +28,7 @@ class TestNewAPI(object):
cache = config.cache cache = config.cache
pytest.raises(TypeError, lambda: cache.set("key/name", cache)) pytest.raises(TypeError, lambda: cache.set("key/name", cache))
config.cache.set("key/name", 0) config.cache.set("key/name", 0)
config.cache._getvaluepath("key/name").write("123invalid") config.cache._getvaluepath("key/name").write_bytes(b"123invalid")
val = config.cache.get("key/name", -2) val = config.cache.get("key/name", -2)
assert val == -2 assert val == -2
@ -824,8 +826,8 @@ class TestReadme(object):
def check_readme(self, testdir): def check_readme(self, testdir):
config = testdir.parseconfigure() config = testdir.parseconfigure()
readme = config.cache._cachedir.join("README.md") readme = config.cache._cachedir.joinpath("README.md")
return readme.isfile() return readme.is_file()
def test_readme_passed(self, testdir): def test_readme_passed(self, testdir):
testdir.makepyfile( testdir.makepyfile(