Hide keyboard shortcuts

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 

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: ``__Path__`` 

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 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] 

144 

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

146 """ 

147 Shipped, root, user, improper locations 

148 

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 } 

158 

159 @property 

160 def xdg(self) -> PlfmXdg: 

161 return self._xdg 

162 

163 @xdg.setter 

164 def xdg(self, value: PlfmXdg): 

165 self._xdg = value 

166 

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) 

172 

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

174 """ 

175 Walk up to nearest mountpoint or project root. 

176 

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 

183 

184 Args: 

185 child_dir: walk ancestry of `this` directory 

186 

187 Returns: 

188 List of Paths to ancestors: 

189 First directory is most dominant 

190 """ 

191 pedigree: List[Path] = [] 

192 

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 

204 

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

206 """ 

207 Get XDG_<BASE>_HOME locations. 

208 

209 `specifications 

210 <https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html>`__ 

211 

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] 

238 

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

240 """ 

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

242 

243 `specifications 

244 <https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html>`__ 

245 

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 [] 

259 

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

261 """ 

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

263 

264 This is strongly discouraged. 

265 

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 ('', '.')] 

278 

279 def get_loc(self, 

280 dom_start: bool = True, 

281 improper: bool = False, 

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

283 """ 

284 Get discovered locations. 

285 

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] = [] 

299 

300 custom = kwargs.get('custom') 

301 if custom is not None: 

302 # don't check 

303 dom_order.append(Path(custom)) 

304 

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) 

311 

312 if improper: 

313 dom_order.extend(self.locations()['improper']) 

314 

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))