Skip to content

Commit d420e34

Browse files
authored
More cleanup for update_quick_index.py + helpers (pythonarcade#2113)
* Deduplicate logic in API module processing * Move shared member iteration into a generator function * Rename member special rules declarations dict to be general instead of class-specific * Fix indentation for class rendering * Add comments and top-level docstring to explain file structure * Clean up unused items removed in earlier commits * Explain the decision to keep NotExcludedBy * Move Vfs and SharedPaths into util/doc_helpers * Move reusable items into doc_helpers * Move NotExcludedBy, get_module_path, and EMPTY_TUPLE into doc_helpers * Update __all__ * Clean up imports * Rename rules for tab-completion friendliness * Fix docstring fro VirtualFile * Type-annotate VirtualFile.write * Remove extra whitespace from Vfs.py * Annotate Vfs.write * Use pathlib.Path instead of strings inside vfs objects * Use EMPTY_TUPLE instead of recreating new list * Line ending consistency
1 parent e9bfacf commit d420e34

File tree

4 files changed

+177
-119
lines changed

4 files changed

+177
-119
lines changed

util/create_resources_listing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
sys.path.insert(0, str(Path(__file__).parent.parent.resolve()))
1313

1414
import arcade
15-
from vfs import Vfs
15+
from doc_helpers.vfs import Vfs
1616

1717
MODULE_DIR = Path(__file__).parent.resolve()
1818
ARCADE_ROOT = MODULE_DIR.parent

util/doc_helpers/__init__.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from pathlib import Path
5+
from typing import Iterable
6+
7+
from .vfs import VirtualFile, Vfs, F
8+
9+
10+
__all__ = (
11+
'get_module_path',
12+
'EMPTY_TUPLE',
13+
'F',
14+
'SharedPaths',
15+
'NotExcludedBy',
16+
'VirtualFile',
17+
'Vfs'
18+
)
19+
20+
21+
EMPTY_TUPLE = tuple()
22+
23+
24+
class SharedPaths:
25+
"""These are often used to set up a Vfs and open files."""
26+
REPO_UTILS_DIR = Path(__file__).parent.parent.resolve()
27+
REPO_ROOT = REPO_UTILS_DIR.parent
28+
ARCADE_ROOT = REPO_ROOT / "arcade"
29+
DOC_ROOT = REPO_ROOT / "doc"
30+
API_DOC_ROOT = DOC_ROOT / "api_docs"
31+
32+
33+
class NotExcludedBy:
34+
"""Helper predicate for exclusion.
35+
36+
This is here because we may eventually define excludes at per-module
37+
level in our config below instead of a single list.
38+
"""
39+
def __init__(self, collection: Iterable):
40+
self.items = set(collection)
41+
42+
def __call__(self, item) -> bool:
43+
return item not in self.items
44+
45+
46+
_VALID_MODULE_SEGMENT = re.compile(r"[_a-zA-Z][_a-z0-9]*")
47+
48+
49+
def get_module_path(module: str, root = SharedPaths.REPO_ROOT) -> Path:
50+
"""Quick-n-dirty module path estimation relative to the repo root.
51+
52+
:param module: A module path in the project.
53+
:raises ValueError: When a can't be computed.
54+
:return: A
55+
"""
56+
# Convert module.name.here to module/name/here
57+
current = root
58+
for index, part in enumerate(module.split('.')):
59+
if not _VALID_MODULE_SEGMENT.fullmatch(part):
60+
raise ValueError(
61+
f'Invalid module segment at index {index}: {part!r}')
62+
# else:
63+
# print(current, part)
64+
current /= part
65+
66+
# Account for the two kinds of modules:
67+
# 1. arcade/module.py
68+
# 2. arcade/module/__init__.py
69+
as_package = current / "__init__.py"
70+
have_package = as_package.is_file()
71+
as_file = current.with_suffix('.py')
72+
have_file = as_file.is_file()
73+
74+
# TODO: When 3.10 becomes our min Python, make this a match-case?
75+
if have_package and have_file:
76+
raise ValueError(
77+
f"Module conflict between {as_package} and {as_file}")
78+
elif have_package:
79+
current = as_package
80+
elif have_file:
81+
current = as_file
82+
else:
83+
raise ValueError(
84+
f"No folder package or file module detected for "
85+
f"{module}")
86+
87+
return current

util/vfs.py renamed to util/doc_helpers/vfs.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,30 +35,28 @@
3535
from typing import Generator, Type, TypeVar, Generic
3636

3737

38-
class SharedPaths:
39-
"""These are often used to set up a Vfs and open files."""
40-
REPO_UTILS_DIR = Path(__file__).parent.resolve()
41-
REPO_ROOT = REPO_UTILS_DIR.parent
42-
ARCADE_ROOT = REPO_ROOT / "arcade"
43-
DOC_ROOT = REPO_ROOT / "doc"
44-
API_DOC_ROOT = DOC_ROOT / "api_docs"
45-
46-
4738
class VirtualFile:
48-
"""Subclass these to add some magic.
39+
"""An abstraction over an in-memory stream.
4940
41+
This might later change to be an abstraction to over a StringIO
42+
inside the Vfs class to better reflect file system behavior. What
43+
don't change is the write method, which means you can safely do the
44+
following:
45+
46+
1. Subclass this to add helper functions
47+
2. Pass it to a Vfs at creation
5048
"""
5149

52-
def __init__(self, path: str):
53-
self.path = path
50+
def __init__(self, path: Path | str):
51+
self.path = Path(path)
5452
self._content = StringIO()
5553

5654
def include_file(self, path: Path | str) -> int:
5755
"""Copy the path's contents into this file."""
5856
contents = Path(path).read_text()
5957
return self.write(contents)
6058

61-
def write(self, str: str):
59+
def write(self, str: str) -> int:
6260
return self._content.write(str)
6361

6462
def close(self):
@@ -96,7 +94,7 @@ class Vfs(Generic[F]):
9694

9795
def __init__(self, file_type: Type[F] = VirtualFile):
9896
self.file_type: Type[F] = file_type
99-
self.files: dict[str, F] = dict()
97+
self.files: dict[Path, F] = dict()
10098
self.files_to_delete: set[Path] = set()
10199

102100
def request_culling_unwritten(self, directory: str | Path, glob: str):
@@ -109,11 +107,11 @@ def request_culling_unwritten(self, directory: str | Path, glob: str):
109107
Doing it this way allows us to leave the files untouched on disk if
110108
this build would emit an identical file.
111109
"""
112-
path = Path(str(directory))
110+
path = Path(directory)
113111
for p in path.glob(glob):
114112
self.files_to_delete.add(p)
115113

116-
def write(self):
114+
def write(self) -> None:
117115
"""Sync all files of this Vfs to the real filesystem.
118116
119117
This performs the following actions:
@@ -129,16 +127,16 @@ def write(self):
129127
for file in self.files.values():
130128
file._write_to_disk()
131129
for path in self.files_to_delete:
132-
if not str(path) in file_paths:
130+
if not path in file_paths:
133131
print(f"Deleting {path}")
134-
os.remove(path)
132+
path.unlink()
135133

136134
def exists(self, path: str | Path) -> bool:
137135
"""Return True if the file has been opened in this Vfs."""
138136
return str(path) in self.files
139137

140138
def open(self, path: str | Path, mode: str) -> F:
141-
path = str(path)
139+
path = Path(path).resolve()
142140
modes = set(mode)
143141

144142
# Modes which are blatantly unsupported
@@ -158,7 +156,6 @@ def open(self, path: str | Path, mode: str) -> F:
158156

159157
raise ValueError(f"Unsupported mode {mode!r}")
160158

161-
162159
# This is less nasty than dynamically generating a subclass
163160
# which then attaches instances to a specific Vfs on creation
164161
# and assigns itself as the .open value for that Vfs

0 commit comments

Comments
 (0)