Skip to content

Commit b963892

Browse files
committed
src/config(fix[loader]): retain ordered duplicate workspace items
why: Provide structured metadata so writers can replay duplicate workspace roots without loss. what: - extend DuplicateAwareConfigReader to capture ordered top-level items - propagate new load_with_duplicates return signature through CLI/config callers
1 parent fec187d commit b963892

File tree

5 files changed

+33
-12
lines changed

5 files changed

+33
-12
lines changed

src/vcspull/_internal/config_reader.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ def __init__(self, stream: str) -> None:
228228
super().__init__(stream)
229229
self.top_level_key_values: dict[t.Any, list[t.Any]] = {}
230230
self._mapping_depth = 0
231+
self.top_level_items: list[tuple[t.Any, t.Any]] = []
231232

232233

233234
def _duplicate_tracking_construct_mapping(
@@ -248,7 +249,9 @@ def _duplicate_tracking_construct_mapping(
248249
value = construct(value_node)
249250

250251
if loader._mapping_depth == 1:
251-
loader.top_level_key_values.setdefault(key, []).append(copy.deepcopy(value))
252+
duplicated_value = copy.deepcopy(value)
253+
loader.top_level_key_values.setdefault(key, []).append(duplicated_value)
254+
loader.top_level_items.append((copy.deepcopy(key), duplicated_value))
252255

253256
mapping[key] = value
254257

@@ -270,20 +273,27 @@ def __init__(
270273
content: RawConfigData,
271274
*,
272275
duplicate_sections: dict[str, list[t.Any]] | None = None,
276+
top_level_items: list[tuple[str, t.Any]] | None = None,
273277
) -> None:
274278
super().__init__(content)
275279
self._duplicate_sections = duplicate_sections or {}
280+
self._top_level_items = top_level_items or []
276281

277282
@property
278283
def duplicate_sections(self) -> dict[str, list[t.Any]]:
279284
"""Mapping of top-level keys to the list of duplicated values."""
280285
return self._duplicate_sections
281286

287+
@property
288+
def top_level_items(self) -> list[tuple[str, t.Any]]:
289+
"""Ordered list of top-level items, including duplicates."""
290+
return copy.deepcopy(self._top_level_items)
291+
282292
@classmethod
283293
def _load_yaml_with_duplicates(
284294
cls,
285295
content: str,
286-
) -> tuple[dict[str, t.Any], dict[str, list[t.Any]]]:
296+
) -> tuple[dict[str, t.Any], dict[str, list[t.Any]], list[tuple[str, t.Any]]]:
287297
loader = _DuplicateTrackingSafeLoader(content)
288298

289299
try:
@@ -306,33 +316,42 @@ def _load_yaml_with_duplicates(
306316
if len(values) > 1
307317
}
308318

309-
return loaded, duplicate_sections
319+
top_level_items = [
320+
(t.cast("str", key), copy.deepcopy(value))
321+
for key, value in loader.top_level_items
322+
]
323+
324+
return loaded, duplicate_sections, top_level_items
310325

311326
@classmethod
312327
def _load_from_path(
313328
cls,
314329
path: pathlib.Path,
315-
) -> tuple[dict[str, t.Any], dict[str, list[t.Any]]]:
330+
) -> tuple[dict[str, t.Any], dict[str, list[t.Any]], list[tuple[str, t.Any]]]:
316331
if path.suffix.lower() in {".yaml", ".yml"}:
317332
content = path.read_text(encoding="utf-8")
318333
return cls._load_yaml_with_duplicates(content)
319334

320-
return ConfigReader._from_file(path), {}
335+
return ConfigReader._from_file(path), {}, []
321336

322337
@classmethod
323338
def from_file(cls, path: pathlib.Path) -> DuplicateAwareConfigReader:
324-
content, duplicate_sections = cls._load_from_path(path)
325-
return cls(content, duplicate_sections=duplicate_sections)
339+
content, duplicate_sections, top_level_items = cls._load_from_path(path)
340+
return cls(
341+
content,
342+
duplicate_sections=duplicate_sections,
343+
top_level_items=top_level_items,
344+
)
326345

327346
@classmethod
328347
def _from_file(cls, path: pathlib.Path) -> dict[str, t.Any]:
329-
content, _ = cls._load_from_path(path)
348+
content, _, _ = cls._load_from_path(path)
330349
return content
331350

332351
@classmethod
333352
def load_with_duplicates(
334353
cls,
335354
path: pathlib.Path,
336-
) -> tuple[dict[str, t.Any], dict[str, list[t.Any]]]:
355+
) -> tuple[dict[str, t.Any], dict[str, list[t.Any]], list[tuple[str, t.Any]]]:
337356
reader = cls.from_file(path)
338-
return reader.content, reader.duplicate_sections
357+
return reader.content, reader.duplicate_sections, reader.top_level_items

src/vcspull/cli/add.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ def add_repo(
342342
(
343343
raw_config,
344344
duplicate_root_occurrences,
345+
_top_level_items,
345346
) = DuplicateAwareConfigReader.load_with_duplicates(config_file_path)
346347
except TypeError:
347348
log.exception(

src/vcspull/cli/discover.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ def discover_repos(
203203
(
204204
raw_config,
205205
duplicate_root_occurrences,
206+
_top_level_items,
206207
) = DuplicateAwareConfigReader.load_with_duplicates(config_file_path)
207208
except TypeError:
208209
log.exception(

src/vcspull/cli/fmt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def format_single_config(
172172

173173
# Load existing config
174174
try:
175-
raw_config, duplicate_root_occurrences = (
175+
raw_config, duplicate_root_occurrences, _top_level_items = (
176176
DuplicateAwareConfigReader.load_with_duplicates(config_file_path)
177177
)
178178
except TypeError:

src/vcspull/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ def load_configs(
271271
file = pathlib.Path(file)
272272
assert isinstance(file, pathlib.Path)
273273

274-
config_content, duplicate_roots = (
274+
config_content, duplicate_roots, _top_level_items = (
275275
DuplicateAwareConfigReader.load_with_duplicates(file)
276276
)
277277

0 commit comments

Comments
 (0)