Skip to content

Commit 9f28d8f

Browse files
claudespoorcc
authored andcommitted
Return FilterMatch (path + value_location) from find_by_filter
Introduce FilterMatch dataclass combining the item path with the exact source location of the filter field's value scalar, captured in the same yaml.compose() pass. Callers no longer need a second get_node_location call: - find_name_in_manifest uses matches[0].value_location directly - update_filtered / delete_filtered use match.path - _find_project_field_path uses matches[0].path.parts https://claude.ai/code/session_01Snw6Dutwsu9Etmf1oG6HC1
1 parent dcd6c9e commit 9f28d8f

2 files changed

Lines changed: 41 additions & 19 deletions

File tree

dfetch/manifest/manifest.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -372,14 +372,7 @@ def find_name_in_manifest(self, name: str) -> ManifestEntryLocation:
372372
if not matches:
373373
raise RuntimeError(f"{name} was not found in the manifest!")
374374

375-
# Look up the name VALUE node (the scalar "foo", not the mapping wrapper).
376-
name_value_path = FieldPath(matches[0].parts + ["name"])
377-
loc = self._doc.get_node_location(name_value_path)
378-
if loc is None:
379-
raise RuntimeError(
380-
f"{name} was found in YAML AST, but location could not be determined!"
381-
)
382-
375+
loc = matches[0].value_location
383376
return ManifestEntryLocation(
384377
line_number=loc.start_line + 1, # 0-based → 1-based line number
385378
start=loc.start_col + 1, # 0-based → 1-based column number
@@ -582,4 +575,4 @@ def _find_project_field_path(doc: YamlDocument, project_name: str) -> list[str]:
582575
)
583576
if not matches:
584577
raise RuntimeError(f"Project '{project_name}' not found in manifest text")
585-
return matches[0].parts
578+
return matches[0].path.parts

dfetch/util/yaml.py

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,21 @@ class FieldFilter:
4747
value: str
4848

4949

50+
@dataclass
51+
class FilterMatch:
52+
"""A sequence item that matched a :class:`FieldFilter`.
53+
54+
``path`` points to the matched sequence item (e.g.
55+
``["manifest", "projects", "0"]``). ``value_location`` is the exact
56+
source position of the filter field's value scalar inside that item —
57+
the same location that a follow-up ``get_node_location`` call would
58+
return for ``FieldPath([*path.parts, filter.key])``.
59+
"""
60+
61+
path: FieldPath
62+
value_location: FieldLocation
63+
64+
5065
class YamlDocument:
5166
"""A YAML document supporting smart field manipulation with comments, line endings, and filtering."""
5267

@@ -108,17 +123,21 @@ def dump(self) -> str:
108123

109124
def find_by_filter(
110125
self, sequence_path: str | FieldPath, field_filter: FieldFilter
111-
) -> list[FieldPath]:
126+
) -> list[FilterMatch]:
112127
"""Find all entries in a YAML sequence where a field has a given value.
113128
114129
Args:
115130
sequence_path: Path to the sequence node.
116131
field_filter: Filter key and expected value.
117132
118133
Returns:
119-
List of FieldPath pointing to each matching sequence item.
120-
Example: for "manifest.projects" with filter "name=foo", returns
121-
[FieldPath(['manifest', 'projects', '0'])] for the first match.
134+
List of :class:`FilterMatch` objects, each containing:
135+
136+
* ``path`` — path to the matched sequence item (e.g.
137+
``FieldPath(['manifest', 'projects', '0'])``).
138+
* ``value_location`` — exact source position of the filter
139+
field's value scalar within that item, so callers never need
140+
a separate ``get_node_location`` call.
122141
"""
123142
sequence_path = (
124143
FieldPath.from_str(sequence_path)
@@ -131,7 +150,7 @@ def find_by_filter(
131150
if not isinstance(sequence_node, SequenceNode):
132151
return []
133152

134-
matches: list[FieldPath] = []
153+
matches: list[FilterMatch] = []
135154
for idx, item in enumerate(sequence_node.value):
136155
if not isinstance(item, MappingNode):
137156
continue
@@ -141,7 +160,17 @@ def find_by_filter(
141160
isinstance(value_node, ScalarNode)
142161
and str(value_node.value) == field_filter.value
143162
):
144-
matches.append(FieldPath(sequence_path.parts + [str(idx)]))
163+
matches.append(
164+
FilterMatch(
165+
path=FieldPath(sequence_path.parts + [str(idx)]),
166+
value_location=FieldLocation(
167+
start_line=value_node.start_mark.line,
168+
start_col=value_node.start_mark.column,
169+
end_line=value_node.end_mark.line,
170+
end_col=value_node.end_mark.column,
171+
),
172+
)
173+
)
145174

146175
return matches
147176

@@ -162,9 +191,9 @@ def update_filtered(
162191
target_field: Field to update; if None, updates the filtered field itself.
163192
"""
164193
matches = self.find_by_filter(sequence_path, field_filter)
165-
for path in matches:
194+
for match in matches:
166195
field_to_update = target_field if target_field else field_filter.key
167-
field_path = FieldPath(path.parts + [field_to_update])
196+
field_path = FieldPath(match.path.parts + [field_to_update])
168197
current_value: str = self.get(field_path) or ""
169198
new_value = updater(current_value) if callable(updater) else updater
170199
self.set(field_path, new_value)
@@ -182,8 +211,8 @@ def delete_filtered(
182211
"""
183212
matches = self.find_by_filter(sequence_path, field_filter)
184213
# Delete in reverse to avoid shifting line indices
185-
for path in reversed(matches):
186-
self.delete(path)
214+
for match in reversed(matches):
215+
self.delete(match.path)
187216

188217
# ---------------- Internal helpers ----------------
189218
def _update_line(self, idx: int, field_name: str, value: str) -> None:

0 commit comments

Comments
 (0)