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 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': self.improper_loc(cname), 

59 'user_loc': self.user_xdg_loc(cname), 

60 'root_loc': self.root_xdg_loc(cname), 

61 'shipped': self.shipped 

62 } 

63 

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

65 """ 

66 Walk up to nearest mountpoint or project root. 

67 

68 - collect all directories containing __init__.py \ 

69 (assumed to be source directories) 

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

71 or ``setup.py`` 

72 - mountpoint is a unix mountpoint or windows drive root 

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

74 

75 Args: 

76 child_dir: walk ancestry of `this` directory 

77 

78 Returns: 

79 List of Paths to ancestor configs: 

80 First directory is most dominant 

81 """ 

82 config = [] 

83 pedigree = super().trace_ancestors(child_dir) 

84 config.extend( 

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

86 

87 if pedigree: 

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

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

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

91 return config 

92 

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

94 """ 

95 Get XDG_<BASE>_HOME locations. 

96 

97 `specifications 

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

99 

100 Args: 

101 cname: name of config file 

102 

103 Returns: 

104 List of xdg-<base> Paths 

105 First directory is most dominant 

106 Raises: 

107 KeyError: bad variable name 

108 

109 """ 

110 user_base_loc = super().user_xdg_loc() 

111 config = [] 

112 for ext in '.yml', '.yaml', '.toml', '.conf': 

113 for loc in user_base_loc: 

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

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

116 return config 

117 

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

119 """ 

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

121 

122 `specifications 

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

124 

125 Args: 

126 cname: name of config file 

127 

128 Returns: 

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

130 First directory is most dominant 

131 Raises: 

132 KeyError: bad variable name 

133 

134 """ 

135 root_base_loc = super().root_xdg_loc() 

136 config = [] 

137 for ext in '.yml', '.yaml', '.toml', '.conf': 

138 for loc in root_base_loc: 

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

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

141 return config 

142 

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

144 """ 

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

146 

147 `specifications 

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

149 

150 Args: 

151 cname: name of config file 

152 

153 Returns: 

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

155 First directory is most dominant 

156 Raises: 

157 KeyError: bad variable name 

158 

159 """ 

160 improper_base_loc = super().improper_loc() 

161 config = [] 

162 for ext in '.yml', '.yaml', '.toml', '.conf': 

163 for loc in improper_base_loc: 

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

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

166 return config 

167 

168 def get_conf(self, 

169 dom_start: bool = True, 

170 improper: bool = False, 

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

172 """ 

173 Get discovered configuration files. 

174 

175 Args: 

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

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

178 **kwargs: 

179 - custom: custom location 

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

181 project-root and inherit all locations that contain 

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

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

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

185 - cname: name of config file 

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

187 """ 

188 dom_order: List[Path] = [] 

189 

190 custom = kwargs.get('custom') 

191 if custom is not None: 

192 # don't check 

193 dom_order.append(Path(custom)) 

194 

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

196 if rc_val is not None: 

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

198 raise FileNotFoundError( 

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

200 dom_order.append(Path(rc_val)) 

201 

202 trace_pwd = kwargs.get('trace_pwd') 

203 if trace_pwd is True: 

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

205 if trace_pwd: 

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

207 dom_order.extend(inheritance) 

208 

209 if improper: 

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

211 

212 dom_order.extend(self.locations(kwargs.get('cname'))['user_loc']) 

213 dom_order.extend(self.locations(kwargs.get('cname'))['root_loc']) 

214 dom_order.extend(self.locations(kwargs.get('cname'))['shipped']) 

215 permargs = { 

216 key: val 

217 for key, val in kwargs.items() 

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

219 } 

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

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

222 if dom_start: 

223 return dom_order 

224 return list(reversed(dom_order)) 

225 

226 def safe_config(self, 

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

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

229 """ 

230 Locate safe writable paths of configuration files. 

231 

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

233 - User must catch: 

234 - ``PermissionError`` 

235 - ``IsADirectoryError`` 

236 - ``FileNotFoundError`` 

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

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

239 

240 Args: 

241 ext: extension filter(s) 

242 **kwargs: 

243 - custom: custom location 

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

245 project-root and inherit all locations that contain 

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

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

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

249 - cname: name of config file 

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

251 

252 Returns: 

253 Paths: First path is most dominant 

254 

255 """ 

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

257 if isinstance(ext, str): 

258 ext = [ext] 

259 safe_paths: List[Path] = [] 

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

261 if any(private in str(loc) 

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

263 'pyproject')): 

264 continue 

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

266 continue 

267 safe_paths.append(loc) 

268 return safe_paths 

269 

270 def read_config(self, 

271 flatten: bool = False, 

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

273 """ 

274 Locate Paths to standard directories and parse config. 

275 

276 Args: 

277 flatten: superimpose configurations to return the final outcome 

278 **kwargs: 

279 - custom: custom location 

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

281 project-root and inherit all locations that contain 

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

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

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

285 - cname: name of config file 

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

287 

288 Returns: 

289 parsed configuration from each available file: 

290 first file is most dominant 

291 

292 Raises: 

293 BadConf- Bad configuration file format 

294 

295 """ 

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

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

298 # load configs from oldest ancestor to current directory 

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

300 try: 

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

302 except (PermissionError, FileNotFoundError, IsADirectoryError): 

303 pass 

304 

305 if not flatten: 

306 return avail_confs 

307 

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

309 for config in avail_confs.values(): 

310 super_config.update(config) 

311 return {list(avail_confs.keys())[-1]: super_config} 

312 

313 def write_config(self, 

314 data: Dict[str, Any], 

315 force: str = 'fail', 

316 **kwargs) -> bool: 

317 """ 

318 Write data to a safe configuration file. 

319 

320 Args: 

321 data: serial data to save 

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

323 **kwargs: 

324 - custom: custom location 

325 - cname: name of config file 

326 - ext: extension restriction filter(s) 

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

328 project-root and inherit all locations that contain 

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

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

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

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

333 

334 Returns: success 

335 """ 

336 config_l = list( 

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

338 for config in config_l: 

339 try: 

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

341 except (PermissionError, IsADirectoryError, FileNotFoundError): 

342 continue 

343 return False