Coverage for /home/pradyumna/Languages/python/packages/xdgpspconf/xdgpspconf/config.py: 100%

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

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

20Special case of configuration, where base object is a file 

21 

22Read: 

23 - standard xdg-base locations 

24 - current directory and ancestors 

25 - custom location 

26 

27Following kwargs are defined for some functions as indicated: 

28 - custom: custom location 

29 - cname: name of config file 

30 - ext: extension restriction filter(s) 

31 - trace_pwd: when supplied, walk up to mountpoint or project-root and 

32 inherit all locations that contain __init__.py. Project-root is 

33 identified by discovery of ``setup.py`` or ``setup.cfg``. Mountpoint is 

34 ``is_mount`` in unix or Drive in Windows. If ``True``, walk from ``$PWD`` 

35 - kwargs of :py:meth:`xdgpspconf.utils.fs_perm`: passed on 

36 

37""" 

38 

39import os 

40from pathlib import Path 

41from typing import Any, Dict, List, Optional, Union 

42 

43from xdgpspconf.base import FsDisc 

44from xdgpspconf.config_io import parse_rc, write_rc 

45from xdgpspconf.utils import fs_perm 

46 

47CONF_EXT = '.yml', '.yaml', '.toml', '.conf', '.ini' 

48"""Extensions that are supported (parsed) by this module""" 

49 

50 

51class ConfDisc(FsDisc): 

52 """ 

53 CONF DISCoverer 

54 

55 Each location is config file, NOT directory as with FsDisc 

56 """ 

57 

58 def __init__(self, 

59 project: str, 

60 shipped: Union[Path, str] = None, 

61 **permargs): 

62 super().__init__(project, base='config', shipped=shipped, **permargs) 

63 

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

65 """ 

66 Shipped, root, user, improper locations 

67 

68 Args: 

69 cname: name of configuration file 

70 

71 Returns: 

72 named dictionary containing respective list of Paths 

73 """ 

74 cname = cname or 'config' 

75 return { 

76 'improper': 

77 self.improper_loc(cname), 

78 'user_loc': 

79 self.user_xdg_loc(cname), 

80 'root_loc': 

81 self.root_xdg_loc(cname), 

82 'shipped': 

83 [(self.shipped / cname).with_suffix(ext) 

84 for ext in CONF_EXT] if self.shipped is not None else [] 

85 } 

86 

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

88 """ 

89 Walk up to nearest mountpoint or project root. 

90 

91 - collect all directories containing __init__.py \ 

92 (assumed to be source directories) 

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

94 or ``setup.py`` 

95 - mountpoint is a unix mountpoint or windows drive root 

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

97 

98 Args: 

99 child_dir: walk ancestry of `this` directory 

100 

101 Returns: 

102 List of Paths to ancestor configs: 

103 First directory is most dominant 

104 """ 

105 config = [] 

106 pedigree = super().trace_ancestors(child_dir) 

107 if child_dir not in pedigree: # pragma: no cover 

108 pedigree = [child_dir, *pedigree] 

109 config.extend( 

110 (config_dir / f'.{self.project}rc' for config_dir in pedigree)) 

111 

112 if pedigree: 

113 for setup in ('pyproject.toml', 'setup.cfg'): 

114 if (pedigree[-1] / setup).is_file(): 

115 config.append(pedigree[-1] / setup) 

116 return config 

117 

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

119 """ 

120 Get XDG_<BASE>_HOME locations. 

121 

122 Args: 

123 cname: name of config file 

124 

125 Returns: 

126 List of xdg-<base> Paths 

127 First directory is most dominant 

128 Raises: 

129 KeyError: bad variable name 

130 

131 """ 

132 user_base_loc = super().user_xdg_loc() 

133 config = [] 

134 for ext in CONF_EXT: 

135 for loc in user_base_loc: 

136 config.append((loc / cname).with_suffix(ext)) 

137 config.append(loc.with_suffix(ext)) 

138 return config 

139 

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

141 """ 

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

143 

144 Args: 

145 cname: name of config file 

146 

147 Returns: 

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

149 First directory is most dominant 

150 Raises: 

151 KeyError: bad variable name 

152 

153 """ 

154 root_base_loc = super().root_xdg_loc() 

155 config = [] 

156 for ext in CONF_EXT: 

157 for loc in root_base_loc: 

158 config.append((loc / cname).with_suffix(ext)) 

159 config.append(loc.with_suffix(ext)) 

160 return config 

161 

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

163 """ 

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

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 improper_base_loc = super().improper_loc() 

177 config = [] 

178 for ext in CONF_EXT: 

179 for loc in improper_base_loc: 

180 config.append((loc / cname).with_suffix(ext)) 

181 config.append(loc.with_suffix(ext)) 

182 return config 

183 

184 def get_conf(self, 

185 dom_start: bool = True, 

186 improper: bool = False, 

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

188 """ 

189 Get discovered configuration files. 

190 

191 Args: 

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

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

194 **kwargs: 

195 - custom: custom location 

196 - cname: name of configuration file. Default: 'config' 

197 - trace_pwd: when supplied, walk up to mountpoint or 

198 project-root and inherit all locations that contain 

199 ``__init__.py``. Project-root is identified by discovery of 

200 ``setup.py`` or ``setup.cfg``. Mountpoint is ``is_mount`` 

201 in unix or Drive in Windows. If ``True``, walk from ``$PWD`` 

202 - permargs passed on to :py:meth:`xdgpspconf.utils.fs_perm` 

203 

204 Returns: 

205 List of configuration paths 

206 """ 

207 # NOTE: order of following statements IS important 

208 dom_order: List[Path] = [] 

209 

210 custom = kwargs.get('custom') 

211 if custom is not None: 

212 # assume existence and proceed 

213 dom_order.append(Path(custom)) 

214 

215 rc_val = os.environ.get(self.project.upper() + 'RC') 

216 if rc_val is not None: # pragma: no cover 

217 if not Path(rc_val).is_file(): 

218 raise FileNotFoundError( 

219 f'RC configuration file: {rc_val} not found') 

220 dom_order.append(Path(rc_val)) 

221 

222 trace_pwd = kwargs.get('trace_pwd') 

223 if trace_pwd is True: 

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

225 if trace_pwd: 

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

227 dom_order.extend(inheritance) 

228 

229 locations = self.locations(kwargs.get('cname')) 

230 if improper: 

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

232 

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

234 dom_order.extend(locations[loc]) 

235 

236 permargs = { 

237 key: val 

238 for key, val in kwargs.items() 

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

240 } 

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

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

243 if dom_start: 

244 return dom_order 

245 return list(reversed(dom_order)) 

246 

247 def safe_config(self, 

248 ext: Union[str, List[str]] = None, 

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

250 """ 

251 Locate safe writeable paths of configuration files. 

252 

253 - Doesn't care about accessibility or existance of locations. 

254 - User must catch: 

255 - ``PermissionError`` 

256 - ``IsADirectoryError`` 

257 - ``FileNotFoundError`` 

258 - Improper locations (*~/.project*) are deliberately dropped 

259 - Recommendation: use dom_start=``False`` 

260 

261 Args: 

262 ext: extension filter(s) 

263 **kwargs: 

264 - custom: custom location 

265 - cname: name of configuration file. Default: 'config' 

266 - trace_pwd: when supplied, walk up to mountpoint or 

267 project-root and inherit all locations that contain 

268 ``__init__.py``. Project-root is identified by discovery of 

269 ``setup.py`` or ``setup.cfg``. Mountpoint is ``is_mount`` 

270 in unix or Drive in Windows. If ``True``, walk from ``$PWD`` 

271 - dom_start: when ``False``, end with most dominant 

272 - permargs passed on to :py:meth:`xdgpspconf.utils.fs_perm` 

273 

274 

275 Returns: 

276 Safe configuration locations (existing and prospective) 

277 

278 """ 

279 kwargs['mode'] = kwargs.get('mode', 2) 

280 

281 # filter private locations 

282 private_locs = ['site-packages', 'venv', '/etc', 'setup', 'pyproject'] 

283 if self.shipped is not None: 

284 private_locs.append(str(self.shipped)) 

285 safe_paths = filter( 

286 lambda x: not any(private in str(x) for private in private_locs), 

287 self.get_conf(**kwargs)) 

288 if ext is None: 

289 return list(safe_paths) 

290 

291 # filter extensions 

292 if isinstance(ext, str): 

293 ext = [ext] 

294 return list(filter(lambda x: x.suffix in ext, safe_paths)) 

295 

296 def read_config(self, 

297 flatten: bool = False, 

298 **kwargs) -> Dict[Path, Dict[str, Any]]: 

299 """ 

300 Locate Paths to standard directories and parse config. 

301 

302 Args: 

303 flatten: superimpose configurations to return the final outcome 

304 **kwargs: 

305 - custom: custom location 

306 - cname: name of configuration file. Default: 'config' 

307 - trace_pwd: when supplied, walk up to mountpoint or 

308 project-root and inherit all locations that contain 

309 ``__init__.py``. Project-root is identified by discovery of 

310 ``setup.py`` or ``setup.cfg``. Mountpoint is ``is_mount`` 

311 in unix or Drive in Windows. If ``True``, walk from ``$PWD`` 

312 - dom_start: when ``False``, end with most dominant 

313 - permargs passed on to :py:meth:`xdgpspconf.utils.fs_perm` 

314 

315 Returns: 

316 parsed configuration from each available file 

317 

318 Raises: 

319 BadConf- Bad configuration file format 

320 

321 """ 

322 kwargs['mode'] = kwargs.get('mode', 4) 

323 avail_confs: Dict[Path, Dict[str, Any]] = {} 

324 # load configs from oldest ancestor to current directory 

325 for config in self.get_conf(**kwargs): 

326 try: 

327 avail_confs[config] = parse_rc(config, project=self.project) 

328 except (PermissionError, FileNotFoundError, IsADirectoryError): 

329 pass 

330 

331 if not flatten: 

332 return avail_confs 

333 

334 super_config: Dict[str, Any] = {} 

335 for config in reversed(list(avail_confs.values())): 

336 super_config.update(config) 

337 return {Path('.').resolve(): super_config} 

338 

339 def write_config(self, 

340 data: Dict[str, Any], 

341 force: str = 'fail', 

342 **kwargs) -> Optional[Path]: 

343 """ 

344 Write data to the most global safe configuration file. 

345 

346 Args: 

347 data: serial data to save 

348 force: force overwrite {'overwrite','update','fail'} 

349 **kwargs 

350 

351 Returns: Path to which, configuration was written 

352 """ 

353 kwargs['dom_start'] = kwargs.get('dom_start', False) 

354 config_l = self.safe_config(**kwargs) 

355 for config in config_l: 

356 try: 

357 if write_rc(data, config, force=force): 

358 return config 

359 except (PermissionError, IsADirectoryError, FileNotFoundError): 

360 continue 

361 return None # pragma: no cover