forked from tox-dev/python-discovery
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path_cache.py
More file actions
153 lines (109 loc) · 4.06 KB
/
Copy path_cache.py
File metadata and controls
153 lines (109 loc) · 4.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"""Cache Protocol and built-in implementations for Python interpreter discovery."""
from __future__ import annotations
import json
import logging
from contextlib import contextmanager, suppress
from hashlib import sha256
from typing import TYPE_CHECKING, Final, Protocol, runtime_checkable
if TYPE_CHECKING:
from collections.abc import Generator
from pathlib import Path
_LOGGER: Final[logging.Logger] = logging.getLogger(__name__)
@runtime_checkable
class ContentStore(Protocol):
"""A store for reading and writing cached content."""
def exists(self) -> bool: ...
def read(self) -> dict | None: ...
def write(self, content: dict) -> None: ...
def remove(self) -> None: ...
@contextmanager
def locked(self) -> Generator[None]: ...
@runtime_checkable
class PyInfoCache(Protocol):
"""Cache interface for Python interpreter information."""
def py_info(self, path: Path) -> ContentStore: ...
def py_info_clear(self) -> None: ...
class DiskContentStore:
"""JSON file-based content store with file locking."""
def __init__(self, folder: Path, key: str) -> None:
self._folder = folder
self._key = key
@property
def _file(self) -> Path:
return self._folder / f"{self._key}.json"
def exists(self) -> bool:
return self._file.exists()
def read(self) -> dict | None:
data, bad_format = None, False
try:
data = json.loads(self._file.read_text(encoding="utf-8"))
except ValueError:
bad_format = True
except OSError:
_LOGGER.debug("failed to read %s", self._file, exc_info=True)
else:
_LOGGER.debug("got python info from %s", self._file)
return data
if bad_format:
with suppress(OSError):
self.remove()
return None
def write(self, content: dict) -> None:
self._folder.mkdir(parents=True, exist_ok=True)
self._file.write_text(json.dumps(content, sort_keys=True, indent=2), encoding="utf-8")
_LOGGER.debug("wrote python info at %s", self._file)
def remove(self) -> None:
with suppress(OSError):
self._file.unlink()
_LOGGER.debug("removed python info at %s", self._file)
@contextmanager
def locked(self) -> Generator[None]:
from filelock import FileLock # noqa: PLC0415
lock_path = self._folder / f"{self._key}.lock"
lock_path.parent.mkdir(parents=True, exist_ok=True)
with FileLock(str(lock_path)):
yield
class DiskCache:
"""File-system based Python interpreter info cache (``<root>/py_info/4/<sha256>.json``)."""
def __init__(self, root: Path) -> None:
self._root = root
@property
def _py_info_dir(self) -> Path:
return self._root / "py_info" / "4"
def py_info(self, path: Path) -> DiskContentStore:
key = sha256(str(path).encode("utf-8")).hexdigest()
return DiskContentStore(self._py_info_dir, key)
def py_info_clear(self) -> None:
folder = self._py_info_dir
if folder.exists():
for entry in folder.iterdir():
if entry.suffix == ".json":
with suppress(OSError):
entry.unlink()
class NoOpContentStore(ContentStore):
"""Content store that does nothing -- implements ContentStore protocol."""
def exists(self) -> bool: # noqa: PLR6301
return False
def read(self) -> dict | None: # noqa: PLR6301
return None
def write(self, content: dict) -> None:
pass
def remove(self) -> None:
pass
@contextmanager
def locked(self) -> Generator[None]: # noqa: PLR6301
yield
class NoOpCache(PyInfoCache):
"""Cache that does nothing -- implements PyInfoCache protocol."""
def py_info(self, path: Path) -> NoOpContentStore: # noqa: ARG002, PLR6301
return NoOpContentStore()
def py_info_clear(self) -> None:
pass
__all__ = [
"ContentStore",
"DiskCache",
"DiskContentStore",
"NoOpCache",
"NoOpContentStore",
"PyInfoCache",
]