• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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