1# Copyright 2025 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Helpers for finding config files.""" 15 16from collections import defaultdict 17from pathlib import Path 18from typing import Iterable, Iterator, List, Mapping 19 20 21def configs_in_parents(config_file_name: str, path: Path) -> Iterator[Path]: 22 """Finds all config files in a file's parent directories. 23 24 Given the following file system: 25 26 .. code-block:: py 27 28 / (root) 29 home/ 30 gregory/ 31 foo.conf 32 pigweed/ 33 foo.conf 34 pw_cli/ 35 baz.txt 36 37 Calling this with ``config_file_name="foo.conf"`` and 38 ``path=/home/gregory/pigweed/pw_cli/baz.txt``, the following config 39 files will be yielded in order: 40 41 - ``/home/gregory/pigweed/foo.conf`` 42 - ``/home/gregory/foo.conf`` 43 44 Args: 45 config_file_name: The basename of the config file of interest. 46 path: The path to search for config files from. 47 48 Yields: 49 The paths to all config files that match the provided 50 ``config_file_name``, ordered from nearest to ``path`` to farthest. 51 """ 52 path = path.resolve() 53 if path.is_file(): 54 path = path.parent 55 while True: 56 maybe_config = path / config_file_name 57 if maybe_config.is_file(): 58 yield maybe_config 59 if str(path) == path.anchor: 60 break 61 path = path.parent 62 63 64def paths_by_nearest_config( 65 config_file_name: str, paths: Iterable[Path] 66) -> Mapping[Path | None, List[Path]]: 67 """Groups a series of paths by their nearest config file. 68 69 For each path in ``paths``, a lookup of the nearest matching config file 70 is performed. Each identified config file is inserted as a key to the 71 dictionary, and the value of each entry is a list containing every input 72 file path that will use said config file. This is well suited for batching 73 calls to tools that require a config file as a passed argument. 74 75 Example: 76 77 .. code-block:: py 78 79 paths_by_config = paths_by_nearest_config( 80 'settings.ini', 81 paths, 82 ) 83 for conf, grouped_paths: 84 subprocess.run( 85 ( 86 'format_files', 87 '--config', 88 conf, 89 *grouped_paths, 90 ) 91 ) 92 93 Args: 94 config_file_name: The basename of the config file of interest. 95 paths: The paths that should be mapped to their nearest config. 96 97 Returns: 98 A dictionary mapping the path of a config file to files that will 99 pick up that config as their nearest config file. 100 """ 101 # If this ends up being slow to do for many files, GitRepoFinder could 102 # be generalized to support caching for this use case too. 103 paths_by_config = defaultdict(list) 104 for path in paths: 105 config = next(configs_in_parents(config_file_name, path), None) 106 paths_by_config[config].append(path) 107 return paths_by_config 108