Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/traitlets/config/configurable.py: 23%

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

257 statements  

1"""A base class for objects that are configurable.""" 

2 

3# Copyright (c) IPython Development Team. 

4# Distributed under the terms of the Modified BSD License. 

5from __future__ import annotations 

6 

7import logging 

8import typing as t 

9from copy import deepcopy 

10from textwrap import dedent 

11 

12from traitlets.traitlets import ( 

13 Any, 

14 Container, 

15 Dict, 

16 HasTraits, 

17 Instance, 

18 TraitType, 

19 default, 

20 observe, 

21 observe_compat, 

22 validate, 

23) 

24from traitlets.utils import warnings 

25from traitlets.utils.bunch import Bunch 

26from traitlets.utils.text import indent, wrap_paragraphs 

27 

28from .loader import Config, DeferredConfig, LazyConfigValue, _is_section_key 

29 

30if t.TYPE_CHECKING: 

31 from typing_extensions import Self 

32 

33# ----------------------------------------------------------------------------- 

34# Helper classes for Configurables 

35# ----------------------------------------------------------------------------- 

36 

37if t.TYPE_CHECKING: 

38 LoggerType = logging.Logger | logging.LoggerAdapter[t.Any] 

39else: 

40 LoggerType = t.Any 

41 

42 

43class ConfigurableError(Exception): 

44 pass 

45 

46 

47class MultipleInstanceError(ConfigurableError): 

48 pass 

49 

50 

51# ----------------------------------------------------------------------------- 

52# Configurable implementation 

53# ----------------------------------------------------------------------------- 

54 

55 

56class Configurable(HasTraits): 

57 config = Instance(Config, (), {}) 

58 parent = Instance("traitlets.config.configurable.Configurable", allow_none=True) 

59 

60 def __init__(self, **kwargs: t.Any) -> None: 

61 """Create a configurable given a config config. 

62 

63 Parameters 

64 ---------- 

65 config : Config 

66 If this is empty, default values are used. If config is a 

67 :class:`Config` instance, it will be used to configure the 

68 instance. 

69 parent : Configurable instance, optional 

70 The parent Configurable instance of this object. 

71 

72 Notes 

73 ----- 

74 Subclasses of Configurable must call the :meth:`__init__` method of 

75 :class:`Configurable` *before* doing anything else and using 

76 :func:`super`:: 

77 

78 class MyConfigurable(Configurable): 

79 def __init__(self, config=None): 

80 super(MyConfigurable, self).__init__(config=config) 

81 # Then any other code you need to finish initialization. 

82 

83 This ensures that instances will be configured properly. 

84 """ 

85 parent = kwargs.pop("parent", None) 

86 if parent is not None: 

87 # config is implied from parent 

88 if kwargs.get("config") is None: 

89 kwargs["config"] = parent.config 

90 self.parent = parent 

91 

92 config = kwargs.pop("config", None) 

93 

94 # load kwarg traits, other than config 

95 super().__init__(**kwargs) 

96 

97 # record traits set by config 

98 config_override_names = set() 

99 

100 def notice_config_override(change: Bunch) -> None: 

101 """Record traits set by both config and kwargs. 

102 

103 They will need to be overridden again after loading config. 

104 """ 

105 if change.name in kwargs: 

106 config_override_names.add(change.name) 

107 

108 self.observe(notice_config_override) 

109 

110 # load config 

111 if config is not None: 

112 # We used to deepcopy, but for now we are trying to just save 

113 # by reference. This *could* have side effects as all components 

114 # will share config. In fact, I did find such a side effect in 

115 # _config_changed below. If a config attribute value was a mutable type 

116 # all instances of a component were getting the same copy, effectively 

117 # making that a class attribute. 

118 # self.config = deepcopy(config) 

119 self.config = config 

120 else: 

121 # allow _config_default to return something 

122 self._load_config(self.config) 

123 self.unobserve(notice_config_override) 

124 

125 for name in config_override_names: 

126 setattr(self, name, kwargs[name]) 

127 

128 # ------------------------------------------------------------------------- 

129 # Static trait notifications 

130 # ------------------------------------------------------------------------- 

131 

132 @classmethod 

133 def section_names(cls) -> list[str]: 

134 """return section names as a list""" 

135 return [ 

136 c.__name__ 

137 for c in reversed(cls.__mro__) 

138 if issubclass(c, Configurable) and issubclass(cls, c) 

139 ] 

140 

141 def _find_my_config(self, cfg: Config) -> t.Any: 

142 """extract my config from a global Config object 

143 

144 will construct a Config object of only the config values that apply to me 

145 based on my mro(), as well as those of my parent(s) if they exist. 

146 

147 If I am Bar and my parent is Foo, and their parent is Tim, 

148 this will return merge following config sections, in this order:: 

149 

150 [Bar, Foo.Bar, Tim.Foo.Bar] 

151 

152 With the last item being the highest priority. 

153 """ 

154 cfgs = [cfg] 

155 if self.parent: 

156 cfgs.append(self.parent._find_my_config(cfg)) 

157 my_config = Config() 

158 for c in cfgs: 

159 for sname in self.section_names(): 

160 # Don't do a blind getattr as that would cause the config to 

161 # dynamically create the section with name Class.__name__. 

162 if c._has_section(sname): 

163 my_config.merge(c[sname]) 

164 return my_config 

165 

166 def _load_config( 

167 self, 

168 cfg: Config, 

169 section_names: list[str] | None = None, 

170 traits: dict[str, TraitType[t.Any, t.Any]] | None = None, 

171 ) -> None: 

172 """load traits from a Config object""" 

173 

174 if traits is None: 

175 traits = self.traits(config=True) 

176 if section_names is None: 

177 section_names = self.section_names() 

178 

179 my_config = self._find_my_config(cfg) 

180 

181 # hold trait notifications until after all config has been loaded 

182 with self.hold_trait_notifications(): 

183 for name, config_value in my_config.items(): 

184 if name in traits: 

185 if isinstance(config_value, LazyConfigValue): 

186 # ConfigValue is a wrapper for using append / update on containers 

187 # without having to copy the initial value 

188 initial = getattr(self, name) 

189 config_value = config_value.get_value(initial) 

190 elif isinstance(config_value, DeferredConfig): 

191 # DeferredConfig tends to come from CLI/environment variables 

192 config_value = config_value.get_value(traits[name]) 

193 # We have to do a deepcopy here if we don't deepcopy the entire 

194 # config object. If we don't, a mutable config_value will be 

195 # shared by all instances, effectively making it a class attribute. 

196 setattr(self, name, deepcopy(config_value)) 

197 elif not _is_section_key(name) and not isinstance(config_value, Config): 

198 from difflib import get_close_matches 

199 

200 if isinstance(self, LoggingConfigurable): 

201 assert self.log is not None 

202 warn = self.log.warning 

203 else: 

204 

205 def warn(msg: t.Any) -> None: 

206 return warnings.warn(msg, UserWarning, stacklevel=9) 

207 

208 matches = get_close_matches(name, traits) 

209 msg = f"Config option `{name}` not recognized by `{self.__class__.__name__}`." 

210 

211 if len(matches) == 1: 

212 msg += f" Did you mean `{matches[0]}`?" 

213 elif len(matches) >= 1: 

214 msg += " Did you mean one of: `{matches}`?".format( 

215 matches=", ".join(sorted(matches)) 

216 ) 

217 warn(msg) 

218 

219 @observe("config") 

220 @observe_compat 

221 def _config_changed(self, change: Bunch) -> None: 

222 """Update all the class traits having ``config=True`` in metadata. 

223 

224 For any class trait with a ``config`` metadata attribute that is 

225 ``True``, we update the trait with the value of the corresponding 

226 config entry. 

227 """ 

228 # Get all traits with a config metadata entry that is True 

229 traits = self.traits(config=True) 

230 

231 # We auto-load config section for this class as well as any parent 

232 # classes that are Configurable subclasses. This starts with Configurable 

233 # and works down the mro loading the config for each section. 

234 section_names = self.section_names() 

235 self._load_config(change.new, traits=traits, section_names=section_names) 

236 

237 def update_config(self, config: Config) -> None: 

238 """Update config and load the new values""" 

239 # traitlets prior to 4.2 created a copy of self.config in order to trigger change events. 

240 # Some projects (IPython < 5) relied upon one side effect of this, 

241 # that self.config prior to update_config was not modified in-place. 

242 # For backward-compatibility, we must ensure that self.config 

243 # is a new object and not modified in-place, 

244 # but config consumers should not rely on this behavior. 

245 self.config = deepcopy(self.config) 

246 # load config 

247 self._load_config(config) 

248 # merge it into self.config 

249 self.config.merge(config) 

250 # TODO: trigger change event if/when dict-update change events take place 

251 # DO NOT trigger full trait-change 

252 

253 @classmethod 

254 def class_get_help(cls, inst: HasTraits | None = None) -> str: 

255 """Get the help string for this class in ReST format. 

256 

257 If `inst` is given, its current trait values will be used in place of 

258 class defaults. 

259 """ 

260 assert inst is None or isinstance(inst, cls) 

261 final_help = [] 

262 base_classes = ", ".join(p.__name__ for p in cls.__bases__) 

263 final_help.append(f"{cls.__name__}({base_classes}) options") 

264 final_help.append(len(final_help[0]) * "-") 

265 for _, v in sorted(cls.class_traits(config=True).items()): 

266 help = cls.class_get_trait_help(v, inst) 

267 final_help.append(help) 

268 return "\n".join(final_help) 

269 

270 @classmethod 

271 def class_get_trait_help( 

272 cls, 

273 trait: TraitType[t.Any, t.Any], 

274 inst: HasTraits | None = None, 

275 helptext: str | None = None, 

276 ) -> str: 

277 """Get the helptext string for a single trait. 

278 

279 :param inst: 

280 If given, its current trait values will be used in place of 

281 the class default. 

282 :param helptext: 

283 If not given, uses the `help` attribute of the current trait. 

284 """ 

285 assert inst is None or isinstance(inst, cls) 

286 lines = [] 

287 header = f"--{cls.__name__}.{trait.name}" 

288 if isinstance(trait, (Container, Dict)): 

289 multiplicity = trait.metadata.get("multiplicity", "append") 

290 if isinstance(trait, Dict): 

291 sample_value = "<key-1>=<value-1>" 

292 else: 

293 sample_value = f"<{trait.__class__.__name__.lower()}-item-1>" 

294 if multiplicity == "append": 

295 header = f"{header}={sample_value}..." 

296 else: 

297 header = f"{header} {sample_value}..." 

298 else: 

299 header = f"{header}=<{trait.__class__.__name__}>" 

300 # header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__) 

301 lines.append(header) 

302 

303 if helptext is None: 

304 helptext = trait.help 

305 if helptext != "": 

306 helptext = "\n\n".join(wrap_paragraphs(helptext, 76)) 

307 lines.append(indent(helptext)) 

308 

309 if "Enum" in trait.__class__.__name__: 

310 # include Enum choices 

311 lines.append(indent(f"Choices: {trait.info()}")) 

312 

313 if inst is not None: 

314 lines.append(indent(f"Current: {getattr(inst, trait.name or '')!r}")) 

315 else: 

316 try: 

317 dvr = trait.default_value_repr() 

318 except Exception: 

319 dvr = None # ignore defaults we can't construct 

320 if dvr is not None: 

321 if len(dvr) > 64: 

322 dvr = dvr[:61] + "..." 

323 lines.append(indent(f"Default: {dvr}")) 

324 

325 return "\n".join(lines) 

326 

327 @classmethod 

328 def class_print_help(cls, inst: HasTraits | None = None) -> None: 

329 """Get the help string for a single trait and print it.""" 

330 print(cls.class_get_help(inst)) # noqa: T201 

331 

332 @classmethod 

333 def _defining_class( 

334 cls, trait: TraitType[t.Any, t.Any], classes: t.Sequence[type[HasTraits]] 

335 ) -> type[Configurable]: 

336 """Get the class that defines a trait 

337 

338 For reducing redundant help output in config files. 

339 Returns the current class if: 

340 - the trait is defined on this class, or 

341 - the class where it is defined would not be in the config file 

342 

343 Parameters 

344 ---------- 

345 trait : Trait 

346 The trait to look for 

347 classes : list 

348 The list of other classes to consider for redundancy. 

349 Will return `cls` even if it is not defined on `cls` 

350 if the defining class is not in `classes`. 

351 """ 

352 defining_cls = cls 

353 assert trait.name is not None 

354 for parent in cls.mro(): 

355 if ( 

356 issubclass(parent, Configurable) 

357 and parent in classes 

358 and parent.class_own_traits(config=True).get(trait.name, None) is trait 

359 ): 

360 defining_cls = parent 

361 return defining_cls 

362 

363 @classmethod 

364 def class_config_section(cls, classes: t.Sequence[type[HasTraits]] | None = None) -> str: 

365 """Get the config section for this class. 

366 

367 Parameters 

368 ---------- 

369 classes : list, optional 

370 The list of other classes in the config file. 

371 Used to reduce redundant information. 

372 """ 

373 

374 def c(s: str) -> str: 

375 """return a commented, wrapped block.""" 

376 s = "\n\n".join(wrap_paragraphs(s, 78)) 

377 

378 return "## " + s.replace("\n", "\n# ") 

379 

380 # section header 

381 breaker = "#" + "-" * 78 

382 parent_classes = ", ".join(p.__name__ for p in cls.__bases__ if issubclass(p, Configurable)) 

383 

384 s = f"# {cls.__name__}({parent_classes}) configuration" 

385 lines = [breaker, s, breaker] 

386 # get the description trait 

387 desc = cls.class_traits().get("description") 

388 if desc: 

389 desc = desc.default_value 

390 if not desc: 

391 # no description from trait, use __doc__ 

392 desc = getattr(cls, "__doc__", "") # type:ignore[arg-type] 

393 if desc: 

394 lines.append(c(desc)) # type:ignore[arg-type] 

395 lines.append("") 

396 

397 for name, trait in sorted(cls.class_traits(config=True).items()): 

398 default_repr = trait.default_value_repr() 

399 

400 if classes: 

401 defining_class = cls._defining_class(trait, classes) 

402 else: 

403 defining_class = cls 

404 if defining_class is cls: 

405 # cls owns the trait, show full help 

406 if trait.help: 

407 lines.append(c(trait.help)) 

408 if "Enum" in type(trait).__name__: 

409 # include Enum choices 

410 lines.append(f"# Choices: {trait.info()}") 

411 lines.append(f"# Default: {default_repr}") 

412 else: 

413 # Trait appears multiple times and isn't defined here. 

414 # Truncate help to first line + "See also Original.trait" 

415 if trait.help: 

416 lines.append(c(trait.help.split("\n", 1)[0])) 

417 lines.append(f"# See also: {defining_class.__name__}.{name}") 

418 

419 lines.append(f"# c.{cls.__name__}.{name} = {default_repr}") 

420 lines.append("") 

421 return "\n".join(lines) 

422 

423 @classmethod 

424 def class_config_rst_doc(cls) -> str: 

425 """Generate rST documentation for this class' config options. 

426 

427 Excludes traits defined on parent classes. 

428 """ 

429 lines = [] 

430 classname = cls.__name__ 

431 for _, trait in sorted(cls.class_traits(config=True).items()): 

432 ttype = trait.__class__.__name__ 

433 

434 if not trait.name: 

435 continue 

436 termline = classname + "." + trait.name 

437 

438 # Choices or type 

439 if "Enum" in ttype: 

440 # include Enum choices 

441 termline += " : " + trait.info_rst() # type:ignore[attr-defined] 

442 else: 

443 termline += " : " + ttype 

444 lines.append(termline) 

445 

446 # Default value 

447 try: 

448 dvr = trait.default_value_repr() 

449 except Exception: 

450 dvr = None # ignore defaults we can't construct 

451 if dvr is not None: 

452 if len(dvr) > 64: 

453 dvr = dvr[:61] + "..." 

454 # Double up backslashes, so they get to the rendered docs 

455 dvr = dvr.replace("\\n", "\\\\n") 

456 lines.append(indent(f"Default: ``{dvr}``")) 

457 lines.append("") 

458 

459 help = trait.help or "No description" 

460 lines.append(indent(dedent(help))) 

461 

462 # Blank line 

463 lines.append("") 

464 

465 return "\n".join(lines) 

466 

467 

468class LoggingConfigurable(Configurable): 

469 """A parent class for Configurables that log. 

470 

471 Subclasses have a log trait, and the default behavior 

472 is to get the logger from the currently running Application. 

473 """ 

474 

475 log = Any(help="Logger or LoggerAdapter instance", allow_none=False) 

476 

477 @validate("log") 

478 def _validate_log(self, proposal: Bunch) -> LoggerType: 

479 if not isinstance(proposal.value, (logging.Logger, logging.LoggerAdapter)): 

480 # warn about unsupported type, but be lenient to allow for duck typing 

481 warnings.warn( 

482 f"{self.__class__.__name__}.log should be a Logger or LoggerAdapter," 

483 f" got {proposal.value}.", 

484 UserWarning, 

485 stacklevel=2, 

486 ) 

487 return t.cast(LoggerType, proposal.value) 

488 

489 @default("log") 

490 def _log_default(self) -> LoggerType: 

491 if isinstance(self.parent, LoggingConfigurable): 

492 assert self.parent is not None 

493 return t.cast(logging.Logger, self.parent.log) 

494 from traitlets import log 

495 

496 return log.get_logger() 

497 

498 def _get_log_handler(self) -> logging.Handler | None: 

499 """Return the default Handler 

500 

501 Returns None if none can be found 

502 

503 Deprecated, this now returns the first log handler which may or may 

504 not be the default one. 

505 """ 

506 if not self.log: 

507 return None 

508 logger: logging.Logger = ( 

509 self.log if isinstance(self.log, logging.Logger) else self.log.logger 

510 ) 

511 if not getattr(logger, "handlers", None): 

512 # no handlers attribute or empty handlers list 

513 return None 

514 return logger.handlers[0] 

515 

516 

517class SingletonConfigurable(LoggingConfigurable): 

518 """A configurable that only allows one instance. 

519 

520 This class is for classes that should only have one instance of itself 

521 or *any* subclass. To create and retrieve such a class use the 

522 :meth:`SingletonConfigurable.instance` method. 

523 """ 

524 

525 _instance = None 

526 

527 @classmethod 

528 def _walk_mro(cls) -> t.Generator[type[SingletonConfigurable], None, None]: 

529 """Walk the cls.mro() for parent classes that are also singletons 

530 

531 For use in instance() 

532 """ 

533 

534 for subclass in cls.mro(): 

535 if ( 

536 issubclass(cls, subclass) 

537 and issubclass(subclass, SingletonConfigurable) 

538 and subclass != SingletonConfigurable 

539 ): 

540 yield subclass 

541 

542 @classmethod 

543 def clear_instance(cls) -> None: 

544 """unset _instance for this class and singleton parents.""" 

545 if not cls.initialized(): 

546 return 

547 for subclass in cls._walk_mro(): 

548 if isinstance(subclass._instance, cls): 

549 # only clear instances that are instances 

550 # of the calling class 

551 subclass._instance = None # type:ignore[unreachable] 

552 

553 @classmethod 

554 def instance(cls, *args: t.Any, **kwargs: t.Any) -> Self: 

555 """Returns a global instance of this class. 

556 

557 This method create a new instance if none have previously been created 

558 and returns a previously created instance is one already exists. 

559 

560 The arguments and keyword arguments passed to this method are passed 

561 on to the :meth:`__init__` method of the class upon instantiation. 

562 

563 Examples 

564 -------- 

565 Create a singleton class using instance, and retrieve it:: 

566 

567 >>> from traitlets.config.configurable import SingletonConfigurable 

568 >>> class Foo(SingletonConfigurable): pass 

569 >>> foo = Foo.instance() 

570 >>> foo == Foo.instance() 

571 True 

572 

573 Create a subclass that is retrieved using the base class instance:: 

574 

575 >>> class Bar(SingletonConfigurable): pass 

576 >>> class Bam(Bar): pass 

577 >>> bam = Bam.instance() 

578 >>> bam == Bar.instance() 

579 True 

580 """ 

581 # Create and save the instance 

582 if cls._instance is None: 

583 inst = cls(*args, **kwargs) 

584 # Now make sure that the instance will also be returned by 

585 # parent classes' _instance attribute. 

586 for subclass in cls._walk_mro(): 

587 subclass._instance = inst 

588 

589 if isinstance(cls._instance, cls): 

590 return cls._instance 

591 else: 

592 raise MultipleInstanceError( 

593 f"An incompatible sibling of '{cls.__name__}' is already instantiated" 

594 f" as singleton: {type(cls._instance).__name__}" 

595 ) 

596 

597 @classmethod 

598 def initialized(cls) -> bool: 

599 """Has an instance been created?""" 

600 return hasattr(cls, "_instance") and cls._instance is not None