@@ -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+
5065class 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