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.
@@ -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/
@@ -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