Coverage for /home/pradyumna/Languages/python/packages/xdgpspconf/xdgpspconf/config.py: 83%
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, Union
43from xdgpspconf.base import FsDisc
44from xdgpspconf.config_io import CONF_EXT, parse_rc, write_rc
45from xdgpspconf.utils import fs_perm
48class ConfDisc(FsDisc):
49 """
50 CONF DISCoverer
52 Each location is config file, NOT directory as with FsDisc
53 """
55 def __init__(self, project: str, shipped: os.PathLike = None, **permargs):
56 super().__init__(project, base='config', shipped=shipped, **permargs)
58 def locations(self, cname: str = None) -> Dict[str, List[Path]]:
59 """
60 Shipped, root, user, improper locations
62 Args:
63 cname: name of configuration file
65 Returns:
66 named dictionary containing respective list of Paths
67 """
68 cname = cname or 'config'
69 return {
70 'improper':
71 self.improper_loc(cname),
72 'user_loc':
73 self.user_xdg_loc(cname),
74 'root_loc':
75 self.root_xdg_loc(cname),
76 'shipped':
77 [(self.shipped / cname).with_suffix(ext)
78 for ext in CONF_EXT] if self.shipped is not None else []
79 }
81 def trace_ancestors(self, child_dir: Path) -> List[Path]:
82 """
83 Walk up to nearest mountpoint or project root.
85 - collect all directories containing __init__.py \
86 (assumed to be source directories)
87 - project root is directory that contains ``setup.cfg``
88 or ``setup.py``
89 - mountpoint is a unix mountpoint or windows drive root
90 - I **AM** my 0th ancestor
92 Args:
93 child_dir: walk ancestry of `this` directory
95 Returns:
96 List of Paths to ancestor configs:
97 First directory is most dominant
98 """
99 config = []
100 pedigree = super().trace_ancestors(child_dir)
101 if child_dir not in pedigree:
102 pedigree = [child_dir, *pedigree]
103 config.extend(
104 (config_dir / f'.{self.project}rc' for config_dir in pedigree))
106 if pedigree:
107 for setup in ('pyproject.toml', 'setup.cfg'):
108 if (pedigree[-1] / setup).is_file():
109 config.append(pedigree[-1] / setup)
110 return config
112 def user_xdg_loc(self, cname: str = 'config') -> List[Path]:
113 """
114 Get XDG_<BASE>_HOME locations.
116 Args:
117 cname: name of config file
119 Returns:
120 List of xdg-<base> Paths
121 First directory is most dominant
122 Raises:
123 KeyError: bad variable name
125 """
126 user_base_loc = super().user_xdg_loc()
127 config = []
128 for ext in CONF_EXT:
129 for loc in user_base_loc:
130 config.append((loc / cname).with_suffix(ext))
131 config.append(loc.with_suffix(ext))
132 return config
134 def root_xdg_loc(self, cname: str = 'config') -> List[Path]:
135 """
136 Get ROOT's counterparts of XDG_<BASE>_HOME locations.
138 Args:
139 cname: name of config file
141 Returns:
142 List of root-<base> Paths (parents to project's base)
143 First directory is most dominant
144 Raises:
145 KeyError: bad variable name
147 """
148 root_base_loc = super().root_xdg_loc()
149 config = []
150 for ext in CONF_EXT:
151 for loc in root_base_loc:
152 config.append((loc / cname).with_suffix(ext))
153 config.append(loc.with_suffix(ext))
154 return config
156 def improper_loc(self, cname: str = 'config') -> List[Path]:
157 """
158 Get ROOT's counterparts of XDG_<BASE>_HOME locations.
160 Args:
161 cname: name of config file
163 Returns:
164 List of root-<base> Paths (parents to project's base)
165 First directory is most dominant
166 Raises:
167 KeyError: bad variable name
169 """
170 improper_base_loc = super().improper_loc()
171 config = []
172 for ext in CONF_EXT:
173 for loc in improper_base_loc:
174 config.append((loc / cname).with_suffix(ext))
175 config.append(loc.with_suffix(ext))
176 return config
178 def get_conf(self,
179 dom_start: bool = True,
180 improper: bool = False,
181 **kwargs) -> List[Path]:
182 """
183 Get discovered configuration files.
185 Args:
186 dom_start: when ``False``, end with most dominant
187 improper: include improper locations such as *~/.project*
188 **kwargs:
189 - custom: custom location
190 - cname: name of configuration file. Default: 'config'
191 - trace_pwd: when supplied, walk up to mountpoint or
192 project-root and inherit all locations that contain
193 ``__init__.py``. Project-root is identified by discovery of
194 ``setup.py`` or ``setup.cfg``. Mountpoint is ``is_mount``
195 in unix or Drive in Windows. If ``True``, walk from ``$PWD``
196 - permargs passed on to :py:meth:`xdgpspconf.utils.fs_perm`
198 Returns:
199 List of configuration paths
200 """
201 dom_order: List[Path] = []
203 custom = kwargs.get('custom')
204 if custom is not None:
205 # don't check
206 dom_order.append(Path(custom))
208 rc_val = os.environ.get(self.project.upper() + 'RC')
209 if rc_val is not None:
210 if not Path(rc_val).is_file():
211 raise FileNotFoundError(
212 f'RC configuration file: {rc_val} not found')
213 dom_order.append(Path(rc_val))
215 trace_pwd = kwargs.get('trace_pwd')
216 if trace_pwd is True:
217 trace_pwd = Path('.').resolve()
218 if trace_pwd:
219 inheritance = self.trace_ancestors(Path(trace_pwd))
220 dom_order.extend(inheritance)
222 locations = self.locations(kwargs.get('cname'))
223 if improper:
224 dom_order.extend(locations['improper'])
226 for loc in ('user_loc', 'root_loc', 'shipped'):
227 dom_order.extend(locations[loc])
229 permargs = {
230 key: val
231 for key, val in kwargs.items()
232 if key in ('mode', 'dir_fs', 'effective_ids', 'follow_symlinks')
233 }
234 permargs = {**self.permargs, **permargs}
235 dom_order = list(filter(lambda x: fs_perm(x, **permargs), dom_order))
236 if dom_start:
237 return dom_order
238 return list(reversed(dom_order))
240 def safe_config(self,
241 ext: Union[str, List[str]] = None,
242 **kwargs) -> List[Path]:
243 """
244 Locate safe writable paths of configuration files.
246 - Doesn't care about accessibility or existance of locations.
247 - User must catch:
248 - ``PermissionError``
249 - ``IsADirectoryError``
250 - ``FileNotFoundError``
251 - Improper locations (*~/.project*) are deliberately dropped
252 - Recommendation: Try saving your configuration in in reversed order
254 Args:
255 ext: extension filter(s)
256 **kwargs:
257 - custom: custom location
258 - cname: name of configuration file. Default: 'config'
259 - trace_pwd: when supplied, walk up to mountpoint or
260 project-root and inherit all locations that contain
261 ``__init__.py``. Project-root is identified by discovery of
262 ``setup.py`` or ``setup.cfg``. Mountpoint is ``is_mount``
263 in unix or Drive in Windows. If ``True``, walk from ``$PWD``
264 - permargs passed on to :py:meth:`xdgpspconf.utils.fs_perm`
267 Returns:
268 Paths: First path is most dominant
270 """
271 kwargs['mode'] = kwargs.get('mode', 2)
272 if isinstance(ext, str):
273 ext = [ext]
274 safe_paths: List[Path] = []
275 for loc in self.get_conf(**kwargs):
276 if any(private in str(loc)
277 for private in ('site-packages', 'venv', '/etc', 'setup',
278 'pyproject')):
279 continue
280 if ext and loc.suffix and loc.suffix not in list(ext):
281 continue
282 safe_paths.append(loc)
283 return safe_paths
285 def read_config(self,
286 flatten: bool = False,
287 **kwargs) -> Dict[Path, Dict[str, Any]]:
288 """
289 Locate Paths to standard directories and parse config.
291 Args:
292 flatten: superimpose configurations to return the final outcome
293 **kwargs:
294 - custom: custom location
295 - cname: name of configuration file. Default: 'config'
296 - trace_pwd: when supplied, walk up to mountpoint or
297 project-root and inherit all locations that contain
298 ``__init__.py``. Project-root is identified by discovery of
299 ``setup.py`` or ``setup.cfg``. Mountpoint is ``is_mount``
300 in unix or Drive in Windows. If ``True``, walk from ``$PWD``
301 - permargs passed on to :py:meth:`xdgpspconf.utils.fs_perm`
303 Returns:
304 parsed configuration from each available file:
305 first file is most dominant
307 Raises:
308 BadConf- Bad configuration file format
310 """
311 kwargs['mode'] = kwargs.get('mode', 4)
312 avail_confs: Dict[Path, Dict[str, Any]] = {}
313 # load configs from oldest ancestor to current directory
314 for config in self.get_conf(**kwargs):
315 try:
316 avail_confs[config] = parse_rc(config, project=self.project)
317 except (PermissionError, FileNotFoundError, IsADirectoryError):
318 pass
320 if not flatten:
321 return avail_confs
323 super_config: Dict[str, Any] = {}
324 for config in reversed(list(avail_confs.values())):
325 super_config.update(config)
326 return {Path('.').resolve(): super_config}
328 def write_config(self,
329 data: Dict[str, Any],
330 force: str = 'fail',
331 **kwargs) -> bool:
332 """
333 Write data to a safe configuration file.
335 Args:
336 data: serial data to save
337 force: force overwrite {'overwrite','update','fail'}
338 **kwargs
340 Returns: success
341 """
342 config_l = list(
343 reversed(self.safe_config(ext=kwargs.get('ext'), **kwargs)))
344 for config in config_l:
345 try:
346 return write_rc(data, config, force=force)
347 except (PermissionError, IsADirectoryError, FileNotFoundError):
348 continue
349 return False