Coverage for /home/pradyumna/Languages/python/packages/xdgpspconf/xdgpspconf/config.py : 84%

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