Skip to content

Commit 307ef20

Browse files
authored
Optimize glob_mac to use find instead of python version.
1 parent c94886a commit 307ef20

1 file changed

Lines changed: 62 additions & 44 deletions

File tree

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
"""macOS Glob tool: ``git ls-files`` inside git repos, ``pathlib`` outside.
1+
"""macOS Glob tool: ``git ls-files`` inside git repos, ``find`` outside.
22
33
Apple ships neither GNU ``tree`` nor a compatible equivalent, so the
44
non-git fallback in :class:`GlobTool` (which shells out to ``tree``)
55
fails on a stock macOS install. ``GlobMac`` replaces that fallback
6-
with :meth:`pathlib.Path.rglob`, which is pure-Python, requires no
7-
external binary, and produces identical results.
6+
with a hybrid approach: ``find`` (C binary, pre-installed on macOS)
7+
handles directory traversal and pattern matching, while Python handles
8+
mtime sorting — which ``find`` does not support.
9+
10+
This is significantly faster than the previous pure-Python
11+
:meth:`pathlib.Path.rglob` approach on large directory trees, while
12+
producing identical results.
813
914
Only the non-git fallback differs; the git path, the tool name, and
1015
the result format are inherited unchanged so callers, the tool
@@ -13,59 +18,72 @@
1318

1419
from __future__ import annotations
1520

16-
import fnmatch
1721
import os
18-
from pathlib import Path
22+
import subprocess
1923

2024
from .base import ToolContext
2125
from .filesystem import _natnump, _spool
2226
from .glob import GlobTool
2327

2428

2529
class GlobMac(GlobTool):
26-
"""Glob with a pure-Python non-git fallback for macOS."""
30+
"""Glob with a hybrid ``find`` + Python-sort non-git fallback for macOS."""
2731

2832
def _tree_fallback(self, pattern: str, base: str, depth: object) -> str:
29-
"""Walk *base* with :func:`pathlib.Path.rglob` instead of ``tree``.
33+
"""Use ``find`` for traversal + matching, Python for mtime sort.
3034
31-
Results are absolute paths sorted by modification time (newest
32-
first), matching the ``tree --sort=mtime`` order of the Linux
33-
fallback. Hidden directories (``.git``, etc.) are skipped.
35+
``find`` (C binary) walks the directory tree and applies pattern
36+
matching natively, which is significantly faster than
37+
:meth:`pathlib.Path.rglob` on large trees. Results are then
38+
sorted by modification time (newest first) in Python, matching
39+
the ``tree --sort=mtime`` order of the Linux fallback. Hidden
40+
directories (``.git``, etc.) are skipped via ``-path */.* -prune``.
41+
Symlinks are followed (``-L``), matching ``tree -l`` and the old
42+
:meth:`pathlib.Path.rglob` behavior.
3443
"""
35-
root = Path(base)
36-
matches: list[tuple[float, str]] = []
44+
cmd = [
45+
"find",
46+
"-L",
47+
base,
48+
"-path",
49+
"*/.*",
50+
"-prune",
51+
"-o",
52+
"-type",
53+
"f",
54+
"-iname",
55+
pattern,
56+
"-print",
57+
]
3758
if _natnump(depth):
38-
# Depth-limited: walk manually so we can count levels.
39-
for dirpath, dirnames, filenames in os.walk(base):
40-
# Skip hidden directories (mirrors tree's -I .git and
41-
# the general expectation that dotfiles are excluded).
42-
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
43-
rel = os.path.relpath(dirpath, base)
44-
level = 0 if rel == "." else rel.count(os.sep) + 1
45-
if level >= depth:
46-
dirnames.clear()
47-
continue
48-
for name in filenames:
49-
if fnmatch.fnmatch(name.lower(), pattern.lower()):
50-
full = os.path.join(dirpath, name)
51-
try:
52-
mtime = os.path.getmtime(full)
53-
except OSError:
54-
mtime = 0.0
55-
matches.append((mtime, full))
56-
else:
57-
# Unlimited depth: rglob is simpler.
58-
for p in root.rglob("*"):
59-
if any(part.startswith(".") for part in p.relative_to(root).parts):
60-
continue
61-
if p.is_file() and fnmatch.fnmatch(p.name.lower(), pattern.lower()):
62-
try:
63-
mtime = p.stat().st_mtime
64-
except OSError:
65-
mtime = 0.0
66-
matches.append((mtime, str(p)))
67-
68-
# Sort newest-first (like tree --sort=mtime).
59+
cmd[3:3] = ["-maxdepth", str(depth)]
60+
try:
61+
proc = subprocess.run(
62+
cmd,
63+
capture_output=True,
64+
text=True,
65+
encoding="utf-8",
66+
errors="replace",
67+
timeout=60,
68+
)
69+
except (OSError, subprocess.TimeoutExpired) as e:
70+
return f"Error: {e}"
71+
lines = [line for line in proc.stdout.splitlines() if line]
72+
if not lines:
73+
if proc.returncode != 0:
74+
out = f"Glob failed with exit code {proc.returncode}\n"
75+
out += proc.stderr or ""
76+
return _spool(out, "glob")
77+
return ""
78+
79+
matches: list[tuple[float, str]] = []
80+
for path in lines:
81+
try:
82+
mtime = os.path.getmtime(path)
83+
except OSError:
84+
mtime = 0.0
85+
matches.append((mtime, path))
86+
6987
matches.sort(key=lambda t: t[0], reverse=True)
7088
out = "\n".join(path for _, path in matches)
7189
if not out:
@@ -93,5 +111,5 @@ def run(self, args: dict, ctx: ToolContext) -> str:
93111
# The git path is identical to the parent — delegate.
94112
return super().run(args, ctx)
95113

96-
# Non-git: pure-Python fallback instead of `tree`.
114+
# Non-git: hybrid find + Python sort fallback instead of `tree`.
97115
return self._tree_fallback(pattern, base, depth)

0 commit comments

Comments
 (0)