|
8 | 8 | import re |
9 | 9 | import shlex |
10 | 10 | from typing import Iterable, Optional, Union |
| 11 | +from glob import iglob |
11 | 12 |
|
12 | 13 | import configobj |
13 | 14 | try: |
@@ -94,7 +95,9 @@ def __init__(self, config_file: Optional[ConfigFile] = None) -> None: |
94 | 95 | self.abooks: AddressBookCollection |
95 | 96 | locale.setlocale(locale.LC_ALL, '') |
96 | 97 | config = self._load_config_file(config_file) |
97 | | - self.config = self._validate(config) |
| 98 | + config = self._validate(config) |
| 99 | + config["addressbooks"] = self._unfold_discover_books(config["addressbooks"]) |
| 100 | + self.config = config |
98 | 101 | self._set_attributes() |
99 | 102 |
|
100 | 103 | @classmethod |
@@ -137,6 +140,53 @@ def _validate(config: configobj.ConfigObj) -> configobj.ConfigObj: |
137 | 140 | raise ConfigError |
138 | 141 | return config |
139 | 142 |
|
| 143 | + @classmethod |
| 144 | + def _unfold_discover_books(cls, addressbooks: configobj.Section) -> configobj.Section: |
| 145 | + """Expand globs in path of addressbooks of type "discover" |
| 146 | +
|
| 147 | + This expands all addressbooks of type "discover" into (potentially) |
| 148 | + multiple addressbooks of type "vdir". The names are automatically generated |
| 149 | + based on the directory name. |
| 150 | +
|
| 151 | + :param config: the configuration to be changed |
| 152 | + :returns: the changed configuration with no "discover" addressbooks |
| 153 | + """ |
| 154 | + for section_name, book in addressbooks.copy().items(): |
| 155 | + if book["type"] != "discover": |
| 156 | + continue |
| 157 | + hits = iglob(os.path.expanduser(book["path"]), recursive=True) |
| 158 | + dirs = cls._find_leaf_dirs(hits) |
| 159 | + for bookpath in dirs: |
| 160 | + bookname = os.path.basename(bookpath) |
| 161 | + # Make sure our name is unique |
| 162 | + counter = 0 |
| 163 | + while bookname in addressbooks: |
| 164 | + counter += 1 |
| 165 | + if bookname + f"-{counter}" in addressbooks: |
| 166 | + continue |
| 167 | + bookname += f"-{counter}" |
| 168 | + break |
| 169 | + addressbooks[bookname] = { |
| 170 | + "type": "vdir", |
| 171 | + "path": bookpath, |
| 172 | + } |
| 173 | + addressbooks.pop(section_name) |
| 174 | + return addressbooks |
| 175 | + |
| 176 | + @staticmethod |
| 177 | + def _find_leaf_dirs(hits: Iterable[str]) -> set[str]: |
| 178 | + """Find leaf directories in a tree of hits when using glob.iglob |
| 179 | +
|
| 180 | + The hits are neither guaranteed to be unique nor leaf directories, both |
| 181 | + of which are enforced by this function. |
| 182 | +
|
| 183 | + :param hits: the hits of a glob as returned by glob.iglob |
| 184 | + :returns: a set of path strings |
| 185 | + """ |
| 186 | + dirs = {os.path.normpath(hit) for hit in hits if os.path.isdir(hit)} |
| 187 | + parents = {os.path.normpath(os.path.join(dir, os.pardir)) for dir in dirs} |
| 188 | + return dirs - parents |
| 189 | + |
140 | 190 | def _set_attributes(self) -> None: |
141 | 191 | """Set the attributes from the internal config instance on self.""" |
142 | 192 | general = self.config["general"] |
|
0 commit comments