Coverage for /home/pradyumna/Languages/python/packages/xdgpspconf/xdgpspconf/base.py : 93%

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#
20"""
21Discovery base
23"""
25import os
26import sys
27from dataclasses import dataclass, field
28from pathlib import Path
29from typing import Any, Dict, List, Optional
31import yaml
33from xdgpspconf.utils import fs_perm, is_mount
36@dataclass
37class XdgVar():
38 """
39 xdg-defined variable
40 """
41 var: str = ''
42 dirs: Optional[str] = None
43 root: List[str] = field(default_factory=list)
44 default: List[str] = field(default_factory=list)
46 def update(self, master: Dict[str, Any]):
47 """
48 Update values
49 """
50 for key, val in master.items():
51 if key not in self.__dict__:
52 raise KeyError(f'{key} is not a recognised key')
53 setattr(self, key, val)
56@dataclass
57class PlfmXdg():
58 """
59 Platform Suited Variables
60 """
61 win: XdgVar = XdgVar()
62 posix: XdgVar = XdgVar()
65def extract_xdg():
66 """
67 Read from 'strict'-standard locations.
69 'Strict' locations:
70 Posix:
71 - ``<shipped_root>/xdg.yml``
72 - ``/etc/xdgpspconf/xdg.yml``
73 - ``/etc/xdg/xdgpspconf/xdg.yml``
74 - ``${XDG_CONFIG_HOME:-${HOME}/.config}/xdgpspconf/xdg.yml``
75 Windows:
76 - ``%APPDATA%\\xdgpspconf\\xdg.yml``
77 - ``%LOCALAPPDATA%\\xdgpspconf\\xdg.yml``
78 """
79 xdg_info = {}
80 pspxdg_locs = [Path(__file__).parent / 'xdg.yml']
81 config_tail = 'xdgpspconf/xdg.yml'
82 if sys.platform.startswith('win'):
83 pspxdg_locs.extend(
84 (Path(os.environ['APPDATA']) / config_tail,
85 Path(
86 os.environ.get(
87 'LOCALAPPDATA',
88 Path(os.environ['USERPROFILE']) / 'AppData/Local')) /
89 config_tail))
90 else:
91 pspxdg_locs.extend(
92 (Path(__file__).parent / 'xdg.yml', Path('/etc') / config_tail,
93 Path('/etc/xdg') / config_tail,
94 Path(
95 os.environ.get('XDG_CONFIG_HOME',
96 Path(os.environ['HOME']) / '.config')) /
97 config_tail))
98 for conf_xdg in pspxdg_locs:
99 try:
100 with open(conf_xdg) as conf:
101 xdg_info.update(yaml.safe_load(conf))
102 except (FileNotFoundError, IsADirectoryError, PermissionError):
103 pass
105 xdg: Dict[str, PlfmXdg] = {}
106 for var_type, var_info in xdg_info.items():
107 win_xdg = XdgVar()
108 posix_xdg = XdgVar()
109 win_xdg.update(var_info.get('win'))
110 posix_xdg.update(var_info.get('posix'))
111 xdg[var_type] = PlfmXdg(win=win_xdg, posix=posix_xdg)
112 return xdg
115XDG = extract_xdg()
118class FsDisc():
119 """
120 File-System DISCovery functions
122 Args:
123 project: str: project under consideration
124 base: str: xdg base to fetch {CACHE,CONFIG,DATA,STATE}
125 shipped: Path: namespace: ``__Path__``
126 **permargs: all (arguments to :py:meth:`os.access`) are passed to
127 :py:meth:`xdgpspconf.utils.fs_perm`
129 Attributes:
130 project: str: project under consideration
131 xdg: PlfmXdg: cross-platform xdg variables
132 permargs: Dict[str, Any]: permission arguments
134 """
135 def __init__(self,
136 project: str,
137 base: str = 'data',
138 shipped: os.PathLike = None,
139 **permargs):
140 self.project = project
141 self.permargs = permargs
142 self.shipped = [Path(shipped).resolve().parent] if shipped else []
143 self._xdg: PlfmXdg = XDG[base]
145 def locations(self) -> Dict[str, List[Path]]:
146 """
147 Shipped, root, user, improper locations
149 Returns:
150 named dictionary containing respective list of Paths
151 """
152 return {
153 'improper': self.improper_loc(),
154 'user_loc': self.user_xdg_loc(),
155 'root_loc': self.root_xdg_loc(),
156 'shipped': self.shipped
157 }
159 @property
160 def xdg(self) -> PlfmXdg:
161 return self._xdg
163 @xdg.setter
164 def xdg(self, value: PlfmXdg):
165 self._xdg = value
167 def __repr__(self) -> str:
168 r_out = []
169 for attr in ('project', 'permargs', 'shipped', 'xdg'):
170 r_out.append(f'{attr}: {getattr(self, attr)}')
171 return '\n'.join(r_out)
173 def trace_ancestors(self, child_dir: Path) -> List[Path]:
174 """
175 Walk up to nearest mountpoint or project root.
177 - collect all directories containing ``__init__.py``
178 (assumed to be source directories)
179 - project root is directory that contains ``setup.cfg``
180 or ``setup.py``
181 - mountpoint is a unix mountpoint or windows drive root
182 - I **AM** my 0th ancestor
184 Args:
185 child_dir: walk ancestry of `this` directory
187 Returns:
188 List of Paths to ancestors:
189 First directory is most dominant
190 """
191 pedigree: List[Path] = []
193 # I **AM** my 0th ancestor
194 while not is_mount(child_dir):
195 if (child_dir / '__init__.py').is_file():
196 pedigree.append(child_dir)
197 if any((child_dir / setup).is_file()
198 for setup in ('setup.cfg', 'setup.py')):
199 # project directory
200 pedigree.append(child_dir)
201 break
202 child_dir = child_dir.parent
203 return pedigree
205 def user_xdg_loc(self) -> List[Path]:
206 """
207 Get XDG_<BASE>_HOME locations.
209 `specifications
210 <https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html>`__
212 Returns:
213 List of xdg-<base> Paths
214 First directory is most dominant
215 """
216 # environment
217 if sys.platform.startswith('win'): # pragma: no cover
218 # windows
219 user_home = Path(os.environ['USERPROFILE'])
220 os_xdg_loc = os.environ.get(self.xdg.win.var)
221 os_default = self.xdg.win.default
222 else:
223 # assume POSIX
224 user_home = Path(os.environ['HOME'])
225 os_xdg_loc = os.environ.get(self.xdg.posix.var)
226 os_default = self.xdg.posix.default
227 if os_xdg_loc is None:
228 xdg_base_loc = [Path(user_home / loc) for loc in os_default]
229 else:
230 xdg_base_loc = [Path(loc) for loc in os_xdg_loc.split(os.pathsep)]
231 if not sys.platform.startswith('win'):
232 # DONT: combine with previous condition, order is important
233 # assume POSIX
234 if self.xdg.posix.dirs and self.xdg.posix.dirs in os.environ:
235 xdg_base_loc.extend((Path(unix_loc) for unix_loc in os.environ[
236 self.xdg.posix.dirs].split(os.pathsep)))
237 return [loc / self.project for loc in xdg_base_loc]
239 def root_xdg_loc(self) -> List[Path]:
240 """
241 Get ROOT's counterparts of XDG_<BASE>_HOME locations.
243 `specifications
244 <https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html>`__
246 Returns:
247 List of root-<base> Paths (parents to project's base)
248 First directory is most dominant
249 """
250 if sys.platform.startswith('win'): # pragma: no cover
251 # windows
252 os_root = self.xdg.win.root
253 else:
254 # assume POSIX
255 os_root = self.xdg.posix.root
256 if os_root:
257 return [Path(root_base) / self.project for root_base in os_root]
258 return []
260 def improper_loc(self) -> List[Path]:
261 """
262 Get discouraged improper data locations such as *~/.project*.
264 This is strongly discouraged.
266 Returns:
267 List of xdg-<base> Paths (parents to project's base)
268 First directory is most dominant
269 """
270 # environment
271 if sys.platform.startswith('win'): # pragma: no cover
272 # windows
273 user_home = Path(os.environ['USERPROFILE'])
274 else:
275 # assume POSIX
276 user_home = Path(os.environ['HOME'])
277 return [user_home / (hide + self.project) for hide in ('', '.')]
279 def get_loc(self,
280 dom_start: bool = True,
281 improper: bool = False,
282 **kwargs) -> List[Path]:
283 """
284 Get discovered locations.
286 Args:
287 dom_start: when ``False``, end with most dominant
288 improper: include improper locations such as *~/.project*
289 **kwargs:
290 - custom: custom location
291 - trace_pwd: when supplied, walk up to mountpoint or
292 project-root and inherit all locations that contain
293 ``__init__.py``. Project-root is identified by discovery of
294 ``setup.py`` or ``setup.cfg``. Mountpoint is ``is_mount``
295 in unix or Drive in Windows. If ``True``, walk from ``$PWD``
296 - :py:meth:`xdgpspconf.utils.fs_perm` kwargs: passed on
297 """
298 dom_order: List[Path] = []
300 custom = kwargs.get('custom')
301 if custom is not None:
302 # don't check
303 dom_order.append(Path(custom))
305 trace_pwd = kwargs.get('trace_pwd')
306 if trace_pwd is True:
307 trace_pwd = Path('.').resolve()
308 if trace_pwd:
309 inheritance = self.trace_ancestors(Path(trace_pwd))
310 dom_order.extend(inheritance)
312 if improper:
313 dom_order.extend(self.locations()['improper'])
315 dom_order.extend(self.locations()['user_loc'])
316 dom_order.extend(self.locations()['root_loc'])
317 dom_order.extend(self.locations()['shipped'])
318 permargs = {
319 key: val
320 for key, val in kwargs.items()
321 if key in ('mode', 'dir_fs', 'effective_ids', 'follow_symlinks')
322 }
323 permargs = {**self.permargs, **permargs}
324 dom_order = list(filter(lambda x: fs_perm(x, **permargs), dom_order))
325 if dom_start:
326 return dom_order
327 return list(reversed(dom_order))