Fix on-demand wildcard path resolution and metadata scanning
- Resolve normalized wildcard keys through indexed original paths - Prevent recursive self-matches for missing indexed files - Exclude Git metadata consistently from wildcard scans - Cover path lookup, loading modes and precedence regressions
This commit is contained in:
@@ -111,6 +111,7 @@ def calculate_directory_size(directory_path, limit=None):
|
||||
total_size = 0
|
||||
try:
|
||||
for root, directories, files in os.walk(directory_path, followlinks=True):
|
||||
directories[:] = [d for d in directories if d not in ('.git', '.github')]
|
||||
for file in files:
|
||||
if file.endswith(('.txt', '.yaml', '.yml')):
|
||||
file_path = os.path.join(root, file)
|
||||
@@ -145,6 +146,7 @@ def scan_wildcard_metadata(wildcard_path):
|
||||
discovered = 0
|
||||
try:
|
||||
for root, directories, files in os.walk(wildcard_path, followlinks=True):
|
||||
directories[:] = [d for d in directories if d not in ('.git', '.github')]
|
||||
for file in files:
|
||||
if file.endswith('.txt'):
|
||||
file_path = os.path.join(root, file)
|
||||
@@ -243,6 +245,12 @@ def find_wildcard_file(key):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Normalization changes spaces and case; keep the original indexed path.
|
||||
# Preserve direct-file precedence before falling back to the metadata.
|
||||
file_path = available_wildcards.get(key)
|
||||
if file_path and os.path.isfile(file_path):
|
||||
return (file_path, file_path.endswith(('.yaml', '.yml')))
|
||||
|
||||
# Case 2: YAML nested key (e.g., "colors/warm" → "colors.yaml")
|
||||
if '/' in key:
|
||||
parent_key = key.split('/')[0]
|
||||
@@ -297,8 +305,7 @@ def get_wildcard_value(key):
|
||||
# Example: "dragon" matches "dragon.txt", "fantasy/dragon.txt", "dragon/fire.txt", etc.
|
||||
matched_keys = []
|
||||
for k in available_wildcards.keys():
|
||||
if (k == key or
|
||||
k.endswith('/' + key) or
|
||||
if k != key and (k.endswith('/' + key) or
|
||||
k.startswith(key + '/') or
|
||||
('/' + key + '/') in k):
|
||||
matched_keys.append(k)
|
||||
@@ -453,6 +460,7 @@ def read_wildcard_dict(wildcard_path, on_demand=False):
|
||||
"""
|
||||
global wildcard_dict
|
||||
for root, directories, files in os.walk(wildcard_path, followlinks=True):
|
||||
directories[:] = [d for d in directories if d not in ('.git', '.github')]
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
@@ -1120,6 +1128,7 @@ def load_yaml_files_only(wildcard_path):
|
||||
yaml_count = 0
|
||||
try:
|
||||
for root, directories, files in os.walk(wildcard_path, followlinks=True):
|
||||
directories[:] = [d for d in directories if d not in ('.git', '.github')]
|
||||
for file in files:
|
||||
if file.endswith('.yaml') or file.endswith('.yml'):
|
||||
file_path = os.path.join(root, file)
|
||||
|
||||
@@ -24,6 +24,88 @@ from populate_helpers import ( # noqa: E402
|
||||
SEEDS = [0, 1, 42, 100, 999, 12345, 2_147_483_647]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def wildcard_directory(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(wc, "wildcards_path", str(tmp_path))
|
||||
monkeypatch.setattr(wc, "wildcard_dict", {})
|
||||
monkeypatch.setattr(wc, "available_wildcards", {})
|
||||
monkeypatch.setattr(wc, "loaded_wildcards", {})
|
||||
monkeypatch.setattr(wc, "_on_demand_mode", True)
|
||||
monkeypatch.setattr(wc.config, "get_config", dict)
|
||||
return tmp_path
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["my colors.txt", "colors.TXT.txt"])
|
||||
@pytest.mark.parametrize("prefix", ["", "collection/"])
|
||||
@pytest.mark.parametrize("short_name", [False, True])
|
||||
def test_on_demand_expands_indexed_normalized_paths(wildcard_directory, name, prefix, short_name):
|
||||
path = wildcard_directory / prefix / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("red\n", encoding="utf-8")
|
||||
wc.scan_wildcard_metadata(str(wildcard_directory))
|
||||
key = wc.wildcard_normalize(os.path.splitext(name if short_name else prefix + name)[0])
|
||||
|
||||
assert wc.process(f"__{key}__", 42) == "red"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ["missing", "collection/missing"])
|
||||
def test_on_demand_missing_indexed_file_does_not_recurse(wildcard_directory, key):
|
||||
wc.available_wildcards[key] = str(wildcard_directory / "removed.txt")
|
||||
|
||||
assert wc.get_wildcard_value(key) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("on_demand", [False, True])
|
||||
def test_loading_preserves_direct_file_precedence(wildcard_directory, monkeypatch, on_demand):
|
||||
custom = wildcard_directory / "custom"
|
||||
custom.mkdir()
|
||||
(wildcard_directory / "shared.txt").write_text("builtin\n", encoding="utf-8")
|
||||
(custom / "shared.txt").write_text("custom\n", encoding="utf-8")
|
||||
monkeypatch.setattr(wc.config, "get_config", lambda: {"custom_wildcards": str(custom)})
|
||||
monkeypatch.setattr(wc, "get_cache_limit", lambda: 1 if on_demand else 1024 * 1024)
|
||||
|
||||
wc.wildcard_load()
|
||||
|
||||
# Preserve the existing precedence in each mode, rather than changing it here.
|
||||
assert wc.process("__shared__", 42) == ("builtin" if on_demand else "custom")
|
||||
|
||||
|
||||
def test_on_demand_depth_match_keeps_all_available_options(wildcard_directory):
|
||||
for directory, option in (("first", "red"), ("second/nested", "blue")):
|
||||
path = wildcard_directory / directory / "my colors.txt"
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text(option + "\n", encoding="utf-8")
|
||||
wc.scan_wildcard_metadata(str(wildcard_directory))
|
||||
wc.available_wildcards["removed/my-colors"] = str(wildcard_directory / "removed.txt")
|
||||
|
||||
assert sorted(wc.get_wildcard_value("my-colors")) == ["blue", "red"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("on_demand", [False, True])
|
||||
def test_wildcard_scans_ignore_git_metadata(wildcard_directory, monkeypatch, caplog, on_demand):
|
||||
for name in (".git", ".github"):
|
||||
metadata = wildcard_directory / "collection" / name / "workflows"
|
||||
metadata.mkdir(parents=True)
|
||||
(metadata / "publish.yml").write_text("on: [push]\n", encoding="utf-8")
|
||||
(metadata / "internal.txt").write_text("not a wildcard\n", encoding="utf-8")
|
||||
# Other dot-directories remain valid wildcard locations.
|
||||
visible = wildcard_directory / ".styles"
|
||||
visible.mkdir()
|
||||
(visible / "colors.txt").write_text("red\n", encoding="utf-8")
|
||||
(wildcard_directory / "colors.yaml").write_text("palette: [blue]\n", encoding="utf-8")
|
||||
expected_size = sum(p.stat().st_size for p in
|
||||
(visible / "colors.txt", wildcard_directory / "colors.yaml"))
|
||||
monkeypatch.setattr(wc, "get_cache_limit", lambda: 1 if on_demand else 1024 * 1024)
|
||||
|
||||
wc.wildcard_load()
|
||||
|
||||
assert wc.calculate_directory_size(str(wildcard_directory)) == expected_size
|
||||
assert wc.process("__.styles/colors__ __palette__", 42) == "red blue"
|
||||
keys = set(wc.available_wildcards) | set(wc.wildcard_dict) | set(wc.loaded_wildcards)
|
||||
assert not any("/.git/" in k or "/.github/" in k for k in keys)
|
||||
assert not [r for r in caplog.records if r.levelno >= logging.WARNING]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Category 1: Basic wildcard __x__
|
||||
# Tutorial L28 / PRD §2.1, §2.5
|
||||
|
||||
Reference in New Issue
Block a user