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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

110 statements  

1#!/usr/bin/env python3 

2# -*- coding: utf-8; mode: python; -*- 

3# Copyright © 2021, 2022 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 

22 

23""" 

24 

25import os 

26import sys 

27from dataclasses import dataclass, field 

28from pathlib import Path 

29from typing import Any, Dict, List, Optional 

30 

31import yaml 

32 

33from xdgpspconf.utils import fs_perm, is_mount 

34 

35 

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) 

45 

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) 

54 

55 

56@dataclass 

57class PlfmXdg(): 

58 """ 

59 Platform Suited Variables 

60 """ 

61 win: XdgVar = XdgVar() 

62 posix: XdgVar = XdgVar() 

63 

64 

65def extract_xdg(): 

66 """ 

67 Read from 'strict'-standard locations. 

68 

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 

104 

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 

113 

114 

115XDG = extract_xdg() 

116 

117 

118class FsDisc(): 

119 """ 

120 File-System DISCovery functions 

121 

122 Args: 

123 project: str: project under consideration 

124 base: str: xdg base to fetch {CACHE,CONFIG,DATA,STATE} 

125 shipped: Path: ``namespace.__file__`` 

126 **permargs: all (arguments to :py:meth:`os.access`) are passed to 

127 :py:meth:`xdgpspconf.utils.fs_perm` 

128 

129 Attributes: 

130 project: str: project under consideration 

131 xdg: PlfmXdg: cross-platform xdg variables 

132 permargs: Dict[str, Any]: permission arguments 

133 

134 """ 

135 

136 def __init__(self, 

137 project: str, 

138 base: str = 'data', 

139 shipped: os.PathLike = None, 

140 **permargs): 

141 self.project = project 

142 self.permargs = permargs 

143 self.shipped = Path(shipped).resolve().parent if shipped else None 

144 self._xdg: PlfmXdg = XDG[base] 

145 

146 def locations(self) -> Dict[str, List[Path]]: 

147 """ 

148 Shipped, root, user, improper locations 

149 

150 Returns: 

151 named dictionary containing respective list of Paths 

152 """ 

153 return { 

154 'improper': self.improper_loc(), 

155 'user_loc': self.user_xdg_loc(), 

156 'root_loc': self.root_xdg_loc(), 

157 'shipped': [self.shipped] if self.shipped is not None else [] 

158 } 

159 

160 @property 

161 def xdg(self) -> PlfmXdg: 

162 return self._xdg 

163 

164 @xdg.setter 

165 def xdg(self, value: PlfmXdg): 

166 self._xdg = value 

167 

168 def __repr__(self) -> str: 

169 r_out = [] 

170 for attr in ('project', 'permargs', 'shipped', 'xdg'): 

171 r_out.append(f'{attr}: {getattr(self, attr)}') 

172 return '\n'.join(r_out) 

173 

174 def trace_ancestors(self, child_dir: Path) -> List[Path]: 

175 """ 

176 Walk up to nearest mountpoint or project root. 

177 

178 - collect all directories containing ``__init__.py`` 

179 (assumed to be source directories) 

180 - project root is directory that contains ``setup.cfg`` 

181 or ``setup.py`` 

182 - mountpoint is a unix mountpoint or windows drive root 

183 - I **AM** my 0th ancestor 

184 

185 Args: 

186 child_dir: walk ancestry of `this` directory 

187 

188 Returns: 

189 List of Paths to ancestors: 

190 First directory is most dominant 

191 """ 

192 pedigree: List[Path] = [] 

193 

194 # I **AM** my 0th ancestor 

195 while not is_mount(child_dir): 

196 if (child_dir / '__init__.py').is_file(): 

197 pedigree.append(child_dir) 

198 if any((child_dir / setup).is_file() 

199 for setup in ('setup.cfg', 'setup.py')): 

200 # project directory 

201 pedigree.append(child_dir) 

202 break 

203 child_dir = child_dir.parent 

204 return pedigree 

205 

206 def user_xdg_loc(self) -> List[Path]: 

207 """ 

208 Get XDG_<BASE>_HOME locations. 

209 

210 Returns: 

211 List of xdg-<base> Paths 

212 First directory is most dominant 

213 """ 

214 # environment 

215 if sys.platform.startswith('win'): # pragma: no cover 

216 # windows 

217 user_home = Path(os.environ['USERPROFILE']) 

218 os_xdg_loc = os.environ.get(self.xdg.win.var) 

219 os_default = self.xdg.win.default 

220 else: 

221 # assume POSIX 

222 user_home = Path(os.environ['HOME']) 

223 os_xdg_loc = os.environ.get(self.xdg.posix.var) 

224 os_default = self.xdg.posix.default 

225 if os_xdg_loc is None: 

226 xdg_base_loc = [Path(user_home / loc) for loc in os_default] 

227 else: 

228 xdg_base_loc = [Path(loc) for loc in os_xdg_loc.split(os.pathsep)] 

229 if not sys.platform.startswith('win'): 

230 # DONT: combine with previous condition, order is important 

231 # assume POSIX 

232 if self.xdg.posix.dirs and self.xdg.posix.dirs in os.environ: 

233 xdg_base_loc.extend((Path(unix_loc) for unix_loc in os.environ[ 

234 self.xdg.posix.dirs].split(os.pathsep))) 

235 return [loc / self.project for loc in xdg_base_loc] 

236 

237 def root_xdg_loc(self) -> List[Path]: 

238 """ 

239 Get ROOT's counterparts of XDG_<BASE>_HOME locations. 

240 

241 Returns: 

242 List of root-<base> Paths (parents to project's base) 

243 First directory is most dominant 

244 """ 

245 if sys.platform.startswith('win'): # pragma: no cover 

246 # windows 

247 os_root = self.xdg.win.root 

248 else: 

249 # assume POSIX 

250 os_root = self.xdg.posix.root 

251 if os_root: 

252 return [Path(root_base) / self.project for root_base in os_root] 

253 return [] 

254 

255 def improper_loc(self) -> List[Path]: 

256 """ 

257 Get discouraged improper data locations such as *~/.project*. 

258 

259 This is strongly discouraged. 

260 

261 Returns: 

262 List of xdg-<base> Paths (parents to project's base) 

263 First directory is most dominant 

264 """ 

265 # environment 

266 if sys.platform.startswith('win'): # pragma: no cover 

267 # windows 

268 user_home = Path(os.environ['USERPROFILE']) 

269 else: 

270 # assume POSIX 

271 user_home = Path(os.environ['HOME']) 

272 return [user_home / (hide + self.project) for hide in ('', '.')] 

273 

274 def get_loc(self, 

275 dom_start: bool = True, 

276 improper: bool = False, 

277 **kwargs) -> List[Path]: 

278 """ 

279 Get discovered locations. 

280 

281 Args: 

282 dom_start: when ``False``, end with most dominant 

283 improper: include improper locations such as *~/.project* 

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 - permargs passed on to :py:meth:`xdgpspconf.utils.fs_perm` 

292 """ 

293 dom_order: List[Path] = [] 

294 

295 custom = kwargs.get('custom') 

296 if custom is not None: 

297 # don't check 

298 dom_order.append(Path(custom)) 

299 

300 trace_pwd = kwargs.get('trace_pwd') 

301 if trace_pwd is True: 

302 trace_pwd = Path('.').resolve() 

303 if trace_pwd: 

304 inheritance = self.trace_ancestors(Path(trace_pwd)) 

305 dom_order.extend(inheritance) 

306 

307 locations = self.locations() 

308 if improper: 

309 dom_order.extend(locations['improper']) 

310 

311 for loc in ('user_loc', 'root_loc', 'shipped'): 

312 dom_order.extend(locations[loc]) 

313 

314 permargs = { 

315 key: val 

316 for key, val in kwargs.items() 

317 if key in ('mode', 'dir_fs', 'effective_ids', 'follow_symlinks') 

318 } 

319 permargs = {**self.permargs, **permargs} 

320 dom_order = list(filter(lambda x: fs_perm(x, **permargs), dom_order)) 

321 if dom_start: 

322 return dom_order 

323 return list(reversed(dom_order))