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# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21 

22import click 

23import click.testing 

24from contextlib import contextmanager 

25import copy 

26from functools import partial 

27import io 

28import os 

29import textwrap 

30import traceback 

31from unittest.mock import MagicMock, patch 

32import uuid 

33import yaml 

34 

35from .cliLog import CliLog 

36from ..core.utils import iterable 

37 

38 

39# CLI_MOCK_ENV is set by some tests as an environment variable, it 

40# indicates to the cli_handle_exception function that instead of executing the 

41# command implementation function it should use the Mocker class for unit test 

42# verification. 

43mockEnvVarKey = "CLI_MOCK_ENV" 

44mockEnvVar = {mockEnvVarKey: "1"} 

45 

46# This is used as the metavar argument to Options that accept multiple string 

47# inputs, which may be comma-separarated. For example: 

48# --my-opt foo,bar --my-opt baz. 

49# Other arguments to the Option should include multiple=true and 

50# callback=split_kv. 

51typeStrAcceptsMultiple = "TEXT ..." 

52typeStrAcceptsSingle = "TEXT" 

53 

54 

55def textTypeStr(multiple): 

56 """Get the text type string for CLI help documentation. 

57 

58 Parameters 

59 ---------- 

60 multiple : `bool` 

61 True if multiple text values are allowed, False if only one value is 

62 allowed. 

63 

64 Returns 

65 ------- 

66 textTypeStr : `str` 

67 The type string to use. 

68 """ 

69 return typeStrAcceptsMultiple if multiple else typeStrAcceptsSingle 

70 

71 

72# For parameters that support key-value inputs, this defines the separator 

73# for those inputs. 

74split_kv_separator = "=" 

75 

76 

77class Mocker: 

78 

79 mock = MagicMock() 

80 

81 def __init__(self, *args, **kwargs): 

82 """Mocker is a helper class for unit tests. It can be imported and 

83 called and later imported again and call can be verified. 

84 

85 For convenience, constructor arguments are forwarded to the call 

86 function. 

87 """ 

88 self.__call__(*args, **kwargs) 

89 

90 def __call__(self, *args, **kwargs): 

91 """Creates a MagicMock and stores it in a static variable that can 

92 later be verified. 

93 """ 

94 Mocker.mock(*args, **kwargs) 

95 

96 @classmethod 

97 def reset(cls): 

98 cls.mock.reset_mock() 

99 

100 

101class LogCliRunner(click.testing.CliRunner): 

102 """A test runner to use when the logging system will be initialized by code 

103 under test, calls CliLog.resetLog(), which undoes any logging setup that 

104 was done with the CliLog interface. 

105 

106 lsst.log modules can not be set back to an uninitialized state (python 

107 logging modules can be set back to NOTSET), instead they are set to 

108 `CliLog.defaultLsstLogLevel`.""" 

109 

110 def invoke(self, *args, **kwargs): 

111 result = super().invoke(*args, **kwargs) 

112 CliLog.resetLog() 

113 return result 

114 

115 

116def clickResultMsg(result): 

117 """Get a standard assert message from a click result 

118 

119 Parameters 

120 ---------- 

121 result : click.Result 

122 The result object returned from click.testing.CliRunner.invoke 

123 

124 Returns 

125 ------- 

126 msg : `str` 

127 The message string. 

128 """ 

129 msg = f"""\noutput: {result.output}\nexception: {result.exception}""" 

130 if result.exception: 

131 msg += f"""\ntraceback: {"".join(traceback.format_tb(result.exception.__traceback__))}""" 

132 return msg 

133 

134 

135@contextmanager 

136def command_test_env(runner, commandModule, commandName): 

137 """A context manager that creates (and then cleans up) an environment that 

138 provides a CLI plugin command with the given name. 

139 

140 Parameters 

141 ---------- 

142 runner : click.testing.CliRunner 

143 The test runner to use to create the isolated filesystem. 

144 commandModule : `str` 

145 The importable module that the command can be imported from. 

146 commandName : `str` 

147 The name of the command being published to import. 

148 """ 

149 with runner.isolated_filesystem(): 

150 with open("resources.yaml", "w") as f: 

151 f.write(yaml.dump({"cmd": {"import": commandModule, "commands": [commandName]}})) 

152 # Add a colon to the end of the path on the next line, this tests the 

153 # case where the lookup in LoaderCLI._getPluginList generates an empty 

154 # string in one of the list entries and verifies that the empty string 

155 # is properly stripped out. 

156 with patch.dict("os.environ", {"DAF_BUTLER_PLUGINS": f"{os.path.realpath(f.name)}:"}): 

157 yield 

158 

159 

160def addArgumentHelp(doc, helpText): 

161 """Add a Click argument's help message to a function's documentation. 

162 

163 This is needed because click presents arguments in the order the argument 

164 decorators are applied to a function, top down. But, the evaluation of the 

165 decorators happens bottom up, so if arguments just append their help to the 

166 function's docstring, the argument descriptions appear in reverse order 

167 from the order they are applied in. 

168 

169 Parameters 

170 ---------- 

171 doc : `str` 

172 The function's docstring. 

173 helpText : `str` 

174 The argument's help string to be inserted into the function's 

175 docstring. 

176 

177 Returns 

178 ------- 

179 doc : `str` 

180 Updated function documentation. 

181 """ 

182 if doc is None: 182 ↛ 183line 182 didn't jump to line 183, because the condition on line 182 was never true

183 doc = helpText 

184 else: 

185 doclines = doc.splitlines() 

186 doclines.insert(1, helpText) 

187 doclines.insert(1, "\n") 

188 doc = "\n".join(doclines) 

189 return doc 

190 

191 

192def split_commas(context, param, values): 

193 """Process a tuple of values, where each value may contain comma-separated 

194 values, and return a single list of all the passed-in values. 

195 

196 This function can be passed to the 'callback' argument of a click.option to 

197 allow it to process comma-separated values (e.g. "--my-opt a,b,c"). 

198 

199 Parameters 

200 ---------- 

201 context : `click.Context` or `None` 

202 The current execution context. Unused, but Click always passes it to 

203 callbacks. 

204 param : `click.core.Option` or `None` 

205 The parameter being handled. Unused, but Click always passes it to 

206 callbacks. 

207 values : [`str`] 

208 All the values passed for this option. Strings may contain commas, 

209 which will be treated as delimiters for separate values. 

210 

211 Returns 

212 ------- 

213 list of string 

214 The passed in values separated by commas and combined into a single 

215 list. 

216 """ 

217 if values is None: 

218 return values 

219 valueList = [] 

220 for value in iterable(values): 

221 valueList.extend(value.split(",")) 

222 return tuple(valueList) 

223 

224 

225def split_kv(context, param, values, choice=None, multiple=True, normalize=False, separator="=", 

226 unseparated_okay=False, return_type=dict, default_key="", reverse_kv=False): 

227 """Process a tuple of values that are key-value pairs separated by a given 

228 separator. Multiple pairs may be comma separated. Return a dictionary of 

229 all the passed-in values. 

230 

231 This function can be passed to the 'callback' argument of a click.option to 

232 allow it to process comma-separated values (e.g. "--my-opt a=1,b=2"). 

233 

234 Parameters 

235 ---------- 

236 context : `click.Context` or `None` 

237 The current execution context. Unused, but Click always passes it to 

238 callbacks. 

239 param : `click.core.Option` or `None` 

240 The parameter being handled. Unused, but Click always passes it to 

241 callbacks. 

242 values : [`str`] 

243 All the values passed for this option. Strings may contain commas, 

244 which will be treated as delimiters for separate values. 

245 choice : `click.Choice`, optional 

246 If provided, verify each value is a valid choice using the provided 

247 `click.Choice` instance. If None, no verification will be done. By 

248 default None 

249 multiple : `bool`, optional 

250 If true, the value may contain multiple comma-separated values. By 

251 default True. 

252 normalize : `bool`, optional 

253 If True and `choice.case_sensitive == False`, normalize the string the 

254 user provided to match the choice's case. By default False. 

255 separator : str, optional 

256 The character that separates key-value pairs. May not be a comma or an 

257 empty space (for space separators use Click's default implementation 

258 for tuples; `type=(str, str)`). By default "=". 

259 unseparated_okay : `bool`, optional 

260 If True, allow values that do not have a separator. They will be 

261 returned in the values dict as a tuple of values in the key '', that 

262 is: `values[''] = (unseparated_values, )`. By default False. 

263 return_type : `type`, must be `dict` or `tuple` 

264 The type of the value that should be returned. 

265 If `dict` then the returned object will be a dict, for each item in 

266 values, the value to the left of the separator will be the key and the 

267 value to the right of the separator will be the value. 

268 If `tuple` then the returned object will be a tuple. Each item in the 

269 tuple will be 2-item tuple, the first item will be the value to the 

270 left of the separator and the second item will be the value to the 

271 right. By default `dict`. 

272 default_key : `Any` 

273 The key to use if a value is passed that is not a key-value pair. 

274 (Passing values that are not key-value pairs requires 

275 ``unseparated_okay`` to be `True`.) 

276 reverse_kv : bool 

277 If true then for each item in values, the value to the left of the 

278 separator is treated as the value and the value to the right of the 

279 separator is treated as the key. By default False. 

280 

281 Returns 

282 ------- 

283 values : `dict` [`str`, `str`] 

284 The passed-in values in dict form. 

285 

286 Raises 

287 ------ 

288 `click.ClickException` 

289 Raised if the separator is not found in an entry, or if duplicate keys 

290 are encountered. 

291 """ 

292 

293 def norm(val): 

294 """If `normalize` is True and `choice` is not `None`, find the value 

295 in the available choices and return the value as spelled in the 

296 choices. 

297 

298 Assumes that val exists in choices; `split_kv` uses the `choice` 

299 instance to verify val is a valid choice. 

300 """ 

301 if normalize and choice is not None: 

302 v = val.casefold() 

303 for opt in choice.choices: 

304 if opt.casefold() == v: 

305 return opt 

306 return val 

307 

308 class RetDict: 

309 

310 def __init__(self): 

311 self.ret = {} 

312 

313 def add(self, key, val): 

314 if reverse_kv: 

315 key, val = val, key 

316 if key in self.ret: 

317 raise click.ClickException(f"Duplicate entries for '{k}' in '{values}'") 

318 self.ret[key] = val 

319 

320 def get(self): 

321 return self.ret 

322 

323 class RetTuple: 

324 

325 def __init__(self): 

326 self.ret = [] 

327 

328 def add(self, key, val): 

329 if reverse_kv: 

330 key, val = val, key 

331 self.ret.append((key, val)) 

332 

333 def get(self): 

334 return tuple(self.ret) 

335 

336 if separator in (",", " "): 

337 raise RuntimeError(f"'{separator}' is not a supported separator for key-value pairs.") 

338 vals = values # preserve the original argument for error reporting below. 

339 if return_type is dict: 

340 ret = RetDict() 

341 elif return_type is tuple: 

342 ret = RetTuple() 

343 else: 

344 raise click.ClickException(f"Internal error: invalid return type '{return_type}' for split_kv.") 

345 if multiple: 

346 vals = split_commas(context, param, vals) 

347 for val in iterable(vals): 

348 if unseparated_okay and separator not in val: 

349 if choice is not None: 

350 choice(val) # will raise if val is an invalid choice 

351 ret.add(default_key, norm(val)) 

352 else: 

353 try: 

354 k, v = val.split(separator) 

355 if choice is not None: 

356 choice(v) # will raise if val is an invalid choice 

357 except ValueError: 

358 raise click.ClickException( 

359 f"Could not parse key-value pair '{val}' using separator '{separator}', " 

360 f"with multiple values {'allowed' if multiple else 'not allowed'}.") 

361 ret.add(k, norm(v)) 

362 return ret.get() 

363 

364 

365def to_upper(context, param, value): 

366 """Convert a value to upper case. 

367 

368 Parameters 

369 ---------- 

370 context : click.Context 

371 

372 values : string 

373 The value to be converted. 

374 

375 Returns 

376 ------- 

377 string 

378 A copy of the passed-in value, converted to upper case. 

379 """ 

380 return value.upper() 

381 

382 

383def unwrap(val): 

384 """Remove newlines and leading whitespace from a multi-line string with 

385 a consistent indentation level. 

386 

387 The first line of the string may be only a newline or may contain text 

388 followed by a newline, either is ok. After the first line, each line must 

389 begin with a consistant amount of whitespace. So, content of a 

390 triple-quoted string may begin immediately after the quotes, or the string 

391 may start with a newline. Each line after that must be the same amount of 

392 indentation/whitespace followed by text and a newline. The last line may 

393 end with a new line but is not required to do so. 

394 

395 Parameters 

396 ---------- 

397 val : `str` 

398 The string to change. 

399 

400 Returns 

401 ------- 

402 strippedString : `str` 

403 The string with newlines, indentation, and leading and trailing 

404 whitespace removed. 

405 """ 

406 def splitSection(val): 

407 if not val.startswith("\n"): 407 ↛ 411line 407 didn't jump to line 411, because the condition on line 407 was never false

408 firstLine, _, val = val.partition("\n") 

409 firstLine += " " 

410 else: 

411 firstLine = "" 

412 return (firstLine + textwrap.dedent(val).replace("\n", " ")).strip() 

413 

414 return "\n\n".join([splitSection(s) for s in val.split("\n\n")]) 

415 

416 

417def cli_handle_exception(func, *args, **kwargs): 

418 """Wrap a function call in an exception handler that raises a 

419 ClickException if there is an Exception. 

420 

421 Also provides support for unit testing by testing for an environment 

422 variable, and if it is present prints the function name, args, and kwargs 

423 to stdout so they can be read and verified by the unit test code. 

424 

425 Parameters 

426 ---------- 

427 func : function 

428 A function to be called and exceptions handled. Will pass args & kwargs 

429 to the function. 

430 

431 Returns 

432 ------- 

433 The result of calling func. 

434 

435 Raises 

436 ------ 

437 click.ClickException 

438 An exception to be handled by the Click CLI tool. 

439 """ 

440 if mockEnvVarKey in os.environ: 

441 Mocker(*args, **kwargs) 

442 return 

443 try: 

444 return func(*args, **kwargs) 

445 except Exception: 

446 msg = io.StringIO() 

447 msg.write("An error occurred during command execution:\n") 

448 traceback.print_exc(file=msg) 

449 raise click.ClickException(msg.getvalue()) 

450 

451 

452class option_section: # noqa: N801 

453 """Decorator to add a section label between options in the help text of a 

454 command. 

455 

456 Parameters 

457 ---------- 

458 sectionText : `str` 

459 The text to print in the section identifier. 

460 """ 

461 

462 def __init__(self, sectionText): 

463 self.sectionText = "\n" + sectionText 

464 

465 def __call__(self, f): 

466 # Generate a parameter declaration that will be unique for this 

467 # section. 

468 return click.option(f"--option-section-{str(uuid.uuid4())}", 

469 sectionText=self.sectionText, 

470 cls=OptionSection)(f) 

471 

472 

473class MWPath(click.Path): 

474 """Overrides click.Path to implement file-does-not-exist checking. 

475 

476 Changes the definition of ``exists` so that `True` indicates the location 

477 (file or directory) must exist, `False` indicates the location must *not* 

478 exist, and `None` indicates that the file may exist or not. The standard 

479 definition for the `click.Path` ``exists`` parameter is that for `True` a 

480 location must exist, but `False` means it is not required to exist (not 

481 that it is required to not exist). 

482 

483 Parameters 

484 ---------- 

485 exists : `True`, `False`, or `None` 

486 If `True`, the location (file or directory) indicated by the caller 

487 must exist. If `False` the location must not exist. If `None`, the 

488 location may exist or not. 

489 

490 For other parameters see `click.Path`. 

491 """ 

492 

493 def __init__(self, exists=None, file_okay=True, dir_okay=True, 

494 writable=False, readable=True, resolve_path=False, 

495 allow_dash=False, path_type=None): 

496 self.mustNotExist = exists is False 

497 if exists is None: 

498 exists = False 

499 super().__init__(exists, file_okay, dir_okay, writable, readable, 

500 resolve_path, allow_dash, path_type) 

501 

502 def convert(self, value, param, ctx): 

503 """Called by click.ParamType to "convert values through types". 

504 `click.Path` uses this step to verify Path conditions.""" 

505 if self.mustNotExist and os.path.exists(value): 

506 self.fail(f'{self.path_type} "{value}" should not exist.') 

507 return super().convert(value, param, ctx) 

508 

509 

510class MWOption(click.Option): 

511 """Overrides click.Option with desired behaviors.""" 

512 

513 def __init__(self, *args, forward=False, **kwargs): 

514 # `forward` indicates weather a subcommand should forward the value of 

515 # this option to the next subcommand or not. 

516 self.forward = forward 

517 super().__init__(*args, **kwargs) 

518 

519 def make_metavar(self): 

520 """Overrides `click.Option.make_metavar`. Makes the metavar for the 

521 help menu. Adds a space and an elipsis after the metavar name if 

522 the option accepts multiple inputs, otherwise defers to the base 

523 implementation. 

524 

525 By default click does not add an elipsis when multiple is True and 

526 nargs is 1. And when nargs does not equal 1 click adds an elipsis 

527 without a space between the metavar and the elipsis, but we prefer a 

528 space between. 

529 

530 Does not get called for some option types (e.g. flag) so metavar 

531 transformation that must apply to all types should be applied in 

532 get_help_record. 

533 """ 

534 metavar = super().make_metavar() 

535 if self.multiple and self.nargs == 1: 

536 metavar += " ..." 

537 elif self.nargs != 1: 

538 metavar = f"{metavar[:-3]} ..." 

539 return metavar 

540 

541 def get_help_record(self, ctx): 

542 """Overrides `click.Option.get_help_record`. Adds "(f)" to this 

543 option's metavar text if its associated subcommand forwards this option 

544 value to the next subcommand.""" 

545 rv = super().get_help_record(ctx) 

546 if self.forward: 

547 rv = (rv[0] + " (f)",) + rv[1:] 

548 return rv 

549 

550 

551class MWArgument(click.Argument): 

552 """Overrides click.Argument with desired behaviors.""" 

553 

554 def make_metavar(self): 

555 """Overrides `click.Option.make_metavar`. Makes the metavar for the 

556 help menu. Always adds a space and an elipsis (' ...') after the 

557 metavar name if the option accepts multiple inputs. 

558 

559 By default click adds an elipsis without a space between the metavar 

560 and the elipsis, but we prefer a space between. 

561 

562 Returns 

563 ------- 

564 metavar : `str` 

565 The metavar value. 

566 """ 

567 metavar = super().make_metavar() 

568 if self.nargs != 1: 

569 metavar = f"{metavar[:-3]} ..." 

570 return metavar 

571 

572 

573class OptionSection(MWOption): 

574 """Implements an Option that prints a section label in the help text and 

575 does not pass any value to the command function. 

576 

577 This class does a bit of hackery to add a section label to a click command 

578 help output: first, `expose_value` is set to `False` so that no value is 

579 passed to the command function. Second, this class overrides 

580 `click.Option.get_help_record` to return the section label string without 

581 any prefix so that it stands out as a section label. 

582 

583 The intention for this implementation is to do minimally invasive overrides 

584 of the click classes so as to be robust and easy to fix if the click 

585 internals change. 

586 

587 Parameters 

588 ---------- 

589 sectionName : `str` 

590 The parameter declaration for this option. It is not shown to the user, 

591 it must be unique within the command. If using the `section` decorator 

592 to add a section to a command's options, the section name is 

593 auto-generated. 

594 sectionText : `str` 

595 The text to print in the section identifier. 

596 """ 

597 

598 def __init__(self, sectionName, sectionText): 

599 super().__init__(sectionName, expose_value=False) 

600 self.sectionText = sectionText 

601 

602 def get_help_record(self, ctx): 

603 return (self.sectionText, "") 

604 

605 

606class MWOptionDecorator: 

607 """Wraps the click.option decorator to enable shared options to be declared 

608 and allows inspection of the shared option. 

609 """ 

610 

611 def __init__(self, *param_decls, **kwargs): 

612 forward = kwargs.pop("forward", False) 

613 self.partialOpt = partial(click.option, *param_decls, cls=partial(MWOption, forward=forward), 

614 **kwargs) 

615 opt = click.Option(param_decls, **kwargs) 

616 self._name = opt.name 

617 self._opts = opt.opts 

618 

619 def name(self): 

620 """Get the name that will be passed to the command function for this 

621 option.""" 

622 return self._name 

623 

624 def opts(self): 

625 """Get the flags that will be used for this option on the command 

626 line.""" 

627 return self._opts 

628 

629 def __call__(self, *args, **kwargs): 

630 return self.partialOpt(*args, **kwargs) 

631 

632 

633class MWArgumentDecorator: 

634 """Wraps the click.argument decorator to enable shared arguments to be 

635 declared. """ 

636 

637 def __init__(self, *param_decls, **kwargs): 

638 self._helpText = kwargs.pop("help", None) 

639 self.partialArg = partial(click.argument, *param_decls, cls=MWArgument, **kwargs) 

640 

641 def __call__(self, *args, help=None, **kwargs): 

642 def decorator(f): 

643 if help is not None: 

644 self._helpText = help 

645 if self._helpText: 645 ↛ 647line 645 didn't jump to line 647, because the condition on line 645 was never false

646 f.__doc__ = addArgumentHelp(f.__doc__, self._helpText) 

647 return self.partialArg(*args, **kwargs)(f) 

648 return decorator 

649 

650 

651class MWCommand(click.Command): 

652 """Command subclass that stores a copy of the args list for use by the 

653 command.""" 

654 

655 def parse_args(self, ctx, args): 

656 MWCtxObj.getFrom(ctx).args = copy.copy(args) 

657 super().parse_args(ctx, args) 

658 

659 

660class MWCtxObj(): 

661 """Helper object for managing the `click.Context.obj` parameter, allows 

662 obj data to be managed in a consistent way. 

663 

664 `Context.obj` defaults to None. `MWCtxObj.getFrom(ctx)` can be used to 

665 initialize the obj if needed and return a new or existing MWCtxObj. 

666 

667 Attributes 

668 ---------- 

669 args : `list` [`str`] 

670 The list of arguments (argument values, option flags, and option 

671 values), split using whitespace, that were passed in on the command 

672 line for the subcommand represented by the parent context object. 

673 """ 

674 

675 def __init__(self): 

676 

677 self.args = None 

678 

679 @staticmethod 

680 def getFrom(ctx): 

681 """If needed, initialize `ctx.obj` with a new MWCtxObj, and return the 

682 new or already existing MWCtxObj.""" 

683 if ctx.obj is not None: 

684 return ctx.obj 

685 ctx.obj = MWCtxObj() 

686 return ctx.obj 

687 

688 

689class ForwardOptions: 

690 """Captures CLI options to be forwarded from one subcommand to future 

691 subcommands executed as part of a single CLI command invocation 

692 (called "chained commands"). 

693 

694 Attributes 

695 ---------- 

696 functionKwargs : `dict` [`str`, `str`] 

697 The cached kwargs (argument name and argument value) from subcommands 

698 that were called with values passed in as options on the command line. 

699 """ 

700 

701 def __init__(self): 

702 self.functionKwargs = {} 

703 

704 @staticmethod 

705 def _getKwargsToSave(mwOptions, arguments, **kwargs): 

706 """Get kwargs that should be cached for use by subcommands invoked in 

707 the future. 

708 

709 Parameters 

710 ---------- 

711 mwOptions : `list` or `tuple` [`MWOption`] 

712 The options supported by the current command. For each option, its 

713 kwarg will be cached if the option's `forward` parameter is `True`. 

714 arguments : `list` [`str`] 

715 The arguments that were passed in on the command line for the 

716 current subcommand, split on whitespace into a list of arguments, 

717 option flags, and option values. 

718 

719 Returns 

720 ------- 

721 `dict` [`str`, `str`] 

722 The kwargs that should be cached. 

723 """ 

724 saveableOptions = [opt for opt in mwOptions if opt.forward] 

725 argumentSet = set(arguments) 

726 passedOptions = [opt for opt in saveableOptions if set(opt.opts).intersection(argumentSet)] 

727 return {opt.name: kwargs[opt.name] for opt in passedOptions} 

728 

729 def _getKwargsToUse(self, mwOptions, arguments, **kwargs): 

730 """Get kwargs that should be used by the current subcommand. 

731 

732 Parameters 

733 ---------- 

734 mwOptions : `list` or `tuple` [`MWOption`] 

735 The options supported by the current subcommand. 

736 arguments : `list` [`str`] 

737 The arguments that were passed in on the command line for the 

738 current subcommand, split on whitespace into a list of arguments, 

739 option flags, and option values. 

740 

741 Returns 

742 ------- 

743 `dict` [`str`, `str`] 

744 kwargs that add the cached kwargs accepted by the current command 

745 to the kwargs that were provided by CLI arguments. When a kwarg is 

746 present in both places, the CLI argument kwarg value is used. 

747 """ 

748 argumentSet = set(arguments) 

749 # get the cached value for options that were not passed in as CLI 

750 # arguments: 

751 cachedValues = {opt.name: self.functionKwargs[opt.name] for opt in mwOptions 

752 if not set(opt.opts).intersection(argumentSet) and opt.name in self.functionKwargs} 

753 updatedKwargs = copy.copy(kwargs) 

754 updatedKwargs.update(cachedValues) 

755 return updatedKwargs 

756 

757 def _save(self, mwOptions, arguments, **kwargs): 

758 """Save the current forwardable CLI options. 

759 

760 Parameters 

761 ---------- 

762 mwOptions : `list` or `tuple` [`MWOption`] 

763 The options, which are accepted by the current command, that may be 

764 saved. 

765 arguments : `list` [`str`] 

766 The arguments that were passed in on the command line for the 

767 current subcommand, split on whitespace into a list of arguments, 

768 option flags, and option values. 

769 kwargs : `dict` [`str`, `str`] 

770 Arguments that were passed into a command function. Indicated 

771 option arguments will be extracted and cached. 

772 """ 

773 self.functionKwargs.update(self._getKwargsToSave(mwOptions, arguments, **kwargs)) 

774 

775 def update(self, mwOptions, arguments, **kwargs): 

776 """Update what options are forwarded, drop non-forwarded options, and 

777 update cached kwargs with new values in kwargs, and returns a new dict 

778 that adds cached kwargs to the passed-in kwargs. 

779 

780 Parameters 

781 ---------- 

782 mwOptions : `list` or `tuple` [`MWOption`] 

783 The options that will be forwarded. 

784 arguments : `list` [`str`] 

785 The arguments that were passed in on the command line for the 

786 current subcommand, split on whitespace into a list of arguments, 

787 option flags, and option values. 

788 kwargs : `dict` [`str`, `str`] 

789 Arguments that were passed into a command function. A new dict will 

790 be created and returned that adds cached kwargs to the passed-in 

791 non-default-value kwargs. 

792 

793 Returns 

794 ------- 

795 kwargs : dict [`str`, `str`] 

796 kwargs that add the cached kwargs accepted by the current command 

797 to the kwargs that were provided by CLI arguments. When a kwarg is 

798 present in both places, the CLI argument kwarg value is used. 

799 """ 

800 kwargsToUse = self._getKwargsToUse(mwOptions, arguments, **kwargs) 

801 self._save(mwOptions, arguments, **kwargs) 

802 return kwargsToUse