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
27Following kwargs are defined for functions that accept **kwargs:
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 - :py:meth:`xdgpspconf.utils.fs_perm` kwargs: 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 """
54 def __init__(self, project: str, shipped: os.PathLike = None, **permargs):
55 super().__init__(project, base='config', shipped=shipped, **permargs)
57 def locations(self, cname: str = None) -> Dict[str, List[Path]]:
58 """
59 Shipped, root, user, improper locations
61 Args:
62 cname: name of configuration file
63 Returns:
64 named dictionary containing respective list of Paths
65 """
66 cname = cname or 'config'
67 return {
68 'improper':
69 self.improper_loc(cname),
70 'user_loc':
71 self.user_xdg_loc(cname),
72 'root_loc':
73 self.root_xdg_loc(cname),
74 'shipped':
75 [(self.shipped / cname).with_suffix(ext)
76 for ext in CONF_EXT] if self.shipped is not None else []
77 }
79 def trace_ancestors(self, child_dir: Path) -> List[Path]:
80 """
81 Walk up to nearest mountpoint or project root.
83 - collect all directories containing __init__.py \
84 (assumed to be source directories)
85 - project root is directory that contains ``setup.cfg``
86 or ``setup.py``
87 - mountpoint is a unix mountpoint or windows drive root
88 - I **AM** my 0th ancestor
90 Args:
91 child_dir: walk ancestry of `this` directory
93 Returns:
94 List of Paths to ancestor configs:
95 First directory is most dominant
96 """
97 config = []
98 pedigree = super().trace_ancestors(child_dir)
99 config.extend(
100 (config_dir / f'.{self.project}rc' for config_dir in pedigree))
102 if pedigree:
103 for setup in ('pyproject.toml', 'setup.cfg'):
104 if (pedigree[-1] / setup).is_file():
105 config.append(pedigree[-1] / setup)
106 return config
108 def user_xdg_loc(self, cname: str = 'config') -> List[Path]:
109 """
110 Get XDG_<BASE>_HOME locations.
112 `specifications
113 <https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html>`__
115 Args:
116 cname: name of config file
118 Returns:
119 List of xdg-<base> Paths
120 First directory is most dominant
121 Raises:
122 KeyError: bad variable name
124 """
125 user_base_loc = super().user_xdg_loc()
126 config = []
127 for ext in CONF_EXT:
128 for loc in user_base_loc:
129 config.append((loc / cname).with_suffix(ext))
130 config.append(loc.with_suffix(ext))
131 return config
133 def root_xdg_loc(self, cname: str = 'config') -> List[Path]:
134 """
135 Get ROOT's counterparts of XDG_<BASE>_HOME locations.
137 `specifications
138 <https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html>`__
140 Args:
141 cname: name of config file
143 Returns:
144 List of root-<base> Paths (parents to project's base)
145 First directory is most dominant
146 Raises:
147 KeyError: bad variable name
149 """
150 root_base_loc = super().root_xdg_loc()
151 config = []
152 for ext in CONF_EXT:
153 for loc in root_base_loc:
154 config.append((loc / cname).with_suffix(ext))
155 config.append(loc.with_suffix(ext))
156 return config
158 def improper_loc(self, cname: str = 'config') -> List[Path]:
159 """
160 Get ROOT's counterparts of XDG_<BASE>_HOME locations.
162 `specifications
163 <https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html>`__
165 Args:
166 cname: name of config file
168 Returns:
169 List of root-<base> Paths (parents to project's base)
170 First directory is most dominant
171 Raises:
172 KeyError: bad variable name
174 """
175 improper_base_loc = super().improper_loc()
176 config = []
177 for ext in CONF_EXT:
178 for loc in improper_base_loc:
179 config.append((loc / cname).with_suffix(ext))
180 config.append(loc.with_suffix(ext))
181 return config
183 def get_conf(self,
184 dom_start: bool = True,
185 improper: bool = False,
186 **kwargs) -> List[Path]:
187 """
188 Get discovered configuration files.
190 Args:
191 dom_start: when ``False``, end with most dominant
192 improper: include improper locations such as *~/.project*
193 **kwargs
195 Returns:
196 List of configuration paths
197 """
198 dom_order: List[Path] = []
200 custom = kwargs.get('custom')
201 if custom is not None:
202 # don't check
203 dom_order.append(Path(custom))
205 rc_val = os.environ.get(self.project.upper() + 'RC')
206 if rc_val is not None:
207 if not Path(rc_val).is_file():
208 raise FileNotFoundError(
209 f'RC configuration file: {rc_val} not found')
210 dom_order.append(Path(rc_val))
212 trace_pwd = kwargs.get('trace_pwd')
213 if trace_pwd is True:
214 trace_pwd = Path('.').resolve()
215 if trace_pwd:
216 inheritance = self.trace_ancestors(Path(trace_pwd))
217 dom_order.extend(inheritance)
219 locations = self.locations(kwargs.get('cname'))
220 if improper:
221 dom_order.extend(locations['improper'])
223 for loc in ('user_loc', 'root_loc', 'shipped'):
224 dom_order.extend(locations[loc])
226 permargs = {
227 key: val
228 for key, val in kwargs.items()
229 if key in ('mode', 'dir_fs', 'effective_ids', 'follow_symlinks')
230 }
231 permargs = {**self.permargs, **permargs}
232 dom_order = list(filter(lambda x: fs_perm(x, **permargs), dom_order))
233 if dom_start:
234 return dom_order
235 return list(reversed(dom_order))
237 def safe_config(self,
238 ext: Union[str, List[str]] = None,
239 **kwargs) -> List[Path]:
240 """
241 Locate safe writable paths of configuration files.
243 - Doesn't care about accessibility or existance of locations.
244 - User must catch:
245 - ``PermissionError``
246 - ``IsADirectoryError``
247 - ``FileNotFoundError``
248 - Improper locations (*~/.project*) are deliberately dropped
249 - Recommendation: Try saving your configuration in in reversed order
251 Args:
252 ext: extension filter(s)
253 **kwargs
255 Returns:
256 Paths: First path is most dominant
258 """
259 kwargs['mode'] = kwargs.get('mode', 2)
260 if isinstance(ext, str):
261 ext = [ext]
262 safe_paths: List[Path] = []
263 for loc in self.get_conf(**kwargs):
264 if any(private in str(loc)
265 for private in ('site-packages', 'venv', '/etc', 'setup',
266 'pyproject')):
267 continue
268 if ext and loc.suffix and loc.suffix not in list(ext):
269 continue
270 safe_paths.append(loc)
271 return safe_paths
273 def read_config(self,
274 flatten: bool = False,
275 **kwargs) -> Dict[Path, Dict[str, Any]]:
276 """
277 Locate Paths to standard directories and parse config.
279 Args:
280 flatten: superimpose configurations to return the final outcome
281 **kwargs
283 Returns:
284 parsed configuration from each available file:
285 first file is most dominant
287 Raises:
288 BadConf- Bad configuration file format
290 """
291 kwargs['mode'] = kwargs.get('mode', 4)
292 avail_confs: Dict[Path, Dict[str, Any]] = {}
293 # load configs from oldest ancestor to current directory
294 for config in self.get_conf(**kwargs):
295 try:
296 avail_confs[config] = parse_rc(config, project=self.project)
297 except (PermissionError, FileNotFoundError, IsADirectoryError):
298 pass
300 if not flatten:
301 return avail_confs
303 super_config: Dict[str, Any] = {}
304 for config in reversed(list(avail_confs.values())):
305 super_config.update(config)
306 return {list(avail_confs.keys())[0]: super_config}
308 def write_config(self,
309 data: Dict[str, Any],
310 force: str = 'fail',
311 **kwargs) -> bool:
312 """
313 Write data to a safe configuration file.
315 Args:
316 data: serial data to save
317 force: force overwrite {'overwrite','update','fail'}
318 **kwargs
320 Returns: success
321 """
322 config_l = list(
323 reversed(self.safe_config(ext=kwargs.get('ext'), **kwargs)))
324 for config in config_l:
325 try:
326 return write_rc(data, config, force=force)
327 except (PermissionError, IsADirectoryError, FileNotFoundError):
328 continue
329 return False