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

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 

27""" 

28 

29import os 

30from pathlib import Path 

31from typing import Any, Dict, List, Union 

32 

33from xdgpspconf.base import FsDisc 

34from xdgpspconf.config_io import CONF_EXT, parse_rc, write_rc 

35from xdgpspconf.utils import fs_perm 

36 

37 

38class ConfDisc(FsDisc): 

39 """ 

40 CONF DISCoverer 

41 

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

43 """ 

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

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

46 

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

48 """ 

49 Shipped, root, user, improper locations 

50 

51 Args: 

52 cname: name of configuration file 

53 Returns: 

54 named dictionary containing respective list of Paths 

55 """ 

56 cname = cname or 'config' 

57 return { 

58 'improper': 

59 self.improper_loc(cname), 

60 'user_loc': 

61 self.user_xdg_loc(cname), 

62 'root_loc': 

63 self.root_xdg_loc(cname), 

64 'shipped': 

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

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

67 } 

68 

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

70 """ 

71 Walk up to nearest mountpoint or project root. 

72 

73 - collect all directories containing __init__.py \ 

74 (assumed to be source directories) 

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

76 or ``setup.py`` 

77 - mountpoint is a unix mountpoint or windows drive root 

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

79 

80 Args: 

81 child_dir: walk ancestry of `this` directory 

82 

83 Returns: 

84 List of Paths to ancestor configs: 

85 First directory is most dominant 

86 """ 

87 config = [] 

88 pedigree = super().trace_ancestors(child_dir) 

89 config.extend( 

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

91 

92 if pedigree: 

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

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

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

96 return config 

97 

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

99 """ 

100 Get XDG_<BASE>_HOME locations. 

101 

102 `specifications 

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

104 

105 Args: 

106 cname: name of config file 

107 

108 Returns: 

109 List of xdg-<base> Paths 

110 First directory is most dominant 

111 Raises: 

112 KeyError: bad variable name 

113 

114 """ 

115 user_base_loc = super().user_xdg_loc() 

116 config = [] 

117 for ext in CONF_EXT: 

118 for loc in user_base_loc: 

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

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

121 return config 

122 

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

124 """ 

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

126 

127 `specifications 

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

129 

130 Args: 

131 cname: name of config file 

132 

133 Returns: 

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

135 First directory is most dominant 

136 Raises: 

137 KeyError: bad variable name 

138 

139 """ 

140 root_base_loc = super().root_xdg_loc() 

141 config = [] 

142 for ext in CONF_EXT: 

143 for loc in root_base_loc: 

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

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

146 return config 

147 

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

149 """ 

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

151 

152 `specifications 

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

154 

155 Args: 

156 cname: name of config file 

157 

158 Returns: 

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

160 First directory is most dominant 

161 Raises: 

162 KeyError: bad variable name 

163 

164 """ 

165 improper_base_loc = super().improper_loc() 

166 config = [] 

167 for ext in CONF_EXT: 

168 for loc in improper_base_loc: 

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

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

171 return config 

172 

173 def get_conf(self, 

174 dom_start: bool = True, 

175 improper: bool = False, 

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

177 """ 

178 Get discovered configuration files. 

179 

180 Args: 

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

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

183 **kwargs: 

184 - custom: custom location 

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

186 project-root and inherit all locations that contain 

187 __init__.py. Project-root is identified by discovery of 

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

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

190 - cname: name of config file 

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

192 """ 

193 dom_order: List[Path] = [] 

194 

195 custom = kwargs.get('custom') 

196 if custom is not None: 

197 # don't check 

198 dom_order.append(Path(custom)) 

199 

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

201 if rc_val is not None: 

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

203 raise FileNotFoundError( 

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

205 dom_order.append(Path(rc_val)) 

206 

207 trace_pwd = kwargs.get('trace_pwd') 

208 if trace_pwd is True: 

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

210 if trace_pwd: 

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

212 dom_order.extend(inheritance) 

213 

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

215 if improper: 

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

217 

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

219 dom_order.extend(locations[loc]) 

220 

221 permargs = { 

222 key: val 

223 for key, val in kwargs.items() 

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

225 } 

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

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

228 if dom_start: 

229 return dom_order 

230 return list(reversed(dom_order)) 

231 

232 def safe_config(self, 

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

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

235 """ 

236 Locate safe writable paths of configuration files. 

237 

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

239 - User must catch: 

240 - ``PermissionError`` 

241 - ``IsADirectoryError`` 

242 - ``FileNotFoundError`` 

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

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

245 

246 Args: 

247 ext: extension filter(s) 

248 **kwargs: 

249 - custom: custom location 

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

251 project-root and inherit all locations that contain 

252 __init__.py. Project-root is identified by discovery of 

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

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

255 - cname: name of config file 

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

257 

258 Returns: 

259 Paths: First path is most dominant 

260 

261 """ 

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

263 if isinstance(ext, str): 

264 ext = [ext] 

265 safe_paths: List[Path] = [] 

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

267 if any(private in str(loc) 

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

269 'pyproject')): 

270 continue 

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

272 continue 

273 safe_paths.append(loc) 

274 return safe_paths 

275 

276 def read_config(self, 

277 flatten: bool = False, 

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

279 """ 

280 Locate Paths to standard directories and parse config. 

281 

282 Args: 

283 flatten: superimpose configurations to return the final outcome 

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 - cname: name of config file 

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

293 

294 Returns: 

295 parsed configuration from each available file: 

296 first file is most dominant 

297 

298 Raises: 

299 BadConf- Bad configuration file format 

300 

301 """ 

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

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

304 # load configs from oldest ancestor to current directory 

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

306 try: 

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

308 except (PermissionError, FileNotFoundError, IsADirectoryError): 

309 pass 

310 

311 if not flatten: 

312 return avail_confs 

313 

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

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

316 super_config.update(config) 

317 return {list(avail_confs.keys())[0]: super_config} 

318 

319 def write_config(self, 

320 data: Dict[str, Any], 

321 force: str = 'fail', 

322 **kwargs) -> bool: 

323 """ 

324 Write data to a safe configuration file. 

325 

326 Args: 

327 data: serial data to save 

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

329 **kwargs: 

330 - custom: custom location 

331 - cname: name of config file 

332 - ext: extension restriction filter(s) 

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

334 project-root and inherit all locations that contain 

335 __init__.py. Project-root is identified by discovery of 

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

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

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

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