Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/invoke/config.py: 20%

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

407 statements  

1import copy 

2import json 

3import os 

4import types 

5from importlib.util import spec_from_loader 

6from os import PathLike 

7from os.path import join, splitext, expanduser 

8from types import ModuleType 

9from typing import Any, Dict, Iterator, Optional, Tuple, Type, Union 

10 

11from .env import Environment 

12from .exceptions import UnknownFileType, UnpicklableConfigMember 

13from .runners import Local 

14from .terminals import WINDOWS 

15from .util import debug, yaml 

16 

17 

18try: 

19 from importlib.machinery import SourceFileLoader 

20except ImportError: # PyPy3 

21 from importlib._bootstrap import ( # type: ignore[no-redef] 

22 _SourceFileLoader as SourceFileLoader, 

23 ) 

24 

25 

26def load_source(name: str, path: str) -> Dict[str, Any]: 

27 if not os.path.exists(path): 

28 return {} 

29 loader = SourceFileLoader("mod", path) 

30 mod = ModuleType("mod") 

31 mod.__spec__ = spec_from_loader("mod", loader) 

32 loader.exec_module(mod) 

33 return vars(mod) 

34 

35 

36class DataProxy: 

37 """ 

38 Helper class implementing nested dict+attr access for `.Config`. 

39 

40 Specifically, is used both for `.Config` itself, and to wrap any other 

41 dicts assigned as config values (recursively). 

42 

43 .. warning:: 

44 All methods (of this object or in subclasses) must take care to 

45 initialize new attributes via ``self._set(name='value')``, or they'll 

46 run into recursion errors! 

47 

48 .. versionadded:: 1.0 

49 """ 

50 

51 # Attributes which get proxied through to inner merged-dict config obj. 

52 _proxies = ( 

53 tuple( 

54 """ 

55 get 

56 has_key 

57 items 

58 iteritems 

59 iterkeys 

60 itervalues 

61 keys 

62 values 

63 """.split() 

64 ) 

65 + tuple( 

66 "__{}__".format(x) 

67 for x in """ 

68 cmp 

69 contains 

70 iter 

71 sizeof 

72 """.split() 

73 ) 

74 ) 

75 

76 @classmethod 

77 def from_data( 

78 cls, 

79 data: Dict[str, Any], 

80 root: Optional["DataProxy"] = None, 

81 keypath: Tuple[str, ...] = tuple(), 

82 ) -> "DataProxy": 

83 """ 

84 Alternate constructor for 'baby' DataProxies used as sub-dict values. 

85 

86 Allows creating standalone DataProxy objects while also letting 

87 subclasses like `.Config` define their own ``__init__`` without 

88 muddling the two. 

89 

90 :param dict data: 

91 This particular DataProxy's personal data. Required, it's the Data 

92 being Proxied. 

93 

94 :param root: 

95 Optional handle on a root DataProxy/Config which needs notification 

96 on data updates. 

97 

98 :param tuple keypath: 

99 Optional tuple describing the path of keys leading to this 

100 DataProxy's location inside the ``root`` structure. Required if 

101 ``root`` was given (and vice versa.) 

102 

103 .. versionadded:: 1.0 

104 """ 

105 obj = cls() 

106 obj._set(_config=data) 

107 obj._set(_root=root) 

108 obj._set(_keypath=keypath) 

109 return obj 

110 

111 def __getattr__(self, key: str) -> Any: 

112 # NOTE: due to default Python attribute-lookup semantics, "real" 

113 # attributes will always be yielded on attribute access and this method 

114 # is skipped. That behavior is good for us (it's more intuitive than 

115 # having a config key accidentally shadow a real attribute or method). 

116 try: 

117 return self._get(key) 

118 except KeyError: 

119 # Proxy most special vars to config for dict procotol. 

120 if key in self._proxies: 

121 return getattr(self._config, key) 

122 # Otherwise, raise useful AttributeError to follow getattr proto. 

123 err = "No attribute or config key found for {!r}".format(key) 

124 attrs = [x for x in dir(self.__class__) if not x.startswith("_")] 

125 err += "\n\nValid keys: {!r}".format( 

126 sorted(list(self._config.keys())) 

127 ) 

128 err += "\n\nValid real attributes: {!r}".format(attrs) 

129 raise AttributeError(err) 

130 

131 def __setattr__(self, key: str, value: Any) -> None: 

132 # Turn attribute-sets into config updates anytime we don't have a real 

133 # attribute with the given name/key. 

134 has_real_attr = key in dir(self) 

135 if not has_real_attr: 

136 # Make sure to trigger our own __setitem__ instead of going direct 

137 # to our internal dict/cache 

138 self[key] = value 

139 else: 

140 super().__setattr__(key, value) 

141 

142 def __iter__(self) -> Iterator[Dict[str, Any]]: 

143 # For some reason Python is ignoring our __hasattr__ when determining 

144 # whether we support __iter__. BOO 

145 return iter(self._config) 

146 

147 def __eq__(self, other: object) -> bool: 

148 # NOTE: Can't proxy __eq__ because the RHS will always be an obj of the 

149 # current class, not the proxied-to class, and that causes 

150 # NotImplemented. 

151 # Try comparing to other objects like ourselves, falling back to a not 

152 # very comparable value (None) so comparison fails. 

153 other_val = getattr(other, "_config", None) 

154 # But we can compare to vanilla dicts just fine, since our _config is 

155 # itself just a dict. 

156 if isinstance(other, dict): 

157 other_val = other 

158 return bool(self._config == other_val) 

159 

160 def __len__(self) -> int: 

161 return len(self._config) 

162 

163 def __setitem__(self, key: str, value: str) -> None: 

164 self._config[key] = value 

165 self._track_modification_of(key, value) 

166 

167 def __getitem__(self, key: str) -> Any: 

168 return self._get(key) 

169 

170 def _get(self, key: str) -> Any: 

171 # Short-circuit if pickling/copying mechanisms are asking if we've got 

172 # __setstate__ etc; they'll ask this w/o calling our __init__ first, so 

173 # we'd be in a RecursionError-causing catch-22 otherwise. 

174 if key in ("__setstate__",): 

175 raise AttributeError(key) 

176 # At this point we should be able to assume a self._config... 

177 value = self._config[key] 

178 if isinstance(value, dict): 

179 # New object's keypath is simply the key, prepended with our own 

180 # keypath if we've got one. 

181 keypath = (key,) 

182 if hasattr(self, "_keypath"): 

183 keypath = self._keypath + keypath 

184 # If we have no _root, we must be the root, so it's us. Otherwise, 

185 # pass along our handle on the root. 

186 root = getattr(self, "_root", self) 

187 value = DataProxy.from_data(data=value, root=root, keypath=keypath) 

188 return value 

189 

190 def _set(self, *args: Any, **kwargs: Any) -> None: 

191 """ 

192 Convenience workaround of default 'attrs are config keys' behavior. 

193 

194 Uses `object.__setattr__` to work around the class' normal proxying 

195 behavior, but is less verbose than using that directly. 

196 

197 Has two modes (which may be combined if you really want): 

198 

199 - ``self._set('attrname', value)``, just like ``__setattr__`` 

200 - ``self._set(attname=value)`` (i.e. kwargs), even less typing. 

201 """ 

202 if args: 

203 object.__setattr__(self, *args) 

204 for key, value in kwargs.items(): 

205 object.__setattr__(self, key, value) 

206 

207 def __repr__(self) -> str: 

208 return "<{}: {}>".format(self.__class__.__name__, self._config) 

209 

210 def __contains__(self, key: str) -> bool: 

211 return key in self._config 

212 

213 @property 

214 def _is_leaf(self) -> bool: 

215 return hasattr(self, "_root") 

216 

217 @property 

218 def _is_root(self) -> bool: 

219 return hasattr(self, "_modify") 

220 

221 def _track_removal_of(self, key: str) -> None: 

222 # Grab the root object responsible for tracking removals; either the 

223 # referenced root (if we're a leaf) or ourselves (if we're not). 

224 # (Intermediate nodes never have anything but __getitem__ called on 

225 # them, otherwise they're by definition being treated as a leaf.) 

226 target = None 

227 if self._is_leaf: 

228 target = self._root 

229 elif self._is_root: 

230 target = self 

231 if target is not None: 

232 target._remove(getattr(self, "_keypath", tuple()), key) 

233 

234 def _track_modification_of(self, key: str, value: str) -> None: 

235 target = None 

236 if self._is_leaf: 

237 target = self._root 

238 elif self._is_root: 

239 target = self 

240 if target is not None: 

241 target._modify(getattr(self, "_keypath", tuple()), key, value) 

242 

243 def __delitem__(self, key: str) -> None: 

244 del self._config[key] 

245 self._track_removal_of(key) 

246 

247 def __delattr__(self, name: str) -> None: 

248 # Make sure we don't screw up true attribute deletion for the 

249 # situations that actually want it. (Uncommon, but not rare.) 

250 if name in self: 

251 del self[name] 

252 else: 

253 object.__delattr__(self, name) 

254 

255 def clear(self) -> None: 

256 keys = list(self.keys()) 

257 for key in keys: 

258 del self[key] 

259 

260 def pop(self, *args: Any) -> Any: 

261 # Must test this up front before (possibly) mutating self._config 

262 key_existed = args and args[0] in self._config 

263 # We always have a _config (whether it's a real dict or a cache of 

264 # merged levels) so we can fall back to it for all the corner case 

265 # handling re: args (arity, handling a default, raising KeyError, etc) 

266 ret = self._config.pop(*args) 

267 # If it looks like no popping occurred (key wasn't there), presumably 

268 # user gave default, so we can short-circuit return here - no need to 

269 # track a deletion that did not happen. 

270 if not key_existed: 

271 return ret 

272 # Here, we can assume at least the 1st posarg (key) existed. 

273 self._track_removal_of(args[0]) 

274 # In all cases, return the popped value. 

275 return ret 

276 

277 def popitem(self) -> Any: 

278 ret = self._config.popitem() 

279 self._track_removal_of(ret[0]) 

280 return ret 

281 

282 def setdefault(self, *args: Any) -> Any: 

283 # Must test up front whether the key existed beforehand 

284 key_existed = args and args[0] in self._config 

285 # Run locally 

286 ret = self._config.setdefault(*args) 

287 # Key already existed -> nothing was mutated, short-circuit 

288 if key_existed: 

289 return ret 

290 # Here, we can assume the key did not exist and thus user must have 

291 # supplied a 'default' (if they did not, the real setdefault() above 

292 # would have excepted.) 

293 key, default = args 

294 self._track_modification_of(key, default) 

295 return ret 

296 

297 def update(self, *args: Any, **kwargs: Any) -> None: 

298 if kwargs: 

299 for key, value in kwargs.items(): 

300 self[key] = value 

301 elif args: 

302 # TODO: complain if arity>1 

303 arg = args[0] 

304 if isinstance(arg, dict): 

305 for key in arg: 

306 self[key] = arg[key] 

307 else: 

308 # TODO: be stricter about input in this case 

309 for pair in arg: 

310 self[pair[0]] = pair[1] 

311 

312 

313class Config(DataProxy): 

314 """ 

315 Invoke's primary configuration handling class. 

316 

317 See :doc:`/concepts/configuration` for details on the configuration system 

318 this class implements, including the :ref:`configuration hierarchy 

319 <config-hierarchy>`. The rest of this class' documentation assumes 

320 familiarity with that document. 

321 

322 **Access** 

323 

324 Configuration values may be accessed and/or updated using dict syntax:: 

325 

326 config['foo'] 

327 

328 or attribute syntax:: 

329 

330 config.foo 

331 

332 Nesting works the same way - dict config values are turned into objects 

333 which honor both the dictionary protocol and the attribute-access method:: 

334 

335 config['foo']['bar'] 

336 config.foo.bar 

337 

338 **A note about attribute access and methods** 

339 

340 This class implements the entire dictionary protocol: methods such as 

341 ``keys``, ``values``, ``items``, ``pop`` and so forth should all function 

342 as they do on regular dicts. It also implements new config-specific methods 

343 such as `load_system`, `load_collection`, `merge`, `clone`, etc. 

344 

345 .. warning:: 

346 Accordingly, this means that if you have configuration options sharing 

347 names with these methods, you **must** use dictionary syntax (e.g. 

348 ``myconfig['keys']``) to access the configuration data. 

349 

350 **Lifecycle** 

351 

352 At initialization time, `.Config`: 

353 

354 - creates per-level data structures; 

355 - stores any levels supplied to `__init__`, such as defaults or overrides, 

356 as well as the various config file paths/filename patterns; 

357 - and loads config files, if found (though typically this just means system 

358 and user-level files, as project and runtime files need more info before 

359 they can be found and loaded.) 

360 

361 - This step can be skipped by specifying ``lazy=True``. 

362 

363 At this point, `.Config` is fully usable - and because it pre-emptively 

364 loads some config files, those config files can affect anything that 

365 comes after, like CLI parsing or loading of task collections. 

366 

367 In the CLI use case, further processing is done after instantiation, using 

368 the ``load_*`` methods such as `load_overrides`, `load_project`, etc: 

369 

370 - the result of argument/option parsing is applied to the overrides level; 

371 - a project-level config file is loaded, as it's dependent on a loaded 

372 tasks collection; 

373 - a runtime config file is loaded, if its flag was supplied; 

374 - then, for each task being executed: 

375 

376 - per-collection data is loaded (only possible now that we have 

377 collection & task in hand); 

378 - shell environment data is loaded (must be done at end of process due 

379 to using the rest of the config as a guide for interpreting env var 

380 names.) 

381 

382 At this point, the config object is handed to the task being executed, as 

383 part of its execution `.Context`. 

384 

385 Any modifications made directly to the `.Config` itself after this point 

386 end up stored in their own (topmost) config level, making it easier to 

387 debug final values. 

388 

389 Finally, any *deletions* made to the `.Config` (e.g. applications of 

390 dict-style mutators like ``pop``, ``clear`` etc) are also tracked in their 

391 own structure, allowing the config object to honor such method calls 

392 without mutating the underlying source data. 

393 

394 **Special class attributes** 

395 

396 The following class-level attributes are used for low-level configuration 

397 of the config system itself, such as which file paths to load. They are 

398 primarily intended for overriding by subclasses. 

399 

400 - ``prefix``: Supplies the default value for ``file_prefix`` (directly) and 

401 ``env_prefix`` (uppercased). See their descriptions for details. Its 

402 default value is ``"invoke"``. 

403 - ``file_prefix``: The config file 'basename' default (though it is not a 

404 literal basename; it can contain path parts if desired) which is appended 

405 to the configured values of ``system_prefix``, ``user_prefix``, etc, to 

406 arrive at the final (pre-extension) file paths. 

407 

408 Thus, by default, a system-level config file path concatenates the 

409 ``system_prefix`` of ``/etc/`` with the ``file_prefix`` of ``invoke`` to 

410 arrive at paths like ``/etc/invoke.json``. 

411 

412 Defaults to ``None``, meaning to use the value of ``prefix``. 

413 

414 - ``env_prefix``: A prefix used (along with a joining underscore) to 

415 determine which environment variables are loaded as the env var 

416 configuration level. Since its default is the value of ``prefix`` 

417 capitalized, this means env vars like ``INVOKE_RUN_ECHO`` are sought by 

418 default. 

419 

420 Defaults to ``None``, meaning to use the value of ``prefix``. 

421 

422 .. versionadded:: 1.0 

423 """ 

424 

425 prefix = "invoke" 

426 file_prefix = None 

427 env_prefix = None 

428 

429 @staticmethod 

430 def global_defaults() -> Dict[str, Any]: 

431 """ 

432 Return the core default settings for Invoke. 

433 

434 Generally only for use by `.Config` internals. For descriptions of 

435 these values, see :ref:`default-values`. 

436 

437 Subclasses may choose to override this method, calling 

438 ``Config.global_defaults`` and applying `.merge_dicts` to the result, 

439 to add to or modify these values. 

440 

441 .. versionadded:: 1.0 

442 """ 

443 # On Windows, which won't have /bin/bash, check for a set COMSPEC env 

444 # var (https://en.wikipedia.org/wiki/COMSPEC) or fallback to an 

445 # unqualified cmd.exe otherwise. 

446 if WINDOWS: 

447 shell = os.environ.get("COMSPEC", "cmd.exe") 

448 # Else, assume Unix, most distros of which have /bin/bash available. 

449 # TODO: consider an automatic fallback to /bin/sh for systems lacking 

450 # /bin/bash; however users may configure run.shell quite easily, so... 

451 else: 

452 shell = "/bin/bash" 

453 

454 return { 

455 # TODO: we document 'debug' but it's not truly implemented outside 

456 # of env var and CLI flag. If we honor it, we have to go around and 

457 # figure out at what points we might want to call 

458 # `util.enable_logging`: 

459 # - just using it as a fallback default for arg parsing isn't much 

460 # use, as at that point the config holds nothing but defaults & CLI 

461 # flag values 

462 # - doing it at file load time might be somewhat useful, though 

463 # where this happens may be subject to change soon 

464 # - doing it at env var load time seems a bit silly given the 

465 # existing support for at-startup testing for INVOKE_DEBUG 

466 # 'debug': False, 

467 # TODO: I feel like we want these to be more consistent re: default 

468 # values stored here vs 'stored' as logic where they are 

469 # referenced, there are probably some bits that are all "if None -> 

470 # default" that could go here. Alternately, make _more_ of these 

471 # default to None? 

472 "run": { 

473 "asynchronous": False, 

474 "disown": False, 

475 "dry": False, 

476 "echo": False, 

477 "echo_stdin": None, 

478 "encoding": None, 

479 "env": {}, 

480 "err_stream": None, 

481 "fallback": True, 

482 "hide": None, 

483 "in_stream": None, 

484 "out_stream": None, 

485 "echo_format": "\033[1;37m{command}\033[0m", 

486 "pty": False, 

487 "replace_env": False, 

488 "shell": shell, 

489 "warn": False, 

490 "watchers": [], 

491 }, 

492 # This doesn't live inside the 'run' tree; otherwise it'd make it 

493 # somewhat harder to extend/override in Fabric 2 which has a split 

494 # local/remote runner situation. 

495 "runners": {"local": Local}, 

496 "sudo": { 

497 "password": None, 

498 "prompt": "[sudo] password: ", 

499 "user": None, 

500 }, 

501 "tasks": { 

502 "auto_dash_names": True, 

503 "collection_name": "tasks", 

504 "dedupe": True, 

505 "executor_class": None, 

506 "ignore_unknown_help": False, 

507 "search_root": None, 

508 }, 

509 "timeouts": {"command": None}, 

510 } 

511 

512 def __init__( 

513 self, 

514 overrides: Optional[Dict[str, Any]] = None, 

515 defaults: Optional[Dict[str, Any]] = None, 

516 system_prefix: Optional[str] = None, 

517 user_prefix: Optional[str] = None, 

518 project_location: Optional[PathLike] = None, 

519 runtime_path: Optional[PathLike] = None, 

520 lazy: bool = False, 

521 ): 

522 """ 

523 Creates a new config object. 

524 

525 :param dict defaults: 

526 A dict containing default (lowest level) config data. Default: 

527 `global_defaults`. 

528 

529 :param dict overrides: 

530 A dict containing override-level config data. Default: ``{}``. 

531 

532 :param str system_prefix: 

533 Base path for the global config file location; combined with the 

534 prefix and file suffixes to arrive at final file path candidates. 

535 

536 Default: ``/etc/`` (thus e.g. ``/etc/invoke.yaml`` or 

537 ``/etc/invoke.json``). 

538 

539 :param str user_prefix: 

540 Like ``system_prefix`` but for the per-user config file. These 

541 variables are joined as strings, not via path-style joins, so they 

542 may contain partial file paths; for the per-user config file this 

543 often means a leading dot, to make the final result a hidden file 

544 on most systems. 

545 

546 Default: ``~/.`` (e.g. ``~/.invoke.yaml``). 

547 

548 :param str project_location: 

549 Optional directory path of the currently loaded `.Collection` (as 

550 loaded by `.Loader`). When non-empty, will trigger seeking of 

551 per-project config files in this directory. 

552 

553 :param str runtime_path: 

554 Optional file path to a runtime configuration file. 

555 

556 Used to fill the penultimate slot in the config hierarchy. Should 

557 be a full file path to an existing file, not a directory path or a 

558 prefix. 

559 

560 :param bool lazy: 

561 Whether to automatically load some of the lower config levels. 

562 

563 By default (``lazy=False``), ``__init__`` automatically calls 

564 `load_system` and `load_user` to load system and user config files, 

565 respectively. 

566 

567 For more control over what is loaded when, you can say 

568 ``lazy=True``, and no automatic loading is done. 

569 

570 .. note:: 

571 If you give ``defaults`` and/or ``overrides`` as ``__init__`` 

572 kwargs instead of waiting to use `load_defaults` or 

573 `load_overrides` afterwards, those *will* still end up 'loaded' 

574 immediately. 

575 """ 

576 # Technically an implementation detail - do not expose in public API. 

577 # Stores merged configs and is accessed via DataProxy. 

578 self._set(_config={}) 

579 

580 # Config file suffixes to search, in preference order. 

581 self._set(_file_suffixes=("yaml", "yml", "json", "py")) 

582 

583 # Default configuration values, typically a copy of `global_defaults`. 

584 if defaults is None: 

585 defaults = copy_dict(self.global_defaults()) 

586 self._set(_defaults=defaults) 

587 

588 # Collection-driven config data, gathered from the collection tree 

589 # containing the currently executing task. 

590 self._set(_collection={}) 

591 

592 # Path prefix searched for the system config file. 

593 # NOTE: There is no default system prefix on Windows. 

594 if system_prefix is None and not WINDOWS: 

595 system_prefix = "/etc/" 

596 self._set(_system_prefix=system_prefix) 

597 # Path to loaded system config file, if any. 

598 self._set(_system_path=None) 

599 # Whether the system config file has been loaded or not (or ``None`` if 

600 # no loading has been attempted yet.) 

601 self._set(_system_found=None) 

602 # Data loaded from the system config file. 

603 self._set(_system={}) 

604 

605 # Path prefix searched for per-user config files. 

606 if user_prefix is None: 

607 user_prefix = "~/." 

608 self._set(_user_prefix=user_prefix) 

609 # Path to loaded user config file, if any. 

610 self._set(_user_path=None) 

611 # Whether the user config file has been loaded or not (or ``None`` if 

612 # no loading has been attempted yet.) 

613 self._set(_user_found=None) 

614 # Data loaded from the per-user config file. 

615 self._set(_user={}) 

616 

617 # As it may want to be set post-init, project conf file related attrs 

618 # get initialized or overwritten via a specific method. 

619 self.set_project_location(project_location) 

620 

621 # Environment variable name prefix 

622 env_prefix = self.env_prefix 

623 if env_prefix is None: 

624 env_prefix = self.prefix 

625 env_prefix = "{}_".format(env_prefix.upper()) 

626 self._set(_env_prefix=env_prefix) 

627 # Config data loaded from the shell environment. 

628 self._set(_env={}) 

629 

630 # As it may want to be set post-init, runtime conf file related attrs 

631 # get initialized or overwritten via a specific method. 

632 self.set_runtime_path(runtime_path) 

633 

634 # Overrides - highest normal config level. Typically filled in from 

635 # command-line flags. 

636 if overrides is None: 

637 overrides = {} 

638 self._set(_overrides=overrides) 

639 

640 # Absolute highest level: user modifications. 

641 self._set(_modifications={}) 

642 # And its sibling: user deletions. (stored as a flat dict of keypath 

643 # keys and dummy values, for constant-time membership testing/removal 

644 # w/ no messy recursion. TODO: maybe redo _everything_ that way? in 

645 # _modifications and other levels, the values would of course be 

646 # valuable and not just None) 

647 self._set(_deletions={}) 

648 

649 # Convenience loading of user and system files, since those require no 

650 # other levels in order to function. 

651 if not lazy: 

652 self.load_base_conf_files() 

653 # Always merge, otherwise defaults, etc are not usable until creator or 

654 # a subroutine does so. 

655 self.merge() 

656 

657 def load_base_conf_files(self) -> None: 

658 # Just a refactor of something done in unlazy init or in clone() 

659 self.load_system(merge=False) 

660 self.load_user(merge=False) 

661 

662 def load_defaults(self, data: Dict[str, Any], merge: bool = True) -> None: 

663 """ 

664 Set or replace the 'defaults' configuration level, from ``data``. 

665 

666 :param dict data: The config data to load as the defaults level. 

667 

668 :param bool merge: 

669 Whether to merge the loaded data into the central config. Default: 

670 ``True``. 

671 

672 :returns: ``None``. 

673 

674 .. versionadded:: 1.0 

675 """ 

676 self._set(_defaults=data) 

677 if merge: 

678 self.merge() 

679 

680 def load_overrides(self, data: Dict[str, Any], merge: bool = True) -> None: 

681 """ 

682 Set or replace the 'overrides' configuration level, from ``data``. 

683 

684 :param dict data: The config data to load as the overrides level. 

685 

686 :param bool merge: 

687 Whether to merge the loaded data into the central config. Default: 

688 ``True``. 

689 

690 :returns: ``None``. 

691 

692 .. versionadded:: 1.0 

693 """ 

694 self._set(_overrides=data) 

695 if merge: 

696 self.merge() 

697 

698 def load_system(self, merge: bool = True) -> None: 

699 """ 

700 Load a system-level config file, if possible. 

701 

702 Checks the configured ``_system_prefix`` path, which defaults to 

703 ``/etc``, and will thus load files like ``/etc/invoke.yml``. 

704 

705 :param bool merge: 

706 Whether to merge the loaded data into the central config. Default: 

707 ``True``. 

708 

709 :returns: ``None``. 

710 

711 .. versionadded:: 1.0 

712 """ 

713 self._load_file(prefix="system", merge=merge) 

714 

715 def load_user(self, merge: bool = True) -> None: 

716 """ 

717 Load a user-level config file, if possible. 

718 

719 Checks the configured ``_user_prefix`` path, which defaults to ``~/.``, 

720 and will thus load files like ``~/.invoke.yml``. 

721 

722 :param bool merge: 

723 Whether to merge the loaded data into the central config. Default: 

724 ``True``. 

725 

726 :returns: ``None``. 

727 

728 .. versionadded:: 1.0 

729 """ 

730 self._load_file(prefix="user", merge=merge) 

731 

732 def load_project(self, merge: bool = True) -> None: 

733 """ 

734 Load a project-level config file, if possible. 

735 

736 Checks the configured ``_project_prefix`` value derived from the path 

737 given to `set_project_location`, which is typically set to the 

738 directory containing the loaded task collection. 

739 

740 Thus, if one were to run the CLI tool against a tasks collection 

741 ``/home/myuser/code/tasks.py``, `load_project` would seek out files 

742 like ``/home/myuser/code/invoke.yml``. 

743 

744 :param bool merge: 

745 Whether to merge the loaded data into the central config. Default: 

746 ``True``. 

747 

748 :returns: ``None``. 

749 

750 .. versionadded:: 1.0 

751 """ 

752 self._load_file(prefix="project", merge=merge) 

753 

754 def set_runtime_path(self, path: Optional[PathLike]) -> None: 

755 """ 

756 Set the runtime config file path. 

757 

758 .. versionadded:: 1.0 

759 """ 

760 # Path to the user-specified runtime config file. 

761 self._set(_runtime_path=path) 

762 # Data loaded from the runtime config file. 

763 self._set(_runtime={}) 

764 # Whether the runtime config file has been loaded or not (or ``None`` 

765 # if no loading has been attempted yet.) 

766 self._set(_runtime_found=None) 

767 

768 def load_runtime(self, merge: bool = True) -> None: 

769 """ 

770 Load a runtime-level config file, if one was specified. 

771 

772 When the CLI framework creates a `Config`, it sets ``_runtime_path``, 

773 which is a full path to the requested config file. This method attempts 

774 to load that file. 

775 

776 :param bool merge: 

777 Whether to merge the loaded data into the central config. Default: 

778 ``True``. 

779 

780 :returns: ``None``. 

781 

782 .. versionadded:: 1.0 

783 """ 

784 self._load_file(prefix="runtime", absolute=True, merge=merge) 

785 

786 def load_shell_env(self) -> None: 

787 """ 

788 Load values from the shell environment. 

789 

790 `.load_shell_env` is intended for execution late in a `.Config` 

791 object's lifecycle, once all other sources (such as a runtime config 

792 file or per-collection configurations) have been loaded. Loading from 

793 the shell is not terrifically expensive, but must be done at a specific 

794 point in time to ensure the "only known config keys are loaded from the 

795 env" behavior works correctly. 

796 

797 See :ref:`env-vars` for details on this design decision and other info 

798 re: how environment variables are scanned and loaded. 

799 

800 .. versionadded:: 1.0 

801 """ 

802 # Force merge of existing data to ensure we have an up to date picture 

803 debug("Running pre-merge for shell env loading...") 

804 self.merge() 

805 debug("Done with pre-merge.") 

806 loader = Environment(config=self._config, prefix=self._env_prefix) 

807 self._set(_env=loader.load()) 

808 debug("Loaded shell environment, triggering final merge") 

809 self.merge() 

810 

811 def load_collection( 

812 self, data: Dict[str, Any], merge: bool = True 

813 ) -> None: 

814 """ 

815 Update collection-driven config data. 

816 

817 `.load_collection` is intended for use by the core task execution 

818 machinery, which is responsible for obtaining collection-driven data. 

819 See :ref:`collection-configuration` for details. 

820 

821 .. versionadded:: 1.0 

822 """ 

823 debug("Loading collection configuration") 

824 self._set(_collection=data) 

825 if merge: 

826 self.merge() 

827 

828 def set_project_location(self, path: Union[PathLike, str, None]) -> None: 

829 """ 

830 Set the directory path where a project-level config file may be found. 

831 

832 Does not do any file loading on its own; for that, see `load_project`. 

833 

834 .. versionadded:: 1.0 

835 """ 

836 # 'Prefix' to match the other sets of attrs 

837 project_prefix = None 

838 if path is not None: 

839 # Ensure the prefix is normalized to a directory-like path string 

840 project_prefix = join(path, "") 

841 self._set(_project_prefix=project_prefix) 

842 # Path to loaded per-project config file, if any. 

843 self._set(_project_path=None) 

844 # Whether the project config file has been loaded or not (or ``None`` 

845 # if no loading has been attempted yet.) 

846 self._set(_project_found=None) 

847 # Data loaded from the per-project config file. 

848 self._set(_project={}) 

849 

850 def _load_file( 

851 self, prefix: str, absolute: bool = False, merge: bool = True 

852 ) -> None: 

853 # Setup 

854 found = "_{}_found".format(prefix) 

855 path = "_{}_path".format(prefix) 

856 data = "_{}".format(prefix) 

857 midfix = self.file_prefix 

858 if midfix is None: 

859 midfix = self.prefix 

860 # Short-circuit if loading appears to have occurred already 

861 if getattr(self, found) is not None: 

862 return 

863 # Moar setup 

864 if absolute: 

865 absolute_path = getattr(self, path) 

866 # None -> expected absolute path but none set, short circuit 

867 if absolute_path is None: 

868 return 

869 paths = [absolute_path] 

870 else: 

871 path_prefix = getattr(self, "_{}_prefix".format(prefix)) 

872 # Short circuit if loading seems unnecessary (eg for project config 

873 # files when not running out of a project) 

874 if path_prefix is None: 

875 return 

876 paths = [ 

877 ".".join((path_prefix + midfix, x)) 

878 for x in self._file_suffixes 

879 ] 

880 # Poke 'em 

881 for filepath in paths: 

882 # Normalize 

883 filepath = expanduser(filepath) 

884 try: 

885 try: 

886 type_ = splitext(filepath)[1].lstrip(".") 

887 loader = getattr(self, "_load_{}".format(type_)) 

888 except AttributeError: 

889 msg = "Config files of type {!r} (from file {!r}) are not supported! Please use one of: {!r}" # noqa 

890 raise UnknownFileType( 

891 msg.format(type_, filepath, self._file_suffixes) 

892 ) 

893 # Store data, the path it was found at, and fact that it was 

894 # found 

895 self._set(data, loader(filepath)) 

896 self._set(path, filepath) 

897 self._set(found, True) 

898 break 

899 # Typically means 'no such file', so just note & skip past. 

900 except IOError as e: 

901 if e.errno == 2: 

902 err = "Didn't see any {}, skipping." 

903 debug(err.format(filepath)) 

904 else: 

905 raise 

906 # Still None -> no suffixed paths were found, record this fact 

907 if getattr(self, path) is None: 

908 self._set(found, False) 

909 # Merge loaded data in if any was found 

910 elif merge: 

911 self.merge() 

912 

913 def _load_yaml(self, path: PathLike) -> Any: 

914 with open(path) as fd: 

915 return yaml.safe_load(fd) 

916 

917 _load_yml = _load_yaml 

918 

919 def _load_json(self, path: PathLike) -> Any: 

920 with open(path) as fd: 

921 return json.load(fd) 

922 

923 def _load_py(self, path: str) -> Dict[str, Any]: 

924 data = {} 

925 for key, value in (load_source("mod", path)).items(): 

926 # Strip special members, as these are always going to be builtins 

927 # and other special things a user will not want in their config. 

928 if key.startswith("__"): 

929 continue 

930 # Raise exceptions on module values; they are unpicklable. 

931 # TODO: suck it up and reimplement copy() without pickling? Then 

932 # again, a user trying to stuff a module into their config is 

933 # probably doing something better done in runtime/library level 

934 # code and not in a "config file"...right? 

935 if isinstance(value, types.ModuleType): 

936 err = "'{}' is a module, which can't be used as a config value. (Are you perhaps giving a tasks file instead of a config file by mistake?)" # noqa 

937 raise UnpicklableConfigMember(err.format(key)) 

938 data[key] = value 

939 return data 

940 

941 def merge(self) -> None: 

942 """ 

943 Merge all config sources, in order. 

944 

945 .. versionadded:: 1.0 

946 """ 

947 debug("Merging config sources in order onto new empty _config...") 

948 self._set(_config={}) 

949 debug("Defaults: {!r}".format(self._defaults)) 

950 merge_dicts(self._config, self._defaults) 

951 debug("Collection-driven: {!r}".format(self._collection)) 

952 merge_dicts(self._config, self._collection) 

953 self._merge_file("system", "System-wide") 

954 self._merge_file("user", "Per-user") 

955 self._merge_file("project", "Per-project") 

956 debug("Environment variable config: {!r}".format(self._env)) 

957 merge_dicts(self._config, self._env) 

958 self._merge_file("runtime", "Runtime") 

959 debug("Overrides: {!r}".format(self._overrides)) 

960 merge_dicts(self._config, self._overrides) 

961 debug("Modifications: {!r}".format(self._modifications)) 

962 merge_dicts(self._config, self._modifications) 

963 debug("Deletions: {!r}".format(self._deletions)) 

964 obliterate(self._config, self._deletions) 

965 

966 def _merge_file(self, name: str, desc: str) -> None: 

967 # Setup 

968 desc += " config file" # yup 

969 found = getattr(self, "_{}_found".format(name)) 

970 path = getattr(self, "_{}_path".format(name)) 

971 data = getattr(self, "_{}".format(name)) 

972 # None -> no loading occurred yet 

973 if found is None: 

974 debug("{} has not been loaded yet, skipping".format(desc)) 

975 # True -> hooray 

976 elif found: 

977 debug("{} ({}): {!r}".format(desc, path, data)) 

978 merge_dicts(self._config, data) 

979 # False -> did try, did not succeed 

980 else: 

981 # TODO: how to preserve what was tried for each case but only for 

982 # the negative? Just a branch here based on 'name'? 

983 debug("{} not found, skipping".format(desc)) 

984 

985 def clone(self, into: Optional[Type["Config"]] = None) -> "Config": 

986 """ 

987 Return a copy of this configuration object. 

988 

989 The new object will be identical in terms of configured sources and any 

990 loaded (or user-manipulated) data, but will be a distinct object with 

991 as little shared mutable state as possible. 

992 

993 Specifically, all `dict` values within the config are recursively 

994 recreated, with non-dict leaf values subjected to `copy.copy` (note: 

995 *not* `copy.deepcopy`, as this can cause issues with various objects 

996 such as compiled regexen or threading locks, often found buried deep 

997 within rich aggregates like API or DB clients). 

998 

999 The only remaining config values that may end up shared between a 

1000 config and its clone are thus those 'rich' objects that do not 

1001 `copy.copy` cleanly, or compound non-dict objects (such as lists or 

1002 tuples). 

1003 

1004 :param into: 

1005 A `.Config` subclass that the new clone should be "upgraded" to. 

1006 

1007 Used by client libraries which have their own `.Config` subclasses 

1008 that e.g. define additional defaults; cloning "into" one of these 

1009 subclasses ensures that any new keys/subtrees are added gracefully, 

1010 without overwriting anything that may have been pre-defined. 

1011 

1012 Default: ``None`` (just clone into another regular `.Config`). 

1013 

1014 :returns: 

1015 A `.Config`, or an instance of the class given to ``into``. 

1016 

1017 .. versionadded:: 1.0 

1018 """ 

1019 # Construct new object 

1020 klass = self.__class__ if into is None else into 

1021 # Also allow arbitrary constructor kwargs, for subclasses where passing 

1022 # (some) data in at init time is desired (vs post-init copying) 

1023 # TODO: probably want to pivot the whole class this way eventually...? 

1024 # No longer recall exactly why we went with the 'fresh init + attribute 

1025 # setting' approach originally...tho there's clearly some impedance 

1026 # mismatch going on between "I want stuff to happen in my config's 

1027 # instantiation" and "I want cloning to not trigger certain things like 

1028 # external data source loading". 

1029 # NOTE: this will include lazy=True, see end of method 

1030 new = klass(**self._clone_init_kwargs(into=into)) 

1031 # Copy/merge/etc all 'private' data sources and attributes 

1032 for name in """ 

1033 collection 

1034 system_prefix 

1035 system_path 

1036 system_found 

1037 system 

1038 user_prefix 

1039 user_path 

1040 user_found 

1041 user 

1042 project_prefix 

1043 project_path 

1044 project_found 

1045 project 

1046 env_prefix 

1047 env 

1048 runtime_path 

1049 runtime_found 

1050 runtime 

1051 overrides 

1052 modifications 

1053 """.split(): 

1054 name = "_{}".format(name) 

1055 my_data = getattr(self, name) 

1056 # Non-dict data gets carried over straight (via a copy()) 

1057 # NOTE: presumably someone could really screw up and change these 

1058 # values' types, but at that point it's on them... 

1059 if not isinstance(my_data, dict): 

1060 new._set(name, copy.copy(my_data)) 

1061 # Dict data gets merged (which also involves a copy.copy 

1062 # eventually) 

1063 else: 

1064 merge_dicts(getattr(new, name), my_data) 

1065 # Do what __init__ would've done if not lazy, i.e. load user/system 

1066 # conf files. 

1067 new.load_base_conf_files() 

1068 # Finally, merge() for reals (_load_base_conf_files doesn't do so 

1069 # internally, so that data wouldn't otherwise show up.) 

1070 new.merge() 

1071 return new 

1072 

1073 def _clone_init_kwargs( 

1074 self, into: Optional[Type["Config"]] = None 

1075 ) -> Dict[str, Any]: 

1076 """ 

1077 Supply kwargs suitable for initializing a new clone of this object. 

1078 

1079 Note that most of the `.clone` process involves copying data between 

1080 two instances instead of passing init kwargs; however, sometimes you 

1081 really do want init kwargs, which is why this method exists. 

1082 

1083 :param into: The value of ``into`` as passed to the calling `.clone`. 

1084 

1085 :returns: A `dict`. 

1086 """ 

1087 # NOTE: must pass in defaults fresh or otherwise global_defaults() gets 

1088 # used instead. Except when 'into' is in play, in which case we truly 

1089 # want the union of the two. 

1090 new_defaults = copy_dict(self._defaults) 

1091 if into is not None: 

1092 merge_dicts(new_defaults, into.global_defaults()) 

1093 # The kwargs. 

1094 return dict( 

1095 defaults=new_defaults, 

1096 # TODO: consider making this 'hardcoded' on the calling end (ie 

1097 # inside clone()) to make sure nobody accidentally nukes it via 

1098 # subclassing? 

1099 lazy=True, 

1100 ) 

1101 

1102 def _modify(self, keypath: Tuple[str, ...], key: str, value: str) -> None: 

1103 """ 

1104 Update our user-modifications config level with new data. 

1105 

1106 :param tuple keypath: 

1107 The key path identifying the sub-dict being updated. May be an 

1108 empty tuple if the update is occurring at the topmost level. 

1109 

1110 :param str key: 

1111 The actual key receiving an update. 

1112 

1113 :param value: 

1114 The value being written. 

1115 """ 

1116 # First, ensure we wipe the keypath from _deletions, in case it was 

1117 # previously deleted. 

1118 excise(self._deletions, keypath + (key,)) 

1119 # Now we can add it to the modifications structure. 

1120 data = self._modifications 

1121 keypath_list = list(keypath) 

1122 while keypath_list: 

1123 subkey = keypath_list.pop(0) 

1124 # TODO: could use defaultdict here, but...meh? 

1125 if subkey not in data: 

1126 # TODO: generify this and the subsequent 3 lines... 

1127 data[subkey] = {} 

1128 data = data[subkey] 

1129 data[key] = value 

1130 self.merge() 

1131 

1132 def _remove(self, keypath: Tuple[str, ...], key: str) -> None: 

1133 """ 

1134 Like `._modify`, but for removal. 

1135 """ 

1136 # NOTE: because deletions are processed in merge() last, we do not need 

1137 # to remove things from _modifications on removal; but we *do* do the 

1138 # inverse - remove from _deletions on modification. 

1139 # TODO: may be sane to push this step up to callers? 

1140 data = self._deletions 

1141 keypath_list = list(keypath) 

1142 while keypath_list: 

1143 subkey = keypath_list.pop(0) 

1144 if subkey in data: 

1145 data = data[subkey] 

1146 # If we encounter None, it means something higher up than our 

1147 # requested keypath is already marked as deleted; so we don't 

1148 # have to do anything or go further. 

1149 if data is None: 

1150 return 

1151 # Otherwise it's presumably another dict, so keep looping... 

1152 else: 

1153 # Key not found -> nobody's marked anything along this part of 

1154 # the path for deletion, so we'll start building it out. 

1155 data[subkey] = {} 

1156 # Then prep for next iteration 

1157 data = data[subkey] 

1158 # Exited loop -> data must be the leafmost dict, so we can now set our 

1159 # deleted key to None 

1160 data[key] = None 

1161 self.merge() 

1162 

1163 

1164class AmbiguousMergeError(ValueError): 

1165 pass 

1166 

1167 

1168def merge_dicts( 

1169 base: Dict[str, Any], updates: Dict[str, Any] 

1170) -> Dict[str, Any]: 

1171 """ 

1172 Recursively merge dict ``updates`` into dict ``base`` (mutating ``base``.) 

1173 

1174 * Values which are themselves dicts will be recursed into. 

1175 * Values which are a dict in one input and *not* a dict in the other input 

1176 (e.g. if our inputs were ``{'foo': 5}`` and ``{'foo': {'bar': 5}}``) are 

1177 irreconciliable and will generate an exception. 

1178 * Non-dict leaf values are run through `copy.copy` to avoid state bleed. 

1179 

1180 .. note:: 

1181 This is effectively a lightweight `copy.deepcopy` which offers 

1182 protection from mismatched types (dict vs non-dict) and avoids some 

1183 core deepcopy problems (such as how it explodes on certain object 

1184 types). 

1185 

1186 :returns: 

1187 The value of ``base``, which is mostly useful for wrapper functions 

1188 like `copy_dict`. 

1189 

1190 .. versionadded:: 1.0 

1191 """ 

1192 # TODO: for chrissakes just make it return instead of mutating? 

1193 for key, value in (updates or {}).items(): 

1194 # Dict values whose keys also exist in 'base' -> recurse 

1195 # (But only if both types are dicts.) 

1196 if key in base: 

1197 if isinstance(value, dict): 

1198 if isinstance(base[key], dict): 

1199 merge_dicts(base[key], value) 

1200 else: 

1201 raise _merge_error(base[key], value) 

1202 else: 

1203 if isinstance(base[key], dict): 

1204 raise _merge_error(base[key], value) 

1205 # Fileno-bearing objects are probably 'real' files which do not 

1206 # copy well & must be passed by reference. Meh. 

1207 elif hasattr(value, "fileno"): 

1208 base[key] = value 

1209 else: 

1210 base[key] = copy.copy(value) 

1211 # New values get set anew 

1212 else: 

1213 # Dict values get reconstructed to avoid being references to the 

1214 # updates dict, which can lead to nasty state-bleed bugs otherwise 

1215 if isinstance(value, dict): 

1216 base[key] = copy_dict(value) 

1217 # Fileno-bearing objects are probably 'real' files which do not 

1218 # copy well & must be passed by reference. Meh. 

1219 elif hasattr(value, "fileno"): 

1220 base[key] = value 

1221 # Non-dict values just get set straight 

1222 else: 

1223 base[key] = copy.copy(value) 

1224 return base 

1225 

1226 

1227def _merge_error(orig: object, new: object) -> AmbiguousMergeError: 

1228 return AmbiguousMergeError( 

1229 "Can't cleanly merge {} with {}".format( 

1230 _format_mismatch(orig), _format_mismatch(new) 

1231 ) 

1232 ) 

1233 

1234 

1235def _format_mismatch(x: object) -> str: 

1236 return "{} ({!r})".format(type(x), x) 

1237 

1238 

1239def copy_dict(source: Dict[str, Any]) -> Dict[str, Any]: 

1240 """ 

1241 Return a fresh copy of ``source`` with as little shared state as possible. 

1242 

1243 Uses `merge_dicts` under the hood, with an empty ``base`` dict; see its 

1244 documentation for details on behavior. 

1245 

1246 .. versionadded:: 1.0 

1247 """ 

1248 return merge_dicts({}, source) 

1249 

1250 

1251def excise(dict_: Dict[str, Any], keypath: Tuple[str, ...]) -> None: 

1252 """ 

1253 Remove key pointed at by ``keypath`` from nested dict ``dict_``, if exists. 

1254 

1255 .. versionadded:: 1.0 

1256 """ 

1257 data = dict_ 

1258 keypath_list = list(keypath) 

1259 leaf_key = keypath_list.pop() 

1260 while keypath_list: 

1261 key = keypath_list.pop(0) 

1262 if key not in data: 

1263 # Not there, nothing to excise 

1264 return 

1265 data = data[key] 

1266 if leaf_key in data: 

1267 del data[leaf_key] 

1268 

1269 

1270def obliterate(base: Dict[str, Any], deletions: Dict[str, Any]) -> None: 

1271 """ 

1272 Remove all (nested) keys mentioned in ``deletions``, from ``base``. 

1273 

1274 .. versionadded:: 1.0 

1275 """ 

1276 for key, value in deletions.items(): 

1277 if isinstance(value, dict): 

1278 # NOTE: not testing for whether base[key] exists; if something's 

1279 # listed in a deletions structure, it must exist in some source 

1280 # somewhere, and thus also in the cache being obliterated. 

1281 obliterate(base[key], deletions[key]) 

1282 else: # implicitly None 

1283 del base[key]