Coverage for pyTooling/CLIAbstraction/Argument.py: 88%

197 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-23 23:21 +0000

1# ==================================================================================================================== # 

2# _____ _ _ ____ _ ___ _ _ _ _ _ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| | |_ _| / \ | |__ ___| |_ _ __ __ _ ___| |_(_) ___ _ __ # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | | | | | / _ \ | '_ \/ __| __| '__/ _` |/ __| __| |/ _ \| '_ \ # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| || |___| |___ | | / ___ \| |_) \__ \ |_| | | (_| | (__| |_| | (_) | | | | # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_____|___/_/ \_\_.__/|___/\__|_| \__,_|\___|\__|_|\___/|_| |_| # 

7# |_| |___/ |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # 

15# Copyright 2014-2016 Technische Universität Dresden - Germany, Chair of VLSI-Design, Diagnostics and Architecture # 

16# # 

17# Licensed under the Apache License, Version 2.0 (the "License"); # 

18# you may not use this file except in compliance with the License. # 

19# You may obtain a copy of the License at # 

20# # 

21# http://www.apache.org/licenses/LICENSE-2.0 # 

22# # 

23# Unless required by applicable law or agreed to in writing, software # 

24# distributed under the License is distributed on an "AS IS" BASIS, # 

25# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

26# See the License for the specific language governing permissions and # 

27# limitations under the License. # 

28# # 

29# SPDX-License-Identifier: Apache-2.0 # 

30# ==================================================================================================================== # 

31# 

32""" 

33This module implements command line arguments without prefix character(s). 

34 

35""" 

36from abc import abstractmethod 

37from pathlib import Path 

38from sys import platform as sys_platform 

39from typing import ClassVar, Union, Iterable, TypeVar, Generic, Any, Optional as Nullable 

40 

41from pyTooling.Decorators import export, readonly 

42from pyTooling.MetaClasses import ExtendedType, abstractclass 

43from pyTooling.Common import getFullyQualifiedName 

44 

45if sys_platform == "win32": 

46 from subprocess import list2cmdline as formatCommandLine 

47else: 

48 from shlex import join as formatCommandLine 

49 

50 

51__all__ = ["ValueT"] 

52 

53 

54ValueT = TypeVar("ValueT") #: The type of value in a valued argument. 

55 

56 

57@export 

58@abstractclass 

59class CommandLineArgument(metaclass=ExtendedType): 

60 """ 

61 Base-class for all *Argument* classes. 

62 

63 An argument instance can be converted via ``AsArgument`` to a single string value or a sequence of string values 

64 (tuple) usable e.g. with :class:`subprocess.Popen`. Each argument class implements at least one ``pattern`` parameter 

65 to specify how argument are formatted. 

66 

67 There are multiple derived formats supporting: 

68 

69 * commands |br| 

70 |rarr| :mod:`~pyTooling.CLIAbstraction.Command` 

71 * simple names (flags) |br| 

72 |rarr| :mod:`~pyTooling.CLIAbstraction.Flag`, :mod:`~pyTooling.CLIAbstraction.BooleanFlag` 

73 * simple values (vlaued flags) |br| 

74 |rarr| :class:`~pyTooling.CLIAbstraction.Argument.StringArgument`, :class:`~pyTooling.CLIAbstraction.Argument.PathArgument` 

75 * names and values |br| 

76 |rarr| :mod:`~pyTooling.CLIAbstraction.ValuedFlag`, :mod:`~pyTooling.CLIAbstraction.OptionalValuedFlag` 

77 * key-value pairs |br| 

78 |rarr| :class:`~pyTooling.CLIAbstraction.KeyValueFlag.NamedKeyValuePairsArgument` 

79 """ 

80 

81 _PATTERN: ClassVar[str] #: Format string to render the argument on the command line. 

82 

83 def __init_subclass__(cls, *args: Any, pattern: Nullable[str] = None, **kwargs: Any) -> None: 

84 """ 

85 This method is called when a class is derived. 

86 

87 :param args: Any positional arguments. 

88 :param pattern: Optional, this pattern is used to format an argument. |br| 

89 Default: ``None``. 

90 :param kwargs: Any keyword argument. 

91 """ 

92 super().__init_subclass__(*args, **kwargs) 

93 cls._PATTERN = pattern 

94 

95 # TODO: Add property to read pattern 

96 

97 @abstractmethod 

98 def AsArgument(self) -> Union[str, Iterable[str]]: # type: ignore[empty-body] 

99 """ 

100 Convert this argument instance to a string representation using the matching pattern based on 

101 the internal name and value. 

102 

103 The result isn't escaped: :class:`subprocess.Popen` passes each string to the program as one argument. 

104 :meth:`__str__` returns it escaped for a command line. 

105 

106 :returns: Formatted argument. 

107 :raises NotImplementedError: This is an abstract method and must be overwritten by a subclass. 

108 """ 

109 raise NotImplementedError("Method 'AsArgument' is an abstract method and must be implemented by a subclass.") 

110 

111 def __str__(self) -> str: 

112 """ 

113 Return this argument as written on a command line, escaped for the current platform. 

114 

115 :returns: The formatted argument, escaped by ``formatCommandLine``: :func:`shlex.join`, on Windows 

116 ``subprocess.list2cmdline()``. 

117 """ 

118 argument = self.AsArgument() 

119 if isinstance(argument, str): 

120 return formatCommandLine((argument, )) 

121 else: 

122 return formatCommandLine(argument) 

123 

124 @staticmethod 

125 def _DoubleQuotedLiteral(value: str) -> str: 

126 """ 

127 Return a string as a Python literal in double quotes, escaped as :func:`repr` escapes it. 

128 

129 :param value: The string. 

130 :returns: The string's literal. 

131 """ 

132 literal = repr(value) 

133 if literal[0] == "\"": 

134 return literal 

135 

136 return "\"" + literal[1:-1].replace("\\'", "'").replace("\"", "\\\"") + "\"" 

137 

138 def __repr__(self) -> str: 

139 """ 

140 Return this argument as a Python literal, with strings in double quotes. 

141 

142 :returns: The formatted argument (:meth:`AsArgument`) as a literal; a sequence as a list. 

143 """ 

144 argument = self.AsArgument() 

145 if isinstance(argument, str): 

146 return self._DoubleQuotedLiteral(argument) 

147 else: 

148 literal = self._DoubleQuotedLiteral 

149 return "[" + ", ".join([literal(item) for item in argument]) + "]" # WORKAROUND: Python <3.12 

150 # return f"[{", ".join([literal(item) for item in argument])}]" 

151 

152 

153@export 

154class ExecutableArgument(CommandLineArgument): 

155 """ 

156 Represents the executable. 

157 """ 

158 

159 _executable: Path #: Path to the executable this argument represents. 

160 

161 def __init__(self, executable: Path) -> None: 

162 """ 

163 Initializes a ExecutableArgument instance. 

164 

165 :param executable: Path to the executable. 

166 :raises TypeError: If parameter 'executable' is not of type :class:`~pathlib.Path`. 

167 """ 

168 if not isinstance(executable, Path): 

169 ex = TypeError("Parameter 'executable' is not of type 'Path'.") 

170 ex.add_note(f"Got type '{getFullyQualifiedName(executable)}'.") 

171 raise ex 

172 

173 self._executable = executable 

174 

175 @property 

176 def Executable(self) -> Path: 

177 """ 

178 Property to access the path to the wrapped executable (:attr:`_executable`). 

179 

180 :returns: Internal path to the executable. 

181 :raises TypeError: If an assigned value is not of type :class:`~pathlib.Path`. 

182 """ 

183 return self._executable 

184 

185 @Executable.setter 

186 def Executable(self, value: Path) -> None: 

187 if not isinstance(value, Path): 

188 ex = TypeError("Parameter 'value' is not of type 'Path'.") 

189 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

190 raise ex 

191 

192 self._executable = value 

193 

194 def AsArgument(self) -> Union[str, Iterable[str]]: 

195 """ 

196 Convert this argument instance to a string representation using the matching pattern based on 

197 the internal path to the wrapped executable. 

198 

199 :returns: Formatted argument. 

200 """ 

201 return f"{self._executable}" 

202 

203 

204@export 

205class DelimiterArgument(CommandLineArgument, pattern="--"): 

206 """ 

207 Represents a delimiter symbol like ``--``. 

208 """ 

209 

210 def __init_subclass__(cls, *args: Any, pattern: str = "--", **kwargs: Any) -> None: 

211 """ 

212 This method is called when a class is derived. 

213 

214 :param args: Any positional arguments. 

215 :param pattern: Optional, this pattern is used to format an argument. |br| 

216 Default: ``"--"``. 

217 :param kwargs: Any keyword argument. 

218 """ 

219 kwargs["pattern"] = pattern 

220 super().__init_subclass__(*args, **kwargs) 

221 

222 def AsArgument(self) -> Union[str, Iterable[str]]: 

223 """ 

224 Convert this argument instance to a string representation using the matching pattern. 

225 

226 :returns: Formatted argument. 

227 """ 

228 return self._PATTERN 

229 

230 

231@export 

232@abstractclass 

233class NamedArgument(CommandLineArgument, pattern="{0}"): 

234 """ 

235 Base-class for all command line arguments with a name. 

236 """ 

237 

238 _NAME: ClassVar[str] #: Name of the argument, inserted into :attr:`_PATTERN`. 

239 

240 def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}", **kwargs: Any) -> None: 

241 """ 

242 This method is called when a class is derived. 

243 

244 :param args: Any positional arguments. 

245 :param name: Optional, name of the CLI argument. 

246 :param pattern: Optional, this pattern is used to format an argument. |br| 

247 Default: ``"{0}"``. 

248 :param kwargs: Any keyword argument. 

249 """ 

250 kwargs["pattern"] = pattern 

251 super().__init_subclass__(*args, **kwargs) 

252 cls._NAME = name 

253 

254 @readonly 

255 def Name(self) -> str: 

256 """ 

257 Get the internal name. 

258 

259 :returns: Internal name. 

260 """ 

261 return self._NAME 

262 

263 def AsArgument(self) -> Union[str, Iterable[str]]: 

264 """ 

265 Convert this argument instance to a string representation using the matching pattern based on 

266 the internal name. 

267 

268 :returns: Formatted argument. 

269 :raises ValueError: If internal name is None. 

270 """ 

271 if self._NAME is None: 271 ↛ 272line 271 didn't jump to line 272 because the condition on line 271 was never true

272 raise ValueError("Internal value '_NAME' is None.") 

273 

274 return self._PATTERN.format(self._NAME) 

275 

276 

277@export 

278class ValuedArgument(CommandLineArgument, Generic[ValueT], pattern="{0}"): 

279 """ 

280 Base-class for all command line arguments with a value. 

281 """ 

282 

283 _value: ValueT #: Value of the argument, inserted into :attr:`_PATTERN`. 

284 

285 def __init_subclass__(cls, *args: Any, pattern: str = "{0}", **kwargs: Any) -> None: 

286 """ 

287 This method is called when a class is derived. 

288 

289 :param args: Any positional arguments. 

290 :param pattern: Optional, this pattern is used to format an argument. |br| 

291 Default: ``"{0}"``. 

292 :param kwargs: Any keyword argument. 

293 """ 

294 kwargs["pattern"] = pattern 

295 super().__init_subclass__(*args, **kwargs) 

296 

297 def __init__(self, value: ValueT) -> None: 

298 """ 

299 Initializes a ValuedArgument instance. 

300 

301 :param value: Value to be stored internally. 

302 :raises ValueError: If parameter 'value' is None. 

303 :raises ValueError: If parameter 'value' is None. 

304 """ 

305 if value is None: 305 ↛ 306line 305 didn't jump to line 306 because the condition on line 305 was never true

306 raise ValueError("Parameter 'value' is None.") 

307 

308 self._value = value 

309 

310 @property 

311 def Value(self) -> ValueT: 

312 """ 

313 Property to access the internal value (:attr:`_value`). 

314 

315 :returns: Internal value. 

316 :raises ValueError: If ``None`` is assigned. 

317 """ 

318 return self._value 

319 

320 @Value.setter 

321 def Value(self, value: ValueT) -> None: 

322 if value is None: 322 ↛ 323line 322 didn't jump to line 323 because the condition on line 322 was never true

323 raise ValueError("Value to set is None.") 

324 

325 self._value = value 

326 

327 def AsArgument(self) -> Union[str, Iterable[str]]: 

328 """ 

329 Convert this argument instance to a string representation using the matching pattern based on 

330 the internal value. 

331 

332 :returns: Formatted argument. 

333 """ 

334 return self._PATTERN.format(self._value) 

335 

336 

337class NamedAndValuedArgument(NamedArgument, ValuedArgument[ValueT], Generic[ValueT], pattern="{0}={1}"): 

338 """ 

339 Base-class for all command line arguments with a name and a value. 

340 """ 

341 

342 def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}={1}", **kwargs: Any) -> None: 

343 """ 

344 This method is called when a class is derived. 

345 

346 :param args: Any positional arguments. 

347 :param name: Optional, name of the CLI argument. 

348 :param pattern: Optional, this pattern is used to format an argument. |br| 

349 Default: ``"{0}={1}"``. 

350 :param kwargs: Any keyword argument. 

351 """ 

352 kwargs["name"] = name 

353 kwargs["pattern"] = pattern 

354 super().__init_subclass__(*args, **kwargs) 

355 del kwargs["name"] 

356 del kwargs["pattern"] 

357 ValuedArgument.__init_subclass__(*args, **kwargs) 

358 

359 def __init__(self, value: ValueT) -> None: 

360 """ 

361 Initialize the argument with the value rendered into its pattern. 

362 

363 :param value: Value of the argument. 

364 """ 

365 ValuedArgument.__init__(self, value) 

366 

367 def AsArgument(self) -> Union[str, Iterable[str]]: 

368 """ 

369 Convert this argument instance to a string representation using the matching pattern based on 

370 the internal name and value. 

371 

372 :returns: Formatted argument. 

373 :raises ValueError: If internal name is None. 

374 """ 

375 if self._NAME is None: 375 ↛ 376line 375 didn't jump to line 376 because the condition on line 375 was never true

376 raise ValueError("Internal value '_NAME' is None.") 

377 

378 return self._PATTERN.format(self._NAME, self._value) 

379 

380 

381@abstractclass 

382class NamedTupledArgument(NamedArgument, ValuedArgument[ValueT], Generic[ValueT], pattern="{0}"): 

383 """ 

384 Class and base-class for all TupleFlag classes, which represents an argument with separate value. 

385 

386 A tuple argument is a command line argument followed by a separate value. Name and value are passed as two arguments 

387 to the executable. 

388 

389 **Example: ** 

390 

391 * `width 100`` 

392 """ 

393 

394 _VALUE_PATTERN: ClassVar[str] #: Format string to render the argument's value as a second command line element. 

395 

396 def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}", valuePattern: str = "{0}", **kwargs: Any) -> None: 

397 """ 

398 This method is called when a class is derived. 

399 

400 :param args: Any positional arguments. 

401 :param name: Optional, name of the CLI argument. 

402 :param pattern: Optional, this pattern is used to format the CLI argument name. |br| 

403 Default: ``"{0}"``. 

404 :param valuePattern: Optional, this pattern is used to format the value. |br| 

405 Default: ``"{0}"``. 

406 :param kwargs: Any keyword argument. 

407 """ 

408 kwargs["name"] = name 

409 kwargs["pattern"] = pattern 

410 super().__init_subclass__(*args, **kwargs) 

411 cls._VALUE_PATTERN = valuePattern 

412 

413 def __init__(self, value: ValueT) -> None: 

414 """ 

415 Initialize the argument with the value rendered into its pattern. 

416 

417 :param value: Value of the argument. 

418 """ 

419 ValuedArgument.__init__(self, value) 

420 

421 # TODO: Add property to read value pattern 

422 

423 # @property 

424 # def ValuePattern(self) -> str: 

425 # if self._VALUE_PATTERN is None: 

426 # raise ValueError("Internal value '_VALUE_PATTERN' is None.") 

427 # 

428 # return self._VALUE_PATTERN 

429 

430 def AsArgument(self) -> Union[str, Iterable[str]]: 

431 """ 

432 Convert this argument instance to a sequence of string representations using the matching 

433 pattern based on the internal name and value. 

434 

435 :returns: Formatted argument as tuple of strings. 

436 :raises ValueError: If internal name is None. 

437 """ 

438 if self._NAME is None: 438 ↛ 439line 438 didn't jump to line 439 because the condition on line 438 was never true

439 raise ValueError("Internal value '_NAME' is None.") 

440 

441 return ( 

442 self._PATTERN.format(self._NAME), 

443 self._VALUE_PATTERN.format(self._value) 

444 ) 

445 

446 

447@export 

448class StringArgument(ValuedArgument[str], pattern="{0}"): 

449 """ 

450 Represents a simple string argument. 

451 

452 A list of strings is available as :class:`~pyTooling.CLIAbstraction.Argument.StringListArgument`. 

453 """ 

454 

455 def __init_subclass__(cls, *args: Any, pattern: str = "{0}", **kwargs: Any) -> None: 

456 """ 

457 This method is called when a class is derived. 

458 

459 :param args: Any positional arguments. 

460 :param pattern: Optional, this pattern is used to format an argument. |br| 

461 Default: ``"{0}"``. 

462 :param kwargs: Any keyword argument. 

463 """ 

464 kwargs["pattern"] = pattern 

465 super().__init_subclass__(*args, **kwargs) 

466 

467 

468@export 

469class StringListArgument(ValuedArgument[str]): 

470 """ 

471 Represents a list of string argument (:class:`~pyTooling.CLIAbstraction.Argument.StringArgument`).""" 

472 

473 def __init__(self, values: Iterable[str]) -> None: 

474 """ 

475 Initializes a StringListArgument instance. 

476 

477 :param values: An iterable of str instances. 

478 :raises TypeError: If iterable parameter 'values' contains elements not of type string. 

479 """ 

480 self._values = [] 

481 for value in values: 

482 if not isinstance(value, str): 482 ↛ 483line 482 didn't jump to line 483 because the condition on line 482 was never true

483 ex = TypeError("Parameter 'values' contains elements which are not of type 'str'.") 

484 ex.add_note(f"Got type '{getFullyQualifiedName(values)}'.") 

485 raise ex 

486 

487 self._values.append(value) 

488 

489 @property 

490 def Value(self) -> list[str]: 

491 """ 

492 Property to access the internal list of str objects (:attr:`_values`). 

493 

494 .. note:: On assignment, the list object is not replaced, but cleared and then reused by adding the given elements 

495 of the iterable. 

496 

497 :returns: Reference to the internal list of str objects. 

498 :raises TypeError: If an assigned iterable contains elements which are not of type string. 

499 """ 

500 return self._values 

501 

502 @Value.setter 

503 def Value(self, value: Iterable[str]) -> None: 

504 self._values.clear() 

505 for value in value: 

506 if not isinstance(value, str): 

507 ex = TypeError("Value contains elements which are not of type 'str'.") 

508 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

509 raise ex 

510 self._values.append(value) 

511 

512 def AsArgument(self) -> Union[str, Iterable[str]]: 

513 """ 

514 Convert this argument instance to a string representation using the matching pattern based on 

515 the internal value. 

516 

517 :returns: Sequence of formatted arguments. 

518 """ 

519 return [f"{value}" for value in self._values] 

520 

521 

522# TODO: Add option to class if path should be checked for existence 

523@export 

524class PathArgument(CommandLineArgument): 

525 """ 

526 Represents a single path argument. 

527 

528 A list of paths is available as :class:`~pyTooling.CLIAbstraction.Argument.PathListArgument`. 

529 """ 

530 # The output format can be forced to the POSIX format with :py:data:`_PosixFormat`. 

531 _path: Path #: Path this argument represents. 

532 

533 def __init__(self, path: Path) -> None: 

534 """ 

535 Initializes a PathArgument instance. 

536 

537 :param path: Path to a filesystem object. 

538 :raises TypeError: If parameter 'path' is not of type :class:`~pathlib.Path`. 

539 """ 

540 if not isinstance(path, Path): 540 ↛ 541line 540 didn't jump to line 541 because the condition on line 540 was never true

541 ex = TypeError("Parameter 'path' is not of type 'Path'.") 

542 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.") 

543 raise ex 

544 self._path = path 

545 

546 @property 

547 def Value(self) -> Path: 

548 """ 

549 Property to access the internal path object (:attr:`_path`). 

550 

551 :returns: Internal path object. 

552 :raises TypeError: If an assigned value is not of type :class:`~pathlib.Path`. 

553 """ 

554 return self._path 

555 

556 @Value.setter 

557 def Value(self, value: Path) -> None: 

558 if not isinstance(value, Path): 558 ↛ 559line 558 didn't jump to line 559 because the condition on line 558 was never true

559 ex = TypeError("Value is not of type 'Path'.") 

560 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

561 raise ex 

562 

563 self._path = value 

564 

565 def AsArgument(self) -> Union[str, Iterable[str]]: 

566 """ 

567 Convert this argument instance to a string representation using the matching pattern based on 

568 the internal value. 

569 

570 :returns: Formatted argument. 

571 """ 

572 return f"{self._path}" 

573 

574 

575@export 

576class PathListArgument(CommandLineArgument): 

577 """ 

578 Represents a list of path arguments (:class:`~pyTooling.CLIAbstraction.Argument.PathArgument`). 

579 """ 

580 # The output format can be forced to the POSIX format with :py:data:`_PosixFormat`. 

581 _paths: list[Path] #: Paths this argument represents. 

582 

583 def __init__(self, paths: Iterable[Path]) -> None: 

584 """ 

585 Initializes a PathListArgument instance. 

586 

587 :param paths: An iterable os Path instances. 

588 :raises TypeError: If iterable parameter 'paths' contains elements not of type :class:`~pathlib.Path`. 

589 """ 

590 self._paths = [] 

591 for path in paths: 

592 if not isinstance(path, Path): 592 ↛ 593line 592 didn't jump to line 593 because the condition on line 592 was never true

593 ex = TypeError("Parameter 'paths' contains elements which are not of type 'Path'.") 

594 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.") 

595 raise ex 

596 

597 self._paths.append(path) 

598 

599 @property 

600 def Value(self) -> list[Path]: 

601 """ 

602 Property to access the internal list of path objects (:attr:`_paths`). 

603 

604 .. note:: On assignment, the list object is not replaced, but cleared and then reused by adding the given elements 

605 of the iterable. 

606 

607 :returns: Reference to the internal list of path objects. 

608 :raises TypeError: If an assigned iterable contains elements which are not of type :class:`~pathlib.Path`. 

609 """ 

610 return self._paths 

611 

612 @Value.setter 

613 def Value(self, value: Iterable[Path]) -> None: 

614 self._paths.clear() 

615 for path in value: 

616 if not isinstance(path, Path): 

617 ex = TypeError("Value contains elements which are not of type 'Path'.") 

618 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.") 

619 raise ex 

620 self._paths.append(path) 

621 

622 def AsArgument(self) -> Union[str, Iterable[str]]: 

623 """ 

624 Convert this argument instance to a string representation using the matching pattern based on 

625 the internal value. 

626 

627 :returns: Sequence of formatted arguments. 

628 """ 

629 return [f"{path}" for path in self._paths]