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 CONF_EXT, 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':
59 self.improper_loc(cname),
60 'user_loc':
61 self.user_xdg_loc(cname),
62 'root_loc':
63 self.root_xdg_loc(cname),
64 'shipped':
65 [(self.shipped / cname).with_suffix(ext)
66 for ext in CONF_EXT] if self.shipped is not None else []
67 }
69 def trace_ancestors(self, child_dir: Path) -> List[Path]:
70 """
71 Walk up to nearest mountpoint or project root.
73 - collect all directories containing __init__.py \
74 (assumed to be source directories)
75 - project root is directory that contains ``setup.cfg``
76 or ``setup.py``
77 - mountpoint is a unix mountpoint or windows drive root
78 - I **AM** my 0th ancestor
80 Args:
81 child_dir: walk ancestry of `this` directory
83 Returns:
84 List of Paths to ancestor configs:
85 First directory is most dominant
86 """
87 config = []
88 pedigree = super().trace_ancestors(child_dir)
89 config.extend(
90 (config_dir / f'.{self.project}rc' for config_dir in pedigree))
92 if pedigree:
93 for setup in ('pyproject.toml', 'setup.cfg'):
94 if (pedigree[-1] / setup).is_file():
95 config.append(pedigree[-1] / setup)
96 return config
98 def user_xdg_loc(self, cname: str = 'config') -> List[Path]:
99 """
100 Get XDG_<BASE>_HOME locations.
102 `specifications
103 <https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html>`__
105 Args:
106 cname: name of config file
108 Returns:
109 List of xdg-<base> Paths
110 First directory is most dominant
111 Raises:
112 KeyError: bad variable name
114 """
115 user_base_loc = super().user_xdg_loc()
116 config = []
117 for ext in CONF_EXT:
118 for loc in user_base_loc:
119 config.append((loc / cname).with_suffix(ext))
120 config.append(loc.with_suffix(ext))
121 return config
123 def root_xdg_loc(self, cname: str = 'config') -> List[Path]:
124 """
125 Get ROOT's counterparts of XDG_<BASE>_HOME locations.
127 `specifications
128 <https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html>`__
130 Args:
131 cname: name of config file
133 Returns:
134 List of root-<base> Paths (parents to project's base)
135 First directory is most dominant
136 Raises:
137 KeyError: bad variable name
139 """
140 root_base_loc = super().root_xdg_loc()
141 config = []
142 for ext in CONF_EXT:
143 for loc in root_base_loc:
144 config.append((loc / cname).with_suffix(ext))
145 config.append(loc.with_suffix(ext))
146 return config
148 def improper_loc(self, cname: str = 'config') -> List[Path]:
149 """
150 Get ROOT's counterparts of XDG_<BASE>_HOME locations.
152 `specifications
153 <https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html>`__
155 Args:
156 cname: name of config file
158 Returns:
159 List of root-<base> Paths (parents to project's base)
160 First directory is most dominant
161 Raises:
162 KeyError: bad variable name
164 """
165 improper_base_loc = super().improper_loc()
166 config = []
167 for ext in CONF_EXT:
168 for loc in improper_base_loc:
169 config.append((loc / cname).with_suffix(ext))
170 config.append(loc.with_suffix(ext))
171 return config
173 def get_conf(self,
174 dom_start: bool = True,
175 improper: bool = False,
176 **kwargs) -> List[Path]:
177 """
178 Get discovered configuration files.
180 Args:
181 dom_start: when ``False``, end with most dominant
182 improper: include improper locations such as *~/.project*
183 **kwargs:
184 - custom: custom location
185 - trace_pwd: when supplied, walk up to mountpoint or
186 project-root and inherit all locations that contain
187 __init__.py. Project-root is identified by discovery of
188 ``setup.py`` or ``setup.cfg``. Mountpoint is ``is_mount``
189 in unix or Drive in Windows. If ``True``, walk from ``$PWD``
190 - cname: name of config file
191 - :py:meth:`xdgpspconf.utils.fs_perm` kwargs: passed on
192 """
193 dom_order: List[Path] = []
195 custom = kwargs.get('custom')
196 if custom is not None:
197 # don't check
198 dom_order.append(Path(custom))
200 rc_val = os.environ.get(self.project.upper() + 'RC')
201 if rc_val is not None:
202 if not Path(rc_val).is_file():
203 raise FileNotFoundError(
204 f'RC configuration file: {rc_val} not found')
205 dom_order.append(Path(rc_val))
207 trace_pwd = kwargs.get('trace_pwd')
208 if trace_pwd is True:
209 trace_pwd = Path('.').resolve()
210 if trace_pwd:
211 inheritance = self.trace_ancestors(Path(trace_pwd))
212 dom_order.extend(inheritance)
214 locations = self.locations(kwargs.get('cname'))
215 if improper:
216 dom_order.extend(locations['improper'])
218 for loc in ('user_loc', 'root_loc', 'shipped'):
219 dom_order.extend(locations[loc])
221 permargs = {
222 key: val
223 for key, val in kwargs.items()
224 if key in ('mode', 'dir_fs', 'effective_ids', 'follow_symlinks')
225 }
226 permargs = {**self.permargs, **permargs}
227 dom_order = list(filter(lambda x: fs_perm(x, **permargs), dom_order))
228 if dom_start:
229 return dom_order
230 return list(reversed(dom_order))
232 def safe_config(self,
233 ext: Union[str, List[str]] = None,
234 **kwargs) -> List[Path]:
235 """
236 Locate safe writable paths of configuration files.
238 - Doesn't care about accessibility or existance of locations.
239 - User must catch:
240 - ``PermissionError``
241 - ``IsADirectoryError``
242 - ``FileNotFoundError``
243 - Improper locations (*~/.project*) are deliberately dropped
244 - Recommendation: Try saving your configuration in in reversed order
246 Args:
247 ext: extension filter(s)
248 **kwargs:
249 - custom: custom location
250 - trace_pwd: when supplied, walk up to mountpoint or
251 project-root and inherit all locations that contain
252 __init__.py. Project-root is identified by discovery of
253 ``setup.py`` or ``setup.cfg``. Mountpoint is ``is_mount``
254 in unix or Drive in Windows. If ``True``, walk from ``$PWD``
255 - cname: name of config file
256 - :py:meth:`xdgpspconf.utils.fs_perm` kwargs: passed on
258 Returns:
259 Paths: First path is most dominant
261 """
262 kwargs['mode'] = kwargs.get('mode', 2)
263 if isinstance(ext, str):
264 ext = [ext]
265 safe_paths: List[Path] = []
266 for loc in self.get_conf(**kwargs):
267 if any(private in str(loc)
268 for private in ('site-packages', 'venv', '/etc', 'setup',
269 'pyproject')):
270 continue
271 if ext and loc.suffix and loc.suffix not in list(ext):
272 continue
273 safe_paths.append(loc)
274 return safe_paths
276 def read_config(self,
277 flatten: bool = False,
278 **kwargs) -> Dict[Path, Dict[str, Any]]:
279 """
280 Locate Paths to standard directories and parse config.
282 Args:
283 flatten: superimpose configurations to return the final outcome
284 **kwargs:
285 - custom: custom location
286 - trace_pwd: when supplied, walk up to mountpoint or
287 project-root and inherit all locations that contain
288 __init__.py. Project-root is identified by discovery of
289 ``setup.py`` or ``setup.cfg``. Mountpoint is ``is_mount``
290 in unix or Drive in Windows. If ``True``, walk from ``$PWD``
291 - cname: name of config file
292 - :py:meth:`xdgpspconf.utils.fs_perm` kwargs: passed on
294 Returns:
295 parsed configuration from each available file:
296 first file is most dominant
298 Raises:
299 BadConf- Bad configuration file format
301 """
302 kwargs['mode'] = kwargs.get('mode', 4)
303 avail_confs: Dict[Path, Dict[str, Any]] = {}
304 # load configs from oldest ancestor to current directory
305 for config in self.get_conf(**kwargs):
306 try:
307 avail_confs[config] = parse_rc(config, project=self.project)
308 except (PermissionError, FileNotFoundError, IsADirectoryError):
309 pass
311 if not flatten:
312 return avail_confs
314 super_config: Dict[str, Any] = {}
315 for config in reversed(list(avail_confs.values())):
316 super_config.update(config)
317 return {list(avail_confs.keys())[0]: super_config}
319 def write_config(self,
320 data: Dict[str, Any],
321 force: str = 'fail',
322 **kwargs) -> bool:
323 """
324 Write data to a safe configuration file.
326 Args:
327 data: serial data to save
328 force: force overwrite {'overwrite','update','fail'}
329 **kwargs:
330 - custom: custom location
331 - cname: name of config file
332 - ext: extension restriction filter(s)
333 - trace_pwd: when supplied, walk up to mountpoint or
334 project-root and inherit all locations that contain
335 __init__.py. Project-root is identified by discovery of
336 ``setup.py`` or ``setup.cfg``. Mountpoint is ``is_mount``
337 in unix or Drive in Windows. If ``True``, walk from ``$PWD``
338 - :py:meth:`xdgpspconf.utils.fs_perm` kwargs: passed on
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