Coverage for /home/pradyumna/Languages/python/packages/xdgpspconf/xdgpspconf/config.py: 100%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python3
2# -*- coding: utf-8; mode: python; -*-
3# Copyright © 2021, 2022 Pradyumna Paranjape
4#
5# This file is part of xdgpspconf.
6#
7# xdgpspconf is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Lesser General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# xdgpspconf is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with xdgpspconf. If not, see <https://www.gnu.org/licenses/>. #
19"""
20Special case of configuration, where base object is a file
22Read:
23 - standard xdg-base locations
24 - current directory and ancestors
25 - custom location
27Following kwargs are defined for some functions as indicated:
28 - custom: custom location
29 - cname: name of config file
30 - ext: extension restriction filter(s)
31 - trace_pwd: when supplied, walk up to mountpoint or project-root and
32 inherit all locations that contain __init__.py. Project-root is
33 identified by discovery of ``setup.py`` or ``setup.cfg``. Mountpoint is
34 ``is_mount`` in unix or Drive in Windows. If ``True``, walk from ``$PWD``
35 - kwargs of :py:meth:`xdgpspconf.utils.fs_perm`: passed on
37"""
39import os
40from pathlib import Path
41from typing import Any, Dict, List, Optional, Union
43from xdgpspconf.base import FsDisc
44from xdgpspconf.config_io import parse_rc, write_rc
45from xdgpspconf.utils import fs_perm
47CONF_EXT = '.yml', '.yaml', '.toml', '.conf', '.ini'
48"""Extensions that are supported (parsed) by this module"""
51class ConfDisc(FsDisc):
52 """
53 CONF DISCoverer
55 Each location is config file, NOT directory as with FsDisc
56 """
58 def __init__(self,
59 project: str,
60 shipped: Union[Path, str] = None,
61 **permargs):
62 super().__init__(project, base='config', shipped=shipped, **permargs)
64 def locations(self, cname: str = None) -> Dict[str, List[Path]]:
65 """
66 Shipped, root, user, improper locations
68 Args:
69 cname: name of configuration file
71 Returns:
72 named dictionary containing respective list of Paths
73 """
74 cname = cname or 'config'
75 return {
76 'improper':
77 self.improper_loc(cname),
78 'user_loc':
79 self.user_xdg_loc(cname),
80 'root_loc':
81 self.root_xdg_loc(cname),
82 'shipped':
83 [(self.shipped / cname).with_suffix(ext)
84 for ext in CONF_EXT] if self.shipped is not None else []
85 }
87 def trace_ancestors(self, child_dir: Path) -> List[Path]:
88 """
89 Walk up to nearest mountpoint or project root.
91 - collect all directories containing __init__.py \
92 (assumed to be source directories)
93 - project root is directory that contains ``setup.cfg``
94 or ``setup.py``
95 - mountpoint is a unix mountpoint or windows drive root
96 - I **AM** my 0th ancestor
98 Args:
99 child_dir: walk ancestry of `this` directory
101 Returns:
102 List of Paths to ancestor configs:
103 First directory is most dominant
104 """
105 config = []
106 pedigree = super().trace_ancestors(child_dir)
107 if child_dir not in pedigree: # pragma: no cover
108 pedigree = [child_dir, *pedigree]
109 config.extend(
110 (config_dir / f'.{self.project}rc' for config_dir in pedigree))
112 if pedigree:
113 for setup in ('pyproject.toml', 'setup.cfg'):
114 if (pedigree[-1] / setup).is_file():
115 config.append(pedigree[-1] / setup)
116 return config
118 def user_xdg_loc(self, cname: str = 'config') -> List[Path]:
119 """
120 Get XDG_<BASE>_HOME locations.
122 Args:
123 cname: name of config file
125 Returns:
126 List of xdg-<base> Paths
127 First directory is most dominant
128 Raises:
129 KeyError: bad variable name
131 """
132 user_base_loc = super().user_xdg_loc()
133 config = []
134 for ext in CONF_EXT:
135 for loc in user_base_loc:
136 config.append((loc / cname).with_suffix(ext))
137 config.append(loc.with_suffix(ext))
138 return config
140 def root_xdg_loc(self, cname: str = 'config') -> List[Path]:
141 """
142 Get ROOT's counterparts of XDG_<BASE>_HOME locations.
144 Args:
145 cname: name of config file
147 Returns:
148 List of root-<base> Paths (parents to project's base)
149 First directory is most dominant
150 Raises:
151 KeyError: bad variable name
153 """
154 root_base_loc = super().root_xdg_loc()
155 config = []
156 for ext in CONF_EXT:
157 for loc in root_base_loc:
158 config.append((loc / cname).with_suffix(ext))
159 config.append(loc.with_suffix(ext))
160 return config
162 def improper_loc(self, cname: str = 'config') -> List[Path]:
163 """
164 Get ROOT's counterparts of XDG_<BASE>_HOME locations.
166 Args:
167 cname: name of config file
169 Returns:
170 List of root-<base> Paths (parents to project's base)
171 First directory is most dominant
172 Raises:
173 KeyError: bad variable name
175 """
176 improper_base_loc = super().improper_loc()
177 config = []
178 for ext in CONF_EXT:
179 for loc in improper_base_loc:
180 config.append((loc / cname).with_suffix(ext))
181 config.append(loc.with_suffix(ext))
182 return config
184 def get_conf(self,
185 dom_start: bool = True,
186 improper: bool = False,
187 **kwargs) -> List[Path]:
188 """
189 Get discovered configuration files.
191 Args:
192 dom_start: when ``False``, end with most dominant
193 improper: include improper locations such as *~/.project*
194 **kwargs:
195 - custom: custom location
196 - cname: name of configuration file. Default: 'config'
197 - trace_pwd: when supplied, walk up to mountpoint or
198 project-root and inherit all locations that contain
199 ``__init__.py``. Project-root is identified by discovery of
200 ``setup.py`` or ``setup.cfg``. Mountpoint is ``is_mount``
201 in unix or Drive in Windows. If ``True``, walk from ``$PWD``
202 - permargs passed on to :py:meth:`xdgpspconf.utils.fs_perm`
204 Returns:
205 List of configuration paths
206 """
207 # NOTE: order of following statements IS important
208 dom_order: List[Path] = []
210 custom = kwargs.get('custom')
211 if custom is not None:
212 # assume existence and proceed
213 dom_order.append(Path(custom))
215 rc_val = os.environ.get(self.project.upper() + 'RC')
216 if rc_val is not None: # pragma: no cover
217 if not Path(rc_val).is_file():
218 raise FileNotFoundError(
219 f'RC configuration file: {rc_val} not found')
220 dom_order.append(Path(rc_val))
222 trace_pwd = kwargs.get('trace_pwd')
223 if trace_pwd is True:
224 trace_pwd = Path('.').resolve()
225 if trace_pwd:
226 inheritance = self.trace_ancestors(Path(trace_pwd))
227 dom_order.extend(inheritance)
229 locations = self.locations(kwargs.get('cname'))
230 if improper:
231 dom_order.extend(locations['improper'])
233 for loc in ('user_loc', 'root_loc', 'shipped'):
234 dom_order.extend(locations[loc])
236 permargs = {
237 key: val
238 for key, val in kwargs.items()
239 if key in ('mode', 'dir_fs', 'effective_ids', 'follow_symlinks')
240 }
241 permargs = {**self.permargs, **permargs}
242 dom_order = list(filter(lambda x: fs_perm(x, **permargs), dom_order))
243 if dom_start:
244 return dom_order
245 return list(reversed(dom_order))
247 def safe_config(self,
248 ext: Union[str, List[str]] = None,
249 **kwargs) -> List[Path]:
250 """
251 Locate safe writeable paths of configuration files.
253 - Doesn't care about accessibility or existance of locations.
254 - User must catch:
255 - ``PermissionError``
256 - ``IsADirectoryError``
257 - ``FileNotFoundError``
258 - Improper locations (*~/.project*) are deliberately dropped
259 - Recommendation: use dom_start=``False``
261 Args:
262 ext: extension filter(s)
263 **kwargs:
264 - custom: custom location
265 - cname: name of configuration file. Default: 'config'
266 - trace_pwd: when supplied, walk up to mountpoint or
267 project-root and inherit all locations that contain
268 ``__init__.py``. Project-root is identified by discovery of
269 ``setup.py`` or ``setup.cfg``. Mountpoint is ``is_mount``
270 in unix or Drive in Windows. If ``True``, walk from ``$PWD``
271 - dom_start: when ``False``, end with most dominant
272 - permargs passed on to :py:meth:`xdgpspconf.utils.fs_perm`
275 Returns:
276 Safe configuration locations (existing and prospective)
278 """
279 kwargs['mode'] = kwargs.get('mode', 2)
281 # filter private locations
282 private_locs = ['site-packages', 'venv', '/etc', 'setup', 'pyproject']
283 if self.shipped is not None:
284 private_locs.append(str(self.shipped))
285 safe_paths = filter(
286 lambda x: not any(private in str(x) for private in private_locs),
287 self.get_conf(**kwargs))
288 if ext is None:
289 return list(safe_paths)
291 # filter extensions
292 if isinstance(ext, str):
293 ext = [ext]
294 return list(filter(lambda x: x.suffix in ext, safe_paths))
296 def read_config(self,
297 flatten: bool = False,
298 **kwargs) -> Dict[Path, Dict[str, Any]]:
299 """
300 Locate Paths to standard directories and parse config.
302 Args:
303 flatten: superimpose configurations to return the final outcome
304 **kwargs:
305 - custom: custom location
306 - cname: name of configuration file. Default: 'config'
307 - trace_pwd: when supplied, walk up to mountpoint or
308 project-root and inherit all locations that contain
309 ``__init__.py``. Project-root is identified by discovery of
310 ``setup.py`` or ``setup.cfg``. Mountpoint is ``is_mount``
311 in unix or Drive in Windows. If ``True``, walk from ``$PWD``
312 - dom_start: when ``False``, end with most dominant
313 - permargs passed on to :py:meth:`xdgpspconf.utils.fs_perm`
315 Returns:
316 parsed configuration from each available file
318 Raises:
319 BadConf- Bad configuration file format
321 """
322 kwargs['mode'] = kwargs.get('mode', 4)
323 avail_confs: Dict[Path, Dict[str, Any]] = {}
324 # load configs from oldest ancestor to current directory
325 for config in self.get_conf(**kwargs):
326 try:
327 avail_confs[config] = parse_rc(config, project=self.project)
328 except (PermissionError, FileNotFoundError, IsADirectoryError):
329 pass
331 if not flatten:
332 return avail_confs
334 super_config: Dict[str, Any] = {}
335 for config in reversed(list(avail_confs.values())):
336 super_config.update(config)
337 return {Path('.').resolve(): super_config}
339 def write_config(self,
340 data: Dict[str, Any],
341 force: str = 'fail',
342 **kwargs) -> Optional[Path]:
343 """
344 Write data to the most global safe configuration file.
346 Args:
347 data: serial data to save
348 force: force overwrite {'overwrite','update','fail'}
349 **kwargs
351 Returns: Path to which, configuration was written
352 """
353 kwargs['dom_start'] = kwargs.get('dom_start', False)
354 config_l = self.safe_config(**kwargs)
355 for config in config_l:
356 try:
357 if write_rc(data, config, force=force):
358 return config
359 except (PermissionError, IsADirectoryError, FileNotFoundError):
360 continue
361 return None # pragma: no cover