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 

27Following kwargs are defined for functions that accept **kwargs: 

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 - :py:meth:`xdgpspconf.utils.fs_perm` kwargs: 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 def __init__(self, project: str, shipped: os.PathLike = None, **permargs): 

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

56 

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

58 """ 

59 Shipped, root, user, improper locations 

60 

61 Args: 

62 cname: name of configuration file 

63 Returns: 

64 named dictionary containing respective list of Paths 

65 """ 

66 cname = cname or 'config' 

67 return { 

68 'improper': 

69 self.improper_loc(cname), 

70 'user_loc': 

71 self.user_xdg_loc(cname), 

72 'root_loc': 

73 self.root_xdg_loc(cname), 

74 'shipped': 

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

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

77 } 

78 

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

80 """ 

81 Walk up to nearest mountpoint or project root. 

82 

83 - collect all directories containing __init__.py \ 

84 (assumed to be source directories) 

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

86 or ``setup.py`` 

87 - mountpoint is a unix mountpoint or windows drive root 

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

89 

90 Args: 

91 child_dir: walk ancestry of `this` directory 

92 

93 Returns: 

94 List of Paths to ancestor configs: 

95 First directory is most dominant 

96 """ 

97 config = [] 

98 pedigree = super().trace_ancestors(child_dir) 

99 config.extend( 

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

101 

102 if pedigree: 

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

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

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

106 return config 

107 

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

109 """ 

110 Get XDG_<BASE>_HOME locations. 

111 

112 `specifications 

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

114 

115 Args: 

116 cname: name of config file 

117 

118 Returns: 

119 List of xdg-<base> Paths 

120 First directory is most dominant 

121 Raises: 

122 KeyError: bad variable name 

123 

124 """ 

125 user_base_loc = super().user_xdg_loc() 

126 config = [] 

127 for ext in CONF_EXT: 

128 for loc in user_base_loc: 

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

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

131 return config 

132 

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

134 """ 

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

136 

137 `specifications 

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

139 

140 Args: 

141 cname: name of config file 

142 

143 Returns: 

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

145 First directory is most dominant 

146 Raises: 

147 KeyError: bad variable name 

148 

149 """ 

150 root_base_loc = super().root_xdg_loc() 

151 config = [] 

152 for ext in CONF_EXT: 

153 for loc in root_base_loc: 

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

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

156 return config 

157 

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

159 """ 

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

161 

162 `specifications 

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

164 

165 Args: 

166 cname: name of config file 

167 

168 Returns: 

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

170 First directory is most dominant 

171 Raises: 

172 KeyError: bad variable name 

173 

174 """ 

175 improper_base_loc = super().improper_loc() 

176 config = [] 

177 for ext in CONF_EXT: 

178 for loc in improper_base_loc: 

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

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

181 return config 

182 

183 def get_conf(self, 

184 dom_start: bool = True, 

185 improper: bool = False, 

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

187 """ 

188 Get discovered configuration files. 

189 

190 Args: 

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

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

193 **kwargs 

194 

195 Returns: 

196 List of configuration paths 

197 """ 

198 dom_order: List[Path] = [] 

199 

200 custom = kwargs.get('custom') 

201 if custom is not None: 

202 # don't check 

203 dom_order.append(Path(custom)) 

204 

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

206 if rc_val is not None: 

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

208 raise FileNotFoundError( 

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

210 dom_order.append(Path(rc_val)) 

211 

212 trace_pwd = kwargs.get('trace_pwd') 

213 if trace_pwd is True: 

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

215 if trace_pwd: 

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

217 dom_order.extend(inheritance) 

218 

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

220 if improper: 

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

222 

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

224 dom_order.extend(locations[loc]) 

225 

226 permargs = { 

227 key: val 

228 for key, val in kwargs.items() 

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

230 } 

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

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

233 if dom_start: 

234 return dom_order 

235 return list(reversed(dom_order)) 

236 

237 def safe_config(self, 

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

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

240 """ 

241 Locate safe writable paths of configuration files. 

242 

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

244 - User must catch: 

245 - ``PermissionError`` 

246 - ``IsADirectoryError`` 

247 - ``FileNotFoundError`` 

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

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

250 

251 Args: 

252 ext: extension filter(s) 

253 **kwargs 

254 

255 Returns: 

256 Paths: First path is most dominant 

257 

258 """ 

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

260 if isinstance(ext, str): 

261 ext = [ext] 

262 safe_paths: List[Path] = [] 

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

264 if any(private in str(loc) 

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

266 'pyproject')): 

267 continue 

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

269 continue 

270 safe_paths.append(loc) 

271 return safe_paths 

272 

273 def read_config(self, 

274 flatten: bool = False, 

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

276 """ 

277 Locate Paths to standard directories and parse config. 

278 

279 Args: 

280 flatten: superimpose configurations to return the final outcome 

281 **kwargs 

282 

283 Returns: 

284 parsed configuration from each available file: 

285 first file is most dominant 

286 

287 Raises: 

288 BadConf- Bad configuration file format 

289 

290 """ 

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

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

293 # load configs from oldest ancestor to current directory 

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

295 try: 

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

297 except (PermissionError, FileNotFoundError, IsADirectoryError): 

298 pass 

299 

300 if not flatten: 

301 return avail_confs 

302 

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

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

305 super_config.update(config) 

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

307 

308 def write_config(self, 

309 data: Dict[str, Any], 

310 force: str = 'fail', 

311 **kwargs) -> bool: 

312 """ 

313 Write data to a safe configuration file. 

314 

315 Args: 

316 data: serial data to save 

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

318 **kwargs 

319 

320 Returns: success 

321 """ 

322 config_l = list( 

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

324 for config in config_l: 

325 try: 

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

327 except (PermissionError, IsADirectoryError, FileNotFoundError): 

328 continue 

329 return False