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

20Locate standard data. 

21 

22Read: 

23 - standard xdg-base locations 

24 - current directory and ancestors 

25 - custom location 

26 

27""" 

28 

29import os 

30from pathlib import Path 

31from typing import Any, Dict, List, Union 

32 

33from xdgpspconf.base import FsDisc, XdgVar, fs_perm 

34from xdgpspconf.config_io import parse_rc, write_rc 

35 

36 

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

49 

50 

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

62 

63 

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

75 

76 

77class ConfigDisc(FsDisc): 

78 """ 

79 CONFig DISCoverer 

80 

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

88 

89 def locations(self, cname: str = None) -> Dict[str, List[Path]]: 

90 """ 

91 Shipped, root, user, improper locations 

92 

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 } 

105 

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

107 """ 

108 Walk up to nearest mountpoint or project root. 

109 

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 

115 

116 Args: 

117 child_dir: walk ancestry of `this` directory 

118 

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

127 

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 

133 

134 def user_xdg_loc(self, cname: str = 'config') -> List[Path]: 

135 """ 

136 Get XDG_<BASE>_HOME locations. 

137 

138 `specifications 

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

140 

141 Args: 

142 cname: name of config file 

143 

144 Returns: 

145 List of xdg-<base> Paths 

146 First directory is most dominant 

147 Raises: 

148 KeyError: bad variable name 

149 

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 

158 

159 def root_xdg_loc(self, cname: str = 'config') -> List[Path]: 

160 """ 

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

162 

163 `specifications 

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

165 

166 Args: 

167 cname: name of config file 

168 

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 

174 

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 

183 

184 def improper_loc(self, cname: str = 'config') -> List[Path]: 

185 """ 

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

187 

188 `specifications 

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

190 

191 Args: 

192 cname: name of config file 

193 

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 

199 

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 

208 

209 def get_conf(self, 

210 dom_start: bool = True, 

211 improper: bool = False, 

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

213 """ 

214 Get discovered configuration files. 

215 

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

229 

230 custom = kwargs.get('custom') 

231 if custom is not None: 

232 # don't check 

233 dom_order.append(Path(custom)) 

234 

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

241 

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) 

248 

249 if improper: 

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

251 

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

266 

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. 

272 

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 

280 

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 

291 

292 Returns: 

293 Paths: First path is most dominant 

294 

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 

309 

310 def read_config(self, **kwargs) -> Dict[Path, Dict[str, Any]]: 

311 """ 

312 Locate Paths to standard directories and parse config. 

313 

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 

323 

324 Returns: 

325 parsed configuration from each available file: 

326 first file is most dominant 

327 

328 Raises: 

329 BadConf- Bad configuration file format 

330 

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 

340 

341 # initialize with config 

342 return avail_confs 

343 

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. 

350 

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 

363 

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