Merge pull request #3594 from pytest-dev/interal-pathlib
[WIP] port cache plugin internals to pathlib
This commit is contained in:
commit
5b186cd609
|
@ -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"
|
||||||
|
|
4
setup.py
4
setup.py
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue