fastglob 0.1.0__py3-none-any.whl
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/__init__.py +347 -0
- fastglob/py.typed +0 -0
- fastglob-0.1.0.dist-info/METADATA +21 -0
- fastglob-0.1.0.dist-info/RECORD +5 -0
- fastglob-0.1.0.dist-info/WHEEL +4 -0
fastglob/__init__.py
ADDED
|
@@ -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)
|
fastglob/py.typed
ADDED
|
File without changes
|
|
@@ -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,5 @@
|
|
|
1
|
+
fastglob/__init__.py,sha256=IwycW5oZ2PrZHt_2XBQA8dC9JXhuDtFOKwOl8VPAR7Q,12256
|
|
2
|
+
fastglob/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
fastglob-0.1.0.dist-info/METADATA,sha256=kguIvnBGFBDPFfcfjqufoUIAzz7ZKyTGB--_gQZOgdE,837
|
|
4
|
+
fastglob-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
5
|
+
fastglob-0.1.0.dist-info/RECORD,,
|