Skip to content

Commit 5b186cd

Browse files
authored
Merge pull request #3594 from pytest-dev/interal-pathlib
[WIP] port cache plugin internals to pathlib
2 parents de98939 + 5a156b3 commit 5b186cd

File tree

6 files changed

+98
-79
lines changed

6 files changed

+98
-79
lines changed

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ environment:
1414
- TOXENV: "py34"
1515
- TOXENV: "py35"
1616
- TOXENV: "py36"
17-
- TOXENV: "pypy"
17+
# - TOXENV: "pypy" reenable when we are able to provide a scandir wheel or build scandir
1818
- TOXENV: "py27-pexpect"
1919
- TOXENV: "py27-xdist"
2020
- TOXENV: "py27-trial"

setup.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,19 @@ def main():
7373
environment_marker_support_level = get_environment_marker_support_level()
7474
if environment_marker_support_level >= 2:
7575
install_requires.append('funcsigs;python_version<"3.0"')
76+
install_requires.append('pathlib2;python_version<"3.6"')
7677
install_requires.append('colorama;sys_platform=="win32"')
7778
elif environment_marker_support_level == 1:
7879
extras_require[':python_version<"3.0"'] = ["funcsigs"]
80+
extras_require[':python_version<"3.6"'] = ["pathlib2"]
7981
extras_require[':sys_platform=="win32"'] = ["colorama"]
8082
else:
8183
if sys.platform == "win32":
8284
install_requires.append("colorama")
8385
if sys.version_info < (3, 0):
8486
install_requires.append("funcsigs")
87+
if sys.version_info < (3, 6):
88+
install_requires.append("pathlib2")
8589

8690
setup(
8791
name="pytest",

src/_pytest/cacheprovider.py

Lines changed: 68 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,51 @@
55
ignores the external pytest-cache
66
"""
77
from __future__ import absolute_import, division, print_function
8-
98
from collections import OrderedDict
109

1110
import py
1211
import six
12+
import attr
1313

1414
import pytest
1515
import json
16-
import os
17-
from os.path import sep as _sep, altsep as _altsep
18-
from textwrap import dedent
16+
import shutil
17+
18+
from . import paths
19+
from .compat import _PY2 as PY2, Path
20+
21+
README_CONTENT = u"""\
22+
# pytest cache directory #
23+
24+
This directory contains data from the pytest's cache plugin,
25+
which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
26+
27+
**Do not** commit this to version control.
28+
29+
See [the docs](https://docs.pytest.org/en/latest/cache.html) for more information.
30+
"""
1931

2032

33+
@attr.s
2134
class Cache(object):
2235

23-
def __init__(self, config):
24-
self.config = config
25-
self._cachedir = Cache.cache_dir_from_config(config)
26-
self.trace = config.trace.root.get("cache")
27-
if config.getoption("cacheclear"):
28-
self.trace("clearing cachedir")
29-
if self._cachedir.check():
30-
self._cachedir.remove()
31-
self._cachedir.mkdir()
36+
_cachedir = attr.ib(repr=False)
37+
_warn = attr.ib(repr=False)
38+
39+
@classmethod
40+
def for_config(cls, config):
41+
cachedir = cls.cache_dir_from_config(config)
42+
if config.getoption("cacheclear") and cachedir.exists():
43+
shutil.rmtree(str(cachedir))
44+
cachedir.mkdir()
45+
return cls(cachedir, config.warn)
3246

3347
@staticmethod
3448
def cache_dir_from_config(config):
35-
cache_dir = config.getini("cache_dir")
36-
cache_dir = os.path.expanduser(cache_dir)
37-
cache_dir = os.path.expandvars(cache_dir)
38-
if os.path.isabs(cache_dir):
39-
return py.path.local(cache_dir)
40-
else:
41-
return config.rootdir.join(cache_dir)
49+
return paths.resolve_from_str(config.getini("cache_dir"), config.rootdir)
50+
51+
def warn(self, fmt, **args):
52+
self._warn(code="I9", message=fmt.format(**args) if args else fmt)
4253

4354
def makedir(self, name):
4455
""" return a directory path object with the given name. If the
@@ -50,12 +61,15 @@ def makedir(self, name):
5061
Make sure the name contains your plugin or application
5162
identifiers to prevent clashes with other cache users.
5263
"""
53-
if _sep in name or _altsep is not None and _altsep in name:
64+
name = Path(name)
65+
if len(name.parts) > 1:
5466
raise ValueError("name is not allowed to contain path separators")
55-
return self._cachedir.ensure_dir("d", name)
67+
res = self._cachedir.joinpath("d", name)
68+
res.mkdir(exist_ok=True, parents=True)
69+
return py.path.local(res)
5670

5771
def _getvaluepath(self, key):
58-
return self._cachedir.join("v", *key.split("/"))
72+
return self._cachedir.joinpath("v", Path(key))
5973

6074
def get(self, key, default):
6175
""" return cached value for the given key. If no value
@@ -69,13 +83,11 @@ def get(self, key, default):
6983
7084
"""
7185
path = self._getvaluepath(key)
72-
if path.check():
73-
try:
74-
with path.open("r") as f:
75-
return json.load(f)
76-
except ValueError:
77-
self.trace("cache-invalid at %s" % (path,))
78-
return default
86+
try:
87+
with path.open("r") as f:
88+
return json.load(f)
89+
except (ValueError, IOError, OSError):
90+
return default
7991

8092
def set(self, key, value):
8193
""" save value for the given key.
@@ -88,42 +100,25 @@ def set(self, key, value):
88100
"""
89101
path = self._getvaluepath(key)
90102
try:
91-
path.dirpath().ensure_dir()
92-
except (py.error.EEXIST, py.error.EACCES):
93-
self.config.warn(
94-
code="I9", message="could not create cache path %s" % (path,)
95-
)
103+
path.parent.mkdir(exist_ok=True, parents=True)
104+
except (IOError, OSError):
105+
self.warn("could not create cache path {path}", path=path)
96106
return
97107
try:
98-
f = path.open("w")
99-
except py.error.ENOTDIR:
100-
self.config.warn(
101-
code="I9", message="cache could not write path %s" % (path,)
102-
)
108+
f = path.open("wb" if PY2 else "w")
109+
except (IOError, OSError):
110+
self.warn("cache could not write path {path}", path=path)
103111
else:
104112
with f:
105-
self.trace("cache-write %s: %r" % (key, value))
106113
json.dump(value, f, indent=2, sort_keys=True)
107114
self._ensure_readme()
108115

109116
def _ensure_readme(self):
110117

111-
content_readme = dedent(
112-
"""\
113-
# pytest cache directory #
114-
115-
This directory contains data from the pytest's cache plugin,
116-
which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
117-
118-
**Do not** commit this to version control.
119-
120-
See [the docs](https://docs.pytest.org/en/latest/cache.html) for more information.
121-
"""
122-
)
123-
if self._cachedir.check(dir=True):
124-
readme_path = self._cachedir.join("README.md")
125-
if not readme_path.check(file=True):
126-
readme_path.write(content_readme)
118+
if self._cachedir.is_dir():
119+
readme_path = self._cachedir / "README.md"
120+
if not readme_path.is_file():
121+
readme_path.write_text(README_CONTENT)
127122

128123

129124
class LFPlugin(object):
@@ -297,7 +292,7 @@ def pytest_cmdline_main(config):
297292

298293
@pytest.hookimpl(tryfirst=True)
299294
def pytest_configure(config):
300-
config.cache = Cache(config)
295+
config.cache = Cache.for_config(config)
301296
config.pluginmanager.register(LFPlugin(config), "lfplugin")
302297
config.pluginmanager.register(NFPlugin(config), "nfplugin")
303298

@@ -320,41 +315,40 @@ def cache(request):
320315

321316
def pytest_report_header(config):
322317
if config.option.verbose:
323-
relpath = py.path.local().bestrelpath(config.cache._cachedir)
324-
return "cachedir: %s" % relpath
318+
relpath = config.cache._cachedir.relative_to(config.rootdir)
319+
return "cachedir: {}".format(relpath)
325320

326321

327322
def cacheshow(config, session):
328-
from pprint import pprint
323+
from pprint import pformat
329324

330325
tw = py.io.TerminalWriter()
331326
tw.line("cachedir: " + str(config.cache._cachedir))
332-
if not config.cache._cachedir.check():
327+
if not config.cache._cachedir.is_dir():
333328
tw.line("cache is empty")
334329
return 0
335330
dummy = object()
336331
basedir = config.cache._cachedir
337-
vdir = basedir.join("v")
332+
vdir = basedir / "v"
338333
tw.sep("-", "cache values")
339-
for valpath in sorted(vdir.visit(lambda x: x.isfile())):
340-
key = valpath.relto(vdir).replace(valpath.sep, "/")
334+
for valpath in sorted(x for x in vdir.rglob("*") if x.is_file()):
335+
key = valpath.relative_to(vdir)
341336
val = config.cache.get(key, dummy)
342337
if val is dummy:
343338
tw.line("%s contains unreadable content, " "will be ignored" % key)
344339
else:
345340
tw.line("%s contains:" % key)
346-
stream = py.io.TextIO()
347-
pprint(val, stream=stream)
348-
for line in stream.getvalue().splitlines():
341+
for line in pformat(val).splitlines():
349342
tw.line(" " + line)
350343

351-
ddir = basedir.join("d")
352-
if ddir.isdir() and ddir.listdir():
344+
ddir = basedir / "d"
345+
if ddir.is_dir():
346+
contents = sorted(ddir.rglob("*"))
353347
tw.sep("-", "cache directories")
354-
for p in sorted(basedir.join("d").visit()):
348+
for p in contents:
355349
# if p.check(dir=1):
356350
# print("%s/" % p.relto(basedir))
357-
if p.isfile():
358-
key = p.relto(basedir)
359-
tw.line("%s is a file of length %d" % (key, p.size()))
351+
if p.is_file():
352+
key = p.relative_to(basedir)
353+
tw.line("{} is a file of length {:d}".format(key, p.stat().st_size))
360354
return 0

src/_pytest/compat.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
# Only available in Python 3.4+ or as a backport
2323
enum = None
2424

25+
__all__ = ["Path"]
2526

2627
_PY3 = sys.version_info > (3, 0)
2728
_PY2 = not _PY3
@@ -32,14 +33,19 @@
3233
else:
3334
from funcsigs import signature, Parameter as Parameter
3435

35-
3636
NoneType = type(None)
3737
NOTSET = object()
3838

3939
PY35 = sys.version_info[:2] >= (3, 5)
4040
PY36 = sys.version_info[:2] >= (3, 6)
4141
MODULE_NOT_FOUND_ERROR = "ModuleNotFoundError" if PY36 else "ImportError"
4242

43+
if PY36:
44+
from pathlib import Path
45+
else:
46+
from pathlib2 import Path
47+
48+
4349
if _PY3:
4450
from collections.abc import MutableMapping as MappingMixin # noqa
4551
from collections.abc import Mapping, Sequence # noqa

src/_pytest/paths.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from .compat import Path
2+
from os.path import expanduser, expandvars, isabs
3+
4+
5+
def resolve_from_str(input, root):
6+
assert not isinstance(input, Path), "would break on py2"
7+
root = Path(root)
8+
input = expanduser(input)
9+
input = expandvars(input)
10+
if isabs(input):
11+
return Path(input)
12+
else:
13+
return root.joinpath(input)

testing/test_cacheprovider.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import absolute_import, division, print_function
2+
23
import sys
4+
35
import py
46
import _pytest
57
import pytest
@@ -26,7 +28,7 @@ def test_config_cache_dataerror(self, testdir):
2628
cache = config.cache
2729
pytest.raises(TypeError, lambda: cache.set("key/name", cache))
2830
config.cache.set("key/name", 0)
29-
config.cache._getvaluepath("key/name").write("123invalid")
31+
config.cache._getvaluepath("key/name").write_bytes(b"123invalid")
3032
val = config.cache.get("key/name", -2)
3133
assert val == -2
3234

@@ -824,8 +826,8 @@ class TestReadme(object):
824826

825827
def check_readme(self, testdir):
826828
config = testdir.parseconfigure()
827-
readme = config.cache._cachedir.join("README.md")
828-
return readme.isfile()
829+
readme = config.cache._cachedir.joinpath("README.md")
830+
return readme.is_file()
829831

830832
def test_readme_passed(self, testdir):
831833
testdir.makepyfile(

0 commit comments

Comments
 (0)