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

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

109 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, Union 

42 

43from xdgpspconf.base import FsDisc 

44from xdgpspconf.config_io import CONF_EXT, parse_rc, write_rc 

45from xdgpspconf.utils import fs_perm 

46 

47 

48class ConfDisc(FsDisc): 

49 """ 

50 CONF DISCoverer 

51 

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

53 """ 

54 

55 def __init__(self, project: str, shipped: os.PathLike = None, **permargs): 

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

57 

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

59 """ 

60 Shipped, root, user, improper locations 

61 

62 Args: 

63 cname: name of configuration file 

64 

65 Returns: 

66 named dictionary containing respective list of Paths 

67 """ 

68 cname = cname or 'config' 

69 return { 

70 'improper': 

71 self.improper_loc(cname), 

72 'user_loc': 

73 self.user_xdg_loc(cname), 

74 'root_loc': 

75 self.root_xdg_loc(cname), 

76 'shipped': 

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

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

79 } 

80 

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

82 """ 

83 Walk up to nearest mountpoint or project root. 

84 

85 - collect all directories containing __init__.py \ 

86 (assumed to be source directories) 

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

88 or ``setup.py`` 

89 - mountpoint is a unix mountpoint or windows drive root 

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

91 

92 Args: 

93 child_dir: walk ancestry of `this` directory 

94 

95 Returns: 

96 List of Paths to ancestor configs: 

97 First directory is most dominant 

98 """ 

99 config = [] 

100 pedigree = super().trace_ancestors(child_dir) 

101 if child_dir not in pedigree: 

102 pedigree = [child_dir, *pedigree] 

103 config.extend( 

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

105 

106 if pedigree: 

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

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

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

110 return config 

111 

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

113 """ 

114 Get XDG_<BASE>_HOME locations. 

115 

116 Args: 

117 cname: name of config file 

118 

119 Returns: 

120 List of xdg-<base> Paths 

121 First directory is most dominant 

122 Raises: 

123 KeyError: bad variable name 

124 

125 """ 

126 user_base_loc = super().user_xdg_loc() 

127 config = [] 

128 for ext in CONF_EXT: 

129 for loc in user_base_loc: 

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

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

132 return config 

133 

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

135 """ 

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

137 

138 Args: 

139 cname: name of config file 

140 

141 Returns: 

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

143 First directory is most dominant 

144 Raises: 

145 KeyError: bad variable name 

146 

147 """ 

148 root_base_loc = super().root_xdg_loc() 

149 config = [] 

150 for ext in CONF_EXT: 

151 for loc in root_base_loc: 

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

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

154 return config 

155 

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

157 """ 

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

159 

160 Args: 

161 cname: name of config file 

162 

163 Returns: 

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

165 First directory is most dominant 

166 Raises: 

167 KeyError: bad variable name 

168 

169 """ 

170 improper_base_loc = super().improper_loc() 

171 config = [] 

172 for ext in CONF_EXT: 

173 for loc in improper_base_loc: 

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

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

176 return config 

177 

178 def get_conf(self, 

179 dom_start: bool = True, 

180 improper: bool = False, 

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

182 """ 

183 Get discovered configuration files. 

184 

185 Args: 

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

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

188 **kwargs: 

189 - custom: custom location 

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

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

192 project-root and inherit all locations that contain 

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

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

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

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

197 

198 Returns: 

199 List of configuration paths 

200 """ 

201 dom_order: List[Path] = [] 

202 

203 custom = kwargs.get('custom') 

204 if custom is not None: 

205 # don't check 

206 dom_order.append(Path(custom)) 

207 

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

209 if rc_val is not None: 

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

211 raise FileNotFoundError( 

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

213 dom_order.append(Path(rc_val)) 

214 

215 trace_pwd = kwargs.get('trace_pwd') 

216 if trace_pwd is True: 

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

218 if trace_pwd: 

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

220 dom_order.extend(inheritance) 

221 

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

223 if improper: 

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

225 

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

227 dom_order.extend(locations[loc]) 

228 

229 permargs = { 

230 key: val 

231 for key, val in kwargs.items() 

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

233 } 

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

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

236 if dom_start: 

237 return dom_order 

238 return list(reversed(dom_order)) 

239 

240 def safe_config(self, 

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

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

243 """ 

244 Locate safe writable paths of configuration files. 

245 

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

247 - User must catch: 

248 - ``PermissionError`` 

249 - ``IsADirectoryError`` 

250 - ``FileNotFoundError`` 

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

252 - Recommendation: Try saving your configuration in in reversed order 

253 

254 Args: 

255 ext: extension filter(s) 

256 **kwargs: 

257 - custom: custom location 

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

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

260 project-root and inherit all locations that contain 

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

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

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

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

265 

266 

267 Returns: 

268 Paths: First path is most dominant 

269 

270 """ 

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

272 if isinstance(ext, str): 

273 ext = [ext] 

274 safe_paths: List[Path] = [] 

275 for loc in self.get_conf(**kwargs): 

276 if any(private in str(loc) 

277 for private in ('site-packages', 'venv', '/etc', 'setup', 

278 'pyproject')): 

279 continue 

280 if ext and loc.suffix and loc.suffix not in list(ext): 

281 continue 

282 safe_paths.append(loc) 

283 return safe_paths 

284 

285 def read_config(self, 

286 flatten: bool = False, 

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

288 """ 

289 Locate Paths to standard directories and parse config. 

290 

291 Args: 

292 flatten: superimpose configurations to return the final outcome 

293 **kwargs: 

294 - custom: custom location 

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

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

297 project-root and inherit all locations that contain 

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

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

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

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

302 

303 Returns: 

304 parsed configuration from each available file: 

305 first file is most dominant 

306 

307 Raises: 

308 BadConf- Bad configuration file format 

309 

310 """ 

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

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

313 # load configs from oldest ancestor to current directory 

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

315 try: 

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

317 except (PermissionError, FileNotFoundError, IsADirectoryError): 

318 pass 

319 

320 if not flatten: 

321 return avail_confs 

322 

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

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

325 super_config.update(config) 

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

327 

328 def write_config(self, 

329 data: Dict[str, Any], 

330 force: str = 'fail', 

331 **kwargs) -> bool: 

332 """ 

333 Write data to a safe configuration file. 

334 

335 Args: 

336 data: serial data to save 

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

338 **kwargs 

339 

340 Returns: success 

341 """ 

342 config_l = list( 

343 reversed(self.safe_config(ext=kwargs.get('ext'), **kwargs))) 

344 for config in config_l: 

345 try: 

346 return write_rc(data, config, force=force) 

347 except (PermissionError, IsADirectoryError, FileNotFoundError): 

348 continue 

349 return False