fastglob 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastglob-0.1.0/.gitignore +54 -0
- fastglob-0.1.0/PKG-INFO +21 -0
- fastglob-0.1.0/fastglob/__init__.py +347 -0
- fastglob-0.1.0/fastglob/py.typed +0 -0
- fastglob-0.1.0/pyproject.toml +140 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Fast Glob — gitignore (minimal, covers agentic + build artifacts)
|
|
2
|
+
# Secrets: .mcp.json and code_aid_query.sh contain live credentials and MUST NOT be committed to public GitHub.
|
|
3
|
+
# For public release, scrub them from history before publishing (see AGENTS.md RELEASE HYGIENE).
|
|
4
|
+
.mcp.json
|
|
5
|
+
code_aid_query.sh
|
|
6
|
+
|
|
7
|
+
# Large generated index (87MB) — tracked previously, now ignored
|
|
8
|
+
.ix/
|
|
9
|
+
.ix/shard.ix
|
|
10
|
+
.ix/shard.ix.delta
|
|
11
|
+
.ix/beacon.json
|
|
12
|
+
|
|
13
|
+
# Python cache + packaging artifacts
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.pyc
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.ruff_cache/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.venv/
|
|
21
|
+
dist/
|
|
22
|
+
build/
|
|
23
|
+
.coverage
|
|
24
|
+
.coverage.*
|
|
25
|
+
htmlcov/
|
|
26
|
+
|
|
27
|
+
# Agentic artifacts (must not be committed to public GitHub)
|
|
28
|
+
.sniper/
|
|
29
|
+
out/
|
|
30
|
+
probes/
|
|
31
|
+
fastglob.jsonl
|
|
32
|
+
final-fblob.jsonl
|
|
33
|
+
pi-session-fastglob.html
|
|
34
|
+
|
|
35
|
+
# Compat run artifact (regenerated by every `make compat`; embeds
|
|
36
|
+
# machine-specific absolute paths and per-run timings — not a reference)
|
|
37
|
+
tests/compat/candidate.json
|
|
38
|
+
|
|
39
|
+
# Benchmark generated trees and results
|
|
40
|
+
bench/trees/
|
|
41
|
+
bench/results/*.json
|
|
42
|
+
bench/results/*.md
|
|
43
|
+
!bench/results/baseline.md
|
|
44
|
+
|
|
45
|
+
# Rust build artifacts
|
|
46
|
+
src/target/
|
|
47
|
+
|
|
48
|
+
# Misc
|
|
49
|
+
.DS_Store
|
|
50
|
+
*.swp
|
|
51
|
+
*~
|
|
52
|
+
|
|
53
|
+
# Maat audit reports (local-only)
|
|
54
|
+
.maat/
|
fastglob-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fastglob
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python compatibility wrapper around the fastglob engine
|
|
5
|
+
Project-URL: Repository, https://github.com/moeshawky/fastglob
|
|
6
|
+
Author-email: Moe Shawky <moe@libermoe.com>
|
|
7
|
+
Maintainer-email: Moe Shawky <moe@libermoe.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: filesystem,glob,linux,pathname,pattern-matching
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Utilities
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: mypy>=1.4; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""fastglob — Python-compatibility wrapper around the fastglob engine.
|
|
2
|
+
|
|
3
|
+
This is a SEPARATE package: it never shadows the stdlib ``glob`` module
|
|
4
|
+
(``import glob`` still gives you the standard library). Usage:
|
|
5
|
+
|
|
6
|
+
import fastglob
|
|
7
|
+
fastglob.glob("*.py", recursive=True, include_hidden=False)
|
|
8
|
+
fastglob.iglob("a/**/b.txt", recursive=True)
|
|
9
|
+
fastglob.escape("a*b") -> "a[*]b"
|
|
10
|
+
fastglob.escape(b"a*b") -> b"a[*]b" # bytes pattern -> bytes result
|
|
11
|
+
|
|
12
|
+
Documented mechanics (keep these in mind when using in hot loops):
|
|
13
|
+
|
|
14
|
+
* Every call SHELLS OUT to the ``fastglob`` binary (one process per call).
|
|
15
|
+
The engine does all traversal/matching natively; this package only
|
|
16
|
+
marshals arguments and decodes NUL-delimited, byte-exact output.
|
|
17
|
+
* The binary is located via ``$FASTGLOB_BIN`` if set, otherwise
|
|
18
|
+
``<repo>/src/target/release/fastglob`` (build it with ``make build``).
|
|
19
|
+
* ``dir_fd`` is passed to the child by dup()ing the fd with CLOEXEC
|
|
20
|
+
cleared and passing ``--dir-fd N`` plus ``pass_fds`` (PEP 446: fds from
|
|
21
|
+
``os.open`` are close-on-exec and ``subprocess`` closes fds >= 3 in the
|
|
22
|
+
child unless listed in ``pass_fds``).
|
|
23
|
+
* Result TYPE follows the PATTERN type (stdlib parity — VERIFIED against
|
|
24
|
+
CPython 3.12: ``glob.escape(b'a*b') == b'a[*]b'``, ``glob(b'*.py')``
|
|
25
|
+
yields bytes elements; the reference branches on
|
|
26
|
+
``isinstance(pathname, bytes)``): str/PathLike patterns give str results
|
|
27
|
+
decoded via ``os.fsdecode`` (surrogateescape), so arbitrary byte
|
|
28
|
+
filenames round-trip exactly; bytes patterns give raw bytes results —
|
|
29
|
+
the engine output is already byte-exact, so bytes mode simply skips
|
|
30
|
+
decoding (no lossy round-trip).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import fcntl
|
|
36
|
+
import os
|
|
37
|
+
import subprocess
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Iterator, cast, overload
|
|
40
|
+
|
|
41
|
+
__all__ = ["escape", "glob", "iglob"]
|
|
42
|
+
|
|
43
|
+
__version__ = "0.1.0"
|
|
44
|
+
|
|
45
|
+
# Filesystem path arguments: str, bytes, or a PathLike whose ``__fspath__``
|
|
46
|
+
# returns str or bytes (stdlib parity — CPython glob accepts all three).
|
|
47
|
+
# Bare ``os.PathLike`` would be ``os.PathLike[Any]`` (implicit Any), which
|
|
48
|
+
# mypy --strict (disallow_any_generics) rejects.
|
|
49
|
+
_PathArg = str | bytes | os.PathLike[str] | os.PathLike[bytes]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _bin() -> str:
|
|
53
|
+
b = os.environ.get("FASTGLOB_BIN")
|
|
54
|
+
if b:
|
|
55
|
+
return b
|
|
56
|
+
repo = Path(__file__).resolve().parent.parent.parent
|
|
57
|
+
return str(repo / "src" / "target" / "release" / "fastglob")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _fs(x: _PathArg) -> bytes:
|
|
61
|
+
return os.fsencode(x)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _wants_bytes(x: _PathArg) -> bool:
|
|
65
|
+
"""True when results for path argument ``x`` must be bytes-typed.
|
|
66
|
+
|
|
67
|
+
Mirrors the reference implementation's type preservation (CPython
|
|
68
|
+
Lib/glob.py branches on ``isinstance(pathname, bytes)``; VERIFIED live
|
|
69
|
+
on CPython 3.12: ``glob.escape(b'a*b') == b'a[*]b'``, ``glob(b'*.py')``
|
|
70
|
+
yields bytes elements): plain bytes/bytearray follow bytes; a PathLike
|
|
71
|
+
follows whatever its ``__fspath__`` returns, exactly like ``os.fspath``
|
|
72
|
+
typing. str (and str-fspath PathLike) -> False.
|
|
73
|
+
"""
|
|
74
|
+
if isinstance(x, (bytes, bytearray)):
|
|
75
|
+
return True
|
|
76
|
+
if hasattr(x, "__fspath__"):
|
|
77
|
+
return isinstance(os.fspath(x), bytes)
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _run(args: list[bytes], pass_fds: tuple[int, ...] = ()) -> bytes:
|
|
82
|
+
p = subprocess.run(
|
|
83
|
+
[_bin().encode(), *args],
|
|
84
|
+
capture_output=True,
|
|
85
|
+
pass_fds=pass_fds,
|
|
86
|
+
)
|
|
87
|
+
if p.returncode != 0:
|
|
88
|
+
msg = p.stderr.decode("utf-8", "replace").strip()
|
|
89
|
+
raise RuntimeError(f"fastglob exited {p.returncode}: {msg}")
|
|
90
|
+
return p.stdout
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _paths(out: bytes, *, as_bytes: bool = False) -> list[str] | list[bytes]:
|
|
94
|
+
"""NUL-split byte-exact engine output into a result list.
|
|
95
|
+
|
|
96
|
+
Inputs:
|
|
97
|
+
out: raw engine stdout (records separated by NUL)
|
|
98
|
+
as_bytes: False (default) -> decode each record via ``os.fsdecode``
|
|
99
|
+
(surrogateescape), returning List[str]; True -> return the raw
|
|
100
|
+
records as List[bytes]. Used when the caller's pattern was
|
|
101
|
+
bytes so results preserve the input type (Ct38): no decoding,
|
|
102
|
+
no lossy round-trip.
|
|
103
|
+
Output:
|
|
104
|
+
List[str] or List[bytes] — one element per engine record
|
|
105
|
+
Errors:
|
|
106
|
+
None (pure split/decode)
|
|
107
|
+
"""
|
|
108
|
+
parts = out.split(b"\0")
|
|
109
|
+
if parts and parts[-1] == b"":
|
|
110
|
+
parts = parts[:-1]
|
|
111
|
+
if as_bytes:
|
|
112
|
+
return list(parts)
|
|
113
|
+
return [os.fsdecode(x) for x in parts]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _call(
|
|
117
|
+
pathname: _PathArg,
|
|
118
|
+
root_dir: _PathArg | None,
|
|
119
|
+
dir_fd: int | None,
|
|
120
|
+
recursive: bool,
|
|
121
|
+
include_hidden: bool,
|
|
122
|
+
as_bytes: bool = False,
|
|
123
|
+
) -> list[str] | list[bytes]:
|
|
124
|
+
"""Execute the fastglob engine with the given pattern and options.
|
|
125
|
+
|
|
126
|
+
Inputs:
|
|
127
|
+
pathname: glob pattern as str/bytes/PathLike. The RESULT type follows
|
|
128
|
+
the PATTERN type (Ct38): bytes pattern -> List[bytes] (raw engine
|
|
129
|
+
records); str or PathLike -> List[str] via fsdecode.
|
|
130
|
+
root_dir: optional filesystem origin shift (None = cwd). Its type is
|
|
131
|
+
independent of the result type — e.g. root_dir=str with a bytes
|
|
132
|
+
pattern is supported and yields bytes results.
|
|
133
|
+
dir_fd: optional open directory fd to resolve relative patterns against
|
|
134
|
+
recursive: when True, ``**`` matches zero or more directories
|
|
135
|
+
include_hidden: when True, ``*``/``?``/``**`` match dot-prefixed names
|
|
136
|
+
Output:
|
|
137
|
+
Matching pathnames as List[str] or List[bytes], duplicates preserved,
|
|
138
|
+
order unspecified
|
|
139
|
+
Errors:
|
|
140
|
+
RuntimeError if the engine exits non-zero (misuse -> exit 2)
|
|
141
|
+
TypeError/ValueError if pathname/root_dir is not str/bytes/PathLike,
|
|
142
|
+
or dir_fd is not a valid open int fd
|
|
143
|
+
"""
|
|
144
|
+
want_bytes = _wants_bytes(pathname)
|
|
145
|
+
args: list[bytes] = []
|
|
146
|
+
if recursive:
|
|
147
|
+
args.append(b"--recursive")
|
|
148
|
+
if include_hidden:
|
|
149
|
+
args.append(b"--include-hidden")
|
|
150
|
+
if root_dir is not None:
|
|
151
|
+
args += [b"--root-dir", _fs(root_dir)]
|
|
152
|
+
args += [b"--null", b"--", _fs(pathname)]
|
|
153
|
+
|
|
154
|
+
if dir_fd is not None:
|
|
155
|
+
# Validate dir_fd type and value before use (Level A contract).
|
|
156
|
+
if not isinstance(dir_fd, int):
|
|
157
|
+
raise TypeError(f"dir_fd must be int, got {type(dir_fd).__name__}")
|
|
158
|
+
if dir_fd < 0:
|
|
159
|
+
raise ValueError(f"dir_fd must be non-negative, got {dir_fd}")
|
|
160
|
+
# Validate that dir_fd is an open directory fd (fstat).
|
|
161
|
+
try:
|
|
162
|
+
os.fstat(dir_fd)
|
|
163
|
+
except OSError as e:
|
|
164
|
+
raise ValueError(f"dir_fd {dir_fd} is not a valid open fd: {e}") from e
|
|
165
|
+
# Atomic inheritable dup: prefer F_DUPFD (CLOEXEC cleared) which is
|
|
166
|
+
# atomic; fallback to dup()+set_inheritable which is two syscalls but
|
|
167
|
+
# uses the Python-level atomic helper (PEP 446). The old dup()+fcntl
|
|
168
|
+
# F_SETFD clearing was non-atomic (race window between dup and fcntl).
|
|
169
|
+
try:
|
|
170
|
+
# Use F_DUPFD to atomically dup with CLOEXEC cleared (new fd >=3).
|
|
171
|
+
d = fcntl.fcntl(dir_fd, fcntl.F_DUPFD, 3)
|
|
172
|
+
# Ensure inheritable (CLOEXEC cleared) — F_DUPFD already clears,
|
|
173
|
+
# but explicitly set for portability.
|
|
174
|
+
os.set_inheritable(d, True)
|
|
175
|
+
except (OSError, AttributeError):
|
|
176
|
+
d = os.dup(dir_fd)
|
|
177
|
+
try:
|
|
178
|
+
os.set_inheritable(d, True)
|
|
179
|
+
except OSError:
|
|
180
|
+
os.close(d)
|
|
181
|
+
raise
|
|
182
|
+
try:
|
|
183
|
+
args += [b"--dir-fd", str(d).encode()]
|
|
184
|
+
out = _run(args, pass_fds=(d,))
|
|
185
|
+
finally:
|
|
186
|
+
os.close(d)
|
|
187
|
+
else:
|
|
188
|
+
out = _run(args)
|
|
189
|
+
return _paths(out, as_bytes=want_bytes)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@overload
|
|
193
|
+
def glob(
|
|
194
|
+
pathname: str | os.PathLike[str],
|
|
195
|
+
*,
|
|
196
|
+
root_dir: _PathArg | None = None,
|
|
197
|
+
dir_fd: int | None = None,
|
|
198
|
+
recursive: bool = False,
|
|
199
|
+
include_hidden: bool = False,
|
|
200
|
+
) -> list[str]: ...
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@overload
|
|
204
|
+
def glob(
|
|
205
|
+
pathname: bytes | os.PathLike[bytes],
|
|
206
|
+
*,
|
|
207
|
+
root_dir: _PathArg | None = None,
|
|
208
|
+
dir_fd: int | None = None,
|
|
209
|
+
recursive: bool = False,
|
|
210
|
+
include_hidden: bool = False,
|
|
211
|
+
) -> list[bytes]: ...
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def glob(
|
|
215
|
+
pathname: _PathArg,
|
|
216
|
+
*,
|
|
217
|
+
root_dir: _PathArg | None = None,
|
|
218
|
+
dir_fd: int | None = None,
|
|
219
|
+
recursive: bool = False,
|
|
220
|
+
include_hidden: bool = False,
|
|
221
|
+
) -> list[str] | list[bytes]:
|
|
222
|
+
"""Return a list of paths matching ``pathname`` (stdlib-glob kwargs).
|
|
223
|
+
|
|
224
|
+
Type contract (stdlib parity — VERIFIED against CPython 3.12:
|
|
225
|
+
``glob.glob(b'*.py')`` yields bytes elements):
|
|
226
|
+
str or PathLike pattern -> List[str] (fsdecode/surrogateescape)
|
|
227
|
+
bytes pattern -> List[bytes] (raw engine bytes)
|
|
228
|
+
|
|
229
|
+
Example (result type follows the PATTERN type; root_dir type is
|
|
230
|
+
independent — only the pattern decides str-vs-bytes)::
|
|
231
|
+
|
|
232
|
+
fastglob.glob("*.py", root_dir="src") # ['x.py', ...] (str)
|
|
233
|
+
fastglob.glob(b"*.py", root_dir=b"src") # [b'x.py', ...] (bytes)
|
|
234
|
+
fastglob.glob(b"*", root_dir="/tmp") # [b'file', ...] (bytes)
|
|
235
|
+
|
|
236
|
+
Not supported (raises TypeError, like the stdlib): patterns that are
|
|
237
|
+
neither str, bytes, nor PathLike.
|
|
238
|
+
|
|
239
|
+
Inputs:
|
|
240
|
+
pathname: pattern (str/bytes/PathLike)
|
|
241
|
+
root_dir: optional root directory shift (str/bytes/PathLike or None)
|
|
242
|
+
dir_fd: optional directory fd (int) for relative resolution
|
|
243
|
+
recursive: enable ``**`` zero-or-more-dirs matching
|
|
244
|
+
include_hidden: allow ``*``/``?``/``**`` to match dotfiles
|
|
245
|
+
Output:
|
|
246
|
+
List[str] or List[bytes] — matching pathnames (duplicates preserved,
|
|
247
|
+
order unspecified)
|
|
248
|
+
Errors:
|
|
249
|
+
Propagates RuntimeError/TypeError/ValueError from ``_call``
|
|
250
|
+
"""
|
|
251
|
+
return _call(
|
|
252
|
+
pathname,
|
|
253
|
+
root_dir,
|
|
254
|
+
dir_fd,
|
|
255
|
+
recursive,
|
|
256
|
+
include_hidden,
|
|
257
|
+
as_bytes=_wants_bytes(pathname),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@overload
|
|
262
|
+
def iglob(
|
|
263
|
+
pathname: str | os.PathLike[str],
|
|
264
|
+
*,
|
|
265
|
+
root_dir: _PathArg | None = None,
|
|
266
|
+
dir_fd: int | None = None,
|
|
267
|
+
recursive: bool = False,
|
|
268
|
+
include_hidden: bool = False,
|
|
269
|
+
) -> Iterator[str]: ...
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@overload
|
|
273
|
+
def iglob(
|
|
274
|
+
pathname: bytes | os.PathLike[bytes],
|
|
275
|
+
*,
|
|
276
|
+
root_dir: _PathArg | None = None,
|
|
277
|
+
dir_fd: int | None = None,
|
|
278
|
+
recursive: bool = False,
|
|
279
|
+
include_hidden: bool = False,
|
|
280
|
+
) -> Iterator[bytes]: ...
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def iglob(
|
|
284
|
+
pathname: _PathArg,
|
|
285
|
+
*,
|
|
286
|
+
root_dir: _PathArg | None = None,
|
|
287
|
+
dir_fd: int | None = None,
|
|
288
|
+
recursive: bool = False,
|
|
289
|
+
include_hidden: bool = False,
|
|
290
|
+
) -> Iterator[str] | Iterator[bytes]:
|
|
291
|
+
"""Yield paths matching ``pathname`` (one binary call per iglob).
|
|
292
|
+
|
|
293
|
+
Same type contract as ``glob``: str/PathLike pattern yields str items,
|
|
294
|
+
bytes pattern yields bytes items (stdlib parity, VERIFIED on CPython
|
|
295
|
+
3.12). Example::
|
|
296
|
+
|
|
297
|
+
next(fastglob.iglob(b"*")) # -> bytes
|
|
298
|
+
next(fastglob.iglob("*")) # -> str
|
|
299
|
+
|
|
300
|
+
Inputs: same as ``glob``.
|
|
301
|
+
Output: Iterator[str] or Iterator[bytes] — lazily yields each match
|
|
302
|
+
(materialized via one engine call)
|
|
303
|
+
Errors: same as ``glob``
|
|
304
|
+
"""
|
|
305
|
+
items = _call(
|
|
306
|
+
pathname,
|
|
307
|
+
root_dir,
|
|
308
|
+
dir_fd,
|
|
309
|
+
recursive,
|
|
310
|
+
include_hidden,
|
|
311
|
+
as_bytes=_wants_bytes(pathname),
|
|
312
|
+
)
|
|
313
|
+
# _call's union return erases the per-element type; the runtime contract
|
|
314
|
+
# (Ct38, test-verified) is that items is all-str or all-bytes.
|
|
315
|
+
return cast("Iterator[str] | Iterator[bytes]", (p for p in items))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@overload
|
|
319
|
+
def escape(pathname: str | os.PathLike[str]) -> str: ...
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@overload
|
|
323
|
+
def escape(pathname: bytes | os.PathLike[bytes]) -> bytes: ...
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def escape(pathname: _PathArg) -> str | bytes:
|
|
327
|
+
"""Escape all glob special characters (port of ``glob.escape``).
|
|
328
|
+
|
|
329
|
+
Type contract (stdlib parity — VERIFIED against CPython 3.12:
|
|
330
|
+
``glob.escape(b'a*b') == b'a[*]b'``):
|
|
331
|
+
|
|
332
|
+
>>> escape('a*b')
|
|
333
|
+
'a[*]b'
|
|
334
|
+
>>> escape(b'a*b')
|
|
335
|
+
b'a[*]b'
|
|
336
|
+
|
|
337
|
+
PathLike input follows its ``__fspath__`` typing (str-fspath -> str
|
|
338
|
+
result, bytes-fspath -> bytes result); anything else raises TypeError.
|
|
339
|
+
Escaping always operates on the engine's byte-exact output: bytes
|
|
340
|
+
results are returned raw (no lossy round-trip); str results are decoded
|
|
341
|
+
via fsdecode (surrogateescape).
|
|
342
|
+
"""
|
|
343
|
+
out = _run([b"escape", b"--null", b"--", _fs(pathname)])
|
|
344
|
+
first = out.split(b"\0")[0]
|
|
345
|
+
if _wants_bytes(pathname):
|
|
346
|
+
return first
|
|
347
|
+
return os.fsdecode(first)
|
|
File without changes
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Packaging metadata for the `fastglob` Python package (repo layout: python/fastglob/).
|
|
2
|
+
# Exists so that the documented command `pip install -e python` (README.md, docs/api.md)
|
|
3
|
+
# actually succeeds — PEP 517/660 editable installs require build metadata.
|
|
4
|
+
#
|
|
5
|
+
# MAINTENANCE CONTRACT:
|
|
6
|
+
# - `version` MUST stay exactly equal to __version__ in python/fastglob/__init__.py
|
|
7
|
+
# (single source of truth is __init__.py; update both together or wire dynamic
|
|
8
|
+
# metadata later — do NOT let them drift).
|
|
9
|
+
# - Build backend: hatchling (2026 stack, measured 2026-08-23 via `uv build`
|
|
10
|
+
# probe: wheel contains exactly fastglob/__init__.py + fastglob/py.typed +
|
|
11
|
+
# dist-info; PEP 660 editable verified). Do NOT add a second top-level
|
|
12
|
+
# directory under python/ without re-verifying the wheel file list.
|
|
13
|
+
# - Deliberately minimal per assignment W4: no entry-points. Identity fields
|
|
14
|
+
# the operator has not specified (authors/maintainers, copyright holder)
|
|
15
|
+
# mirror the Rust pass placeholders (src/Cargo.toml, LICENSE TODO) —
|
|
16
|
+
# replace together before publishing.
|
|
17
|
+
# - python/ IS the src root of this subproject: the package dir python/fastglob/
|
|
18
|
+
# is imported via `pip install -e python` or the documented PYTHONPATH=python
|
|
19
|
+
# fallback. Do not add a repo-root pyproject.toml claiming the `fastglob`
|
|
20
|
+
# package — it would shadow this file.
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["hatchling>=1.26"]
|
|
24
|
+
build-backend = "hatchling.build"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build]
|
|
27
|
+
# Dev tooling artifacts — never ship in the distribution (measured 2026-08-23:
|
|
28
|
+
# uv.lock/.python-version and the tools' own cache dirs leaked into the first
|
|
29
|
+
# sdist; excluded here). Note: hatchling's VCS-aware sdist also carries the
|
|
30
|
+
# repo-root .gitignore into the sdist root — harmless at install time and
|
|
31
|
+
# not removable via `exclude` (measured), so it is accepted, not fought.
|
|
32
|
+
exclude = [
|
|
33
|
+
".python-version",
|
|
34
|
+
"uv.lock",
|
|
35
|
+
".coverage",
|
|
36
|
+
".ruff_cache",
|
|
37
|
+
".mypy_cache",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project]
|
|
41
|
+
name = "fastglob"
|
|
42
|
+
version = "0.1.0"
|
|
43
|
+
description = "Python compatibility wrapper around the fastglob engine"
|
|
44
|
+
# TODO (operator): add a per-package README (hatchling requires the readme
|
|
45
|
+
# inside python/, so the repo-root README.md cannot be referenced directly).
|
|
46
|
+
# Until then the wheel carries the one-line description; the repo README
|
|
47
|
+
# documents this package fully.
|
|
48
|
+
license = "MIT"
|
|
49
|
+
requires-python = ">=3.8"
|
|
50
|
+
authors = [{ name = "Moe Shawky", email = "moe@libermoe.com" }]
|
|
51
|
+
maintainers = [{ name = "Moe Shawky", email = "moe@libermoe.com" }]
|
|
52
|
+
keywords = ["glob", "pathname", "pattern-matching", "filesystem", "linux"]
|
|
53
|
+
classifiers = [
|
|
54
|
+
# Tested legs only: 3.8 = requires-python floor (enforced via ruff
|
|
55
|
+
# target-version py38, measured), 3.12 = the oracle interpreter.
|
|
56
|
+
# Extend when CI gains matrix legs — do not claim untested versions.
|
|
57
|
+
"Programming Language :: Python :: 3",
|
|
58
|
+
"Programming Language :: Python :: 3.8",
|
|
59
|
+
"Programming Language :: Python :: 3.12",
|
|
60
|
+
"Operating System :: POSIX :: Linux",
|
|
61
|
+
"Intended Audience :: Developers",
|
|
62
|
+
"Topic :: Utilities",
|
|
63
|
+
]
|
|
64
|
+
dependencies = [] # stdlib only — the engine is a subprocess, not a dependency
|
|
65
|
+
|
|
66
|
+
[project.optional-dependencies]
|
|
67
|
+
# `pip install -e "python[dev]"` — the quality-gate stack (G1-G6).
|
|
68
|
+
# No types-* stubs: zero third-party runtime dependencies (stdlib stubs
|
|
69
|
+
# ship with mypy). Dev-tool floors are measured, not assumed: mypy 2.x
|
|
70
|
+
# requires interpreter >=3.10 (mypy 1.x covers 3.8-3.9), so the floors are
|
|
71
|
+
# written as ranges that resolve across the full requires-python span.
|
|
72
|
+
dev = [
|
|
73
|
+
"ruff>=0.15",
|
|
74
|
+
"mypy>=1.4",
|
|
75
|
+
"pytest>=8",
|
|
76
|
+
"pytest-cov>=5",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
[project.urls]
|
|
80
|
+
Repository = "https://github.com/moeshawky/fastglob"
|
|
81
|
+
|
|
82
|
+
# ── Ruff (linter + formatter) ─────────────────────────────────────────────
|
|
83
|
+
[tool.ruff]
|
|
84
|
+
target-version = "py38" # must track requires-python
|
|
85
|
+
line-length = 88
|
|
86
|
+
|
|
87
|
+
[tool.ruff.lint]
|
|
88
|
+
select = [
|
|
89
|
+
"E",
|
|
90
|
+
"W", # pycodestyle
|
|
91
|
+
"F", # pyflakes
|
|
92
|
+
"I", # isort
|
|
93
|
+
"UP", # pyupgrade
|
|
94
|
+
"B", # flake8-bugbear
|
|
95
|
+
"SIM", # flake8-simplify
|
|
96
|
+
"RUF", # ruff-specific
|
|
97
|
+
"S", # flake8-bandit
|
|
98
|
+
"C4", # flake8-comprehensions
|
|
99
|
+
"RET", # flake8-return
|
|
100
|
+
"PTH", # flake8-use-pathlib
|
|
101
|
+
"TC", # flake8-type-checking
|
|
102
|
+
"FA", # flake8-future-annotations
|
|
103
|
+
]
|
|
104
|
+
ignore = ["E501"] # line length is the formatter's job
|
|
105
|
+
|
|
106
|
+
[tool.ruff.lint.per-file-ignores]
|
|
107
|
+
# The package's documented mechanic is to shell out to the fastglob binary
|
|
108
|
+
# (no shell involved; executable = $FASTGLOB_BIN or the repo-relative release
|
|
109
|
+
# path; arguments are fsencoded bytes). S603 is a manual-review nudge, not a
|
|
110
|
+
# defect, for this call.
|
|
111
|
+
"fastglob/__init__.py" = ["S603"]
|
|
112
|
+
|
|
113
|
+
[tool.ruff.format]
|
|
114
|
+
quote-style = "double"
|
|
115
|
+
|
|
116
|
+
# ── mypy (type checker) ───────────────────────────────────────────────────
|
|
117
|
+
[tool.mypy]
|
|
118
|
+
strict = true
|
|
119
|
+
# mypy 2.x minimum target is 3.10 (measured 2026-08-23: a 3.8 target emits
|
|
120
|
+
# "not supported (must be 3.10 or higher)" and clamps). The 3.8-3.9 syntax
|
|
121
|
+
# floor is enforced by ruff's FA/UP rules at target-version py38 (measured:
|
|
122
|
+
# FA102 flags PEP 585/604 annotations missing the future import).
|
|
123
|
+
python_version = "3.10"
|
|
124
|
+
|
|
125
|
+
# ── pytest ────────────────────────────────────────────────────────────────
|
|
126
|
+
[tool.pytest.ini_options]
|
|
127
|
+
# Canonical runner: `python tests/test_package.py` (Makefile `make test`).
|
|
128
|
+
# Note (measured 2026-08-23): pytest's configfile discovery walks from the
|
|
129
|
+
# TEST ARGS (repo-root tests/), so this section governs pytest runs whose
|
|
130
|
+
# args live under python/; the CI quality job passes --cov explicitly.
|
|
131
|
+
testpaths = ["../tests"]
|
|
132
|
+
addopts = "-ra -q --strict-markers --tb=short"
|
|
133
|
+
|
|
134
|
+
# ── coverage ──────────────────────────────────────────────────────────────
|
|
135
|
+
[tool.coverage.run]
|
|
136
|
+
source = ["fastglob"] # resolved relative to python/ (the package root)
|
|
137
|
+
|
|
138
|
+
[tool.coverage.report]
|
|
139
|
+
fail_under = 80
|
|
140
|
+
show_missing = true
|