Coverage for /home/pradyumna/Languages/python/packages/xdgpspconf/xdgpspconf/common.py : 89%

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"""
20Locate standard data.
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, XdgVar, fs_perm
34from xdgpspconf.config_io import parse_rc, write_rc
37class DataDisc(FsDisc):
38 """
39 DATA DISCoverer
40 """
41 def __init__(self, project: str, shipped: os.PathLike = None, **permargs):
42 super().__init__(project=project,
43 base='DATA',
44 shipped=shipped,
45 **permargs)
46 self.xdg = XdgVar(['APPDATA'], 'LOCALAPPDATA', ['AppData/Local'],
47 ['/local/share', '/usr/local/share', '/usr/share'],
48 'XDG_DATA_HOME', 'XDG_DATA_DIRS', ['.local/share'])
51class CacheDisc(FsDisc):
52 """
53 CACHE DISCoverer
54 """
55 def __init__(self, project: str, shipped: os.PathLike = None, **permargs):
56 super().__init__(project=project,
57 base='CACHE',
58 shipped=shipped,
59 **permargs)
60 self.xdg = XdgVar(['TEMP'], 'TEMP', ['AppData/Local/Temp'], None,
61 'XDG_CACHE_HOME', None, ['.cache'])
64class StateDisc(FsDisc):
65 """
66 STATE DISCoverer
67 """
68 def __init__(self, project: str, shipped: os.PathLike = None, **permargs):
69 super().__init__(project=project,
70 base='STATE',
71 shipped=shipped,
72 **permargs)
73 self.xdg = XdgVar(['APPDATA'], 'LOCALAPPDATA', ['AppData/Local'], None,
74 'XDG_STATE_HOME', 'XDG_STATE_DIRS', ['.local/state'])
77class ConfigDisc(FsDisc):
78 """
79 CONFig DISCoverer
81 Each location is config file, NOT directory as with FsDisc
82 """
83 def __init__(self, project: str, shipped: os.PathLike = None, **permargs):
84 super().__init__(project, base='CONFIG', shipped=shipped, **permargs)
85 self.xdg = XdgVar(['APPDATA'], 'LOCALAPPDATA', ['AppData/Local'],
86 ['/etc', '/etc/xdg'], 'XDG_CONFIG_HOME',
87 'XDG_CONFIG_DIRS', ['.config'])
89 def locations(self, cname: str = None) -> Dict[str, List[Path]]:
90 """
91 Shipped, root, user, improper locations
93 Args:
94 cname: name of configuration file
95 Returns:
96 named dictionary containing respective list of Paths
97 """
98 cname = cname or 'config'
99 return {
100 'improper': self.improper_loc(cname),
101 'user_loc': self.user_xdg_loc(cname),
102 'root_loc': self.root_xdg_loc(cname),
103 'shipped': self.shipped
104 }
106 def trace_ancestors(self, child_dir: Path) -> List[Path]:
107 """
108 Walk up to nearest mountpoint or project root.
110 - collect all directories containing __init__.py
111 (assumed to be source directories)
112 - project root is directory that contains ``setup.cfg`` or ``setup.py``
113 - mountpoint is a unix mountpoint or windows drive root
114 - I **AM** my 0th ancestor
116 Args:
117 child_dir: walk ancestry of `this` directory
119 Returns:
120 List of Paths to ancestor configs:
121 First directory is most dominant
122 """
123 config = []
124 pedigree = super().trace_ancestors(child_dir)
125 config.extend(
126 (config_dir / f'.{self.project}rc' for config_dir in pedigree))
128 if pedigree:
129 for setup in ('pyproject.toml', 'setup.cfg'):
130 if (pedigree[-1] / setup).is_file():
131 config.append(pedigree[-1] / setup)
132 return config
134 def user_xdg_loc(self, cname: str = 'config') -> List[Path]:
135 """
136 Get XDG_<BASE>_HOME locations.
138 `specifications
139 <https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html>`__
141 Args:
142 cname: name of config file
144 Returns:
145 List of xdg-<base> Paths
146 First directory is most dominant
147 Raises:
148 KeyError: bad variable name
150 """
151 user_base_loc = super().user_xdg_loc()
152 config = []
153 for ext in '.yml', '.yaml', '.toml', '.conf':
154 for loc in user_base_loc:
155 config.append((loc / cname).with_suffix(ext))
156 config.append(loc.with_suffix(ext))
157 return config
159 def root_xdg_loc(self, cname: str = 'config') -> List[Path]:
160 """
161 Get ROOT's counterparts of XDG_<BASE>_HOME locations.
163 `specifications
164 <https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html>`__
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 root_base_loc = super().root_xdg_loc()
177 config = []
178 for ext in '.yml', '.yaml', '.toml', '.conf':
179 for loc in root_base_loc:
180 config.append((loc / cname).with_suffix(ext))
181 config.append(loc.with_suffix(ext))
182 return config
184 def improper_loc(self, cname: str = 'config') -> List[Path]:
185 """
186 Get ROOT's counterparts of XDG_<BASE>_HOME locations.
188 `specifications
189 <https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html>`__
191 Args:
192 cname: name of config file
194 Returns:
195 List of root-<base> Paths (parents to project's base)
196 First directory is most dominant
197 Raises:
198 KeyError: bad variable name
200 """
201 improper_base_loc = super().improper_loc()
202 config = []
203 for ext in '.yml', '.yaml', '.toml', '.conf':
204 for loc in improper_base_loc:
205 config.append((loc / cname).with_suffix(ext))
206 config.append(loc.with_suffix(ext))
207 return config
209 def get_conf(self,
210 dom_start: bool = True,
211 improper: bool = False,
212 **kwargs) -> List[Path]:
213 """
214 Get discovered configuration files.
216 Args:
217 dom_start: when ``False``, end with most dominant
218 improper: include improper locations such as ~/.project
219 **kwargs: passed to filter_perm
220 custom: custom location
221 trace_pwd: when supplied, walk up to mountpoint or \
222project-root and inherit all locations that contain __init__.py.
223Project-root is identified by discovery of ``setup.py`` or ``setup.cfg``. \
224Mountpoint is ``is_mount`` in unix or Drive in Windows. If ``True``, use $PWD
225 cname: name of config file
226 `fs_perm` kwargs: passed accordingly
227 """
228 dom_order: List[Path] = []
230 custom = kwargs.get('custom')
231 if custom is not None:
232 # don't check
233 dom_order.append(Path(custom))
235 rc_val = os.environ.get(self.project.upper() + 'RC')
236 if rc_val is not None:
237 if not Path(rc_val).is_file():
238 raise FileNotFoundError(
239 f'RC configuration file: {rc_val} not found')
240 dom_order.append(Path(rc_val))
242 trace_pwd = kwargs.get('trace_pwd')
243 if trace_pwd is True:
244 trace_pwd = Path('.').resolve()
245 if trace_pwd:
246 inheritance = self.trace_ancestors(Path(trace_pwd))
247 dom_order.extend(inheritance)
249 if improper:
250 dom_order.extend(self.locations()['improper'])
252 dom_order.extend(self.locations(kwargs.get('cname'))['user_loc'])
253 dom_order.extend(self.locations(kwargs.get('cname'))['root_loc'])
254 dom_order.extend(self.locations(kwargs.get('cname'))['shipped'])
255 permargs = {
256 key: val
257 for key, val in kwargs.items()
258 if key in ('mode', 'dir_fs', 'effective_ids', 'follow_symlinks')
259 }
260 permargs = {**self.permargs, **permargs}
261 dom_order = list(filter(lambda x: fs_perm(x, **permargs), dom_order))
262 print(dom_order)
263 if dom_start:
264 return dom_order
265 return list(reversed(dom_order))
267 def safe_config(self,
268 ext: Union[str, List[str]] = None,
269 **kwargs) -> List[Path]:
270 """
271 Locate safe writable paths of configuration files.
273 - Doesn't care about accessibility or existance of locations.
274 - User must catch:
275 - ``PermissionError``
276 - ``IsADirectoryError``
277 - ``FileNotFoundError``
278 - Improper locations of the form ~/.project are deliberately dropped
279 - Recommendation: Try saving your configuration in in reversed order
281 Args:
282 ext: extension filter(s)
283 **kwargs: passed to filter_perm
284 custom: custom location
285 trace_pwd: when supplied, walk up to mountpoint or \
286project-root and inherit all locations that contain __init__.py.
287Project-root is identified by discovery of ``setup.py`` or ``setup.cfg``. \
288Mountpoint is ``is_mount`` in unix or Drive in Windows. If ``True``, use $PWD
289 cname: name of config file
290 `fs_perm` kwargs: passed accordingly
292 Returns:
293 Paths: First path is most dominant
295 """
296 kwargs['mode'] = kwargs.get('mode', 2)
297 if isinstance(ext, str):
298 ext = [ext]
299 safe_paths: List[Path] = []
300 for loc in self.get_conf(**kwargs):
301 if any(private in str(loc)
302 for private in ('site-packages', 'venv', '/etc', 'setup',
303 'pyproject')):
304 continue
305 if ext and loc.suffix and loc.suffix not in list(ext):
306 continue
307 safe_paths.append(loc)
308 return safe_paths
310 def read_config(self, **kwargs) -> Dict[Path, Dict[str, Any]]:
311 """
312 Locate Paths to standard directories and parse config.
314 Args:
315 **kwargs: passed to filter_perm
316 custom: custom location
317 trace_pwd: when supplied, walk up to mountpoint or \
318project-root and inherit all locations that contain __init__.py.
319Project-root is identified by discovery of ``setup.py`` or ``setup.cfg``. \
320Mountpoint is ``is_mount`` in unix or Drive in Windows. If ``True``, use $PWD
321 cname: name of config file
322 `fs_perm` kwargs: passed accordingly
324 Returns:
325 parsed configuration from each available file:
326 first file is most dominant
328 Raises:
329 BadConf- Bad configuration file format
331 """
332 kwargs['mode'] = kwargs.get('mode', 4)
333 avail_confs: Dict[Path, Dict[str, Any]] = {}
334 # load configs from oldest ancestor to current directory
335 for config in self.get_conf(**kwargs):
336 try:
337 avail_confs[config] = parse_rc(config)
338 except (PermissionError, FileNotFoundError, IsADirectoryError):
339 pass
341 # initialize with config
342 return avail_confs
344 def write_config(self,
345 data: Dict[str, Any],
346 force: str = 'fail',
347 **kwargs) -> bool:
348 """
349 Write data to a safe configuration file.
351 Args:
352 data: serial data to save
353 force: force overwrite {'overwrite', 'update', 'fail'}
354 **kwargs: passed to filter_perm
355 custom: custom location
356 cname: name of config file
357 ext: extension restriction filter(s)
358 trace_pwd: when supplied, walk up to mountpoint or \
359project-root and inherit all locations that contain __init__.py.
360Project-root is identified by discovery of ``setup.py`` or ``setup.cfg``. \
361Mountpoint is ``is_mount`` in unix or Drive in Windows. If ``True``, use $PWD
362 `fs_perm` kwargs: passed accordingly
364 Returns: success
365 """
366 config_l = list(
367 reversed(self.safe_config(ext=kwargs.get('ext'), **kwargs)))
368 for config in config_l:
369 try:
370 return write_rc(data, config, force=force)
371 except (PermissionError, IsADirectoryError, FileNotFoundError):
372 continue
373 return False