Coverage for pyVersioning/__init__.py: 71%

462 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-05-30 22:21 +0000

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

2# __ __ _ _ # 

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

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

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

6# | .__/ \__, | \_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | # 

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

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

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

14# Copyright 2020-2025 Patrick Lehmann - Bötzingen, Germany # 

15# # 

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

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

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

19# # 

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

21# # 

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

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

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

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

26# limitations under the License. # 

27# # 

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

29# ==================================================================================================================== # 

30# 

31__author__ = "Patrick Lehmann" 

32__email__ = "Paebbels@gmail.com" 

33__copyright__ = "2020-2025, Patrick Lehmann" 

34__license__ = "Apache License, Version 2.0" 

35__version__ = "0.18.2" 

36__keywords__ = ["Python3", "Template", "Versioning", "Git"] 

37 

38from dataclasses import make_dataclass 

39from datetime import date, time, datetime 

40from enum import Enum, auto 

41from os import environ 

42from subprocess import run as subprocess_run, PIPE, CalledProcessError 

43from sys import version_info 

44from typing import Union, Any, Dict, Tuple, ClassVar, Generator, Optional as Nullable, List 

45 

46from pyTooling.Decorators import export, readonly 

47from pyTooling.MetaClasses import ExtendedType 

48from pyTooling.Versioning import SemanticVersion 

49from pyTooling.TerminalUI import ILineTerminal 

50 

51from pyVersioning.Configuration import Configuration, Project, Compiler, Build 

52 

53 

54@export 

55class VersioningException(Exception): 

56 """Base-exception for all exceptions thrown by pyVersioning.""" 

57 

58 # WORKAROUND: for Python <3.11 

59 # Implementing a dummy method for Python versions before 

60 if version_info < (3, 11): # pragma: no cover 

61 __notes__: List[str] 

62 

63 def add_note(self, message: str) -> None: 

64 try: 

65 self.__notes__.append(message) 

66 except AttributeError: 

67 self.__notes__ = [message] 

68 

69 

70@export 

71class ToolException(VersioningException): 

72 """Exception thrown when a data collection tools has an error.""" 

73 

74 command: str #: Command that caused the exception. 

75 errorMessage: str #: Error message returned by the tool. 

76 

77 def __init__(self, command: str, errorMessage: str) -> None: 

78 """ 

79 

80 :param command: Command that caused the exception. 

81 :param errorMessage: Error message returned by the tool. 

82 """ 

83 super().__init__(errorMessage) 

84 self.command = command 

85 self.errorMessage = errorMessage 

86 

87 

88@export 

89class SelfDescriptive(metaclass=ExtendedType, slots=True, mixin=True): 

90 # TODO: could this be filled with a decorator? 

91 _public: ClassVar[Tuple[str, ...]] 

92 

93 def Keys(self) -> Generator[str, None, None]: 

94 for element in self._public: 

95 yield element 

96 

97 def KeyValuePairs(self) -> Generator[Tuple[str, Any], None, None]: 

98 for element in self._public: 

99 value = self.__getattribute__(element) 

100 yield element, value 

101 

102 

103@export 

104class Tool(SelfDescriptive): 

105 """This data structure class describes the tool name and version of pyVersioning.""" 

106 

107 _name: str #: Name of pyVersioning 

108 _version: SemanticVersion #: Version of the pyVersioning 

109 

110 _public: ClassVar[Tuple[str, ...]] = ("name", "version") 

111 

112 def __init__(self, name: str, version: SemanticVersion) -> None: 

113 """ 

114 Initialize a tool with name and version. 

115 

116 :param name: The tool's name. 

117 :param version: The tool's version. 

118 """ 

119 self._name = name 

120 self._version = version 

121 

122 @readonly 

123 def name(self) -> str: 

124 """ 

125 Read-only property to return the name of the tool. 

126 

127 :return: Name of the tool. 

128 """ 

129 return self._name 

130 

131 @readonly 

132 def version(self) -> SemanticVersion: 

133 """ 

134 Read-only property to return the version of the tool. 

135 

136 :return: Version of the tool as :class:`SemanticVersion`. 

137 """ 

138 return self._version 

139 

140 def __str__(self) -> str: 

141 """ 

142 Return a string representation of this tool description. 

143 

144 :returns: The tool's name and version. 

145 """ 

146 return f"{self._name} - {self._version}" 

147 

148 

149@export 

150class Date(date, SelfDescriptive): 

151 """ 

152 This data structure class describes a date (dd.mm.yyyy). 

153 """ 

154 

155 _public: ClassVar[Tuple[str, ...]] = ("day", "month", "year") 

156 

157 

158@export 

159class Time(time, SelfDescriptive): 

160 """ 

161 This data structure class describes a time (hh:mm:ss). 

162 """ 

163 

164 _public: ClassVar[Tuple[str, ...]] = ("hour", "minute", "second") 

165 

166 

167@export 

168class Person(SelfDescriptive): 

169 """ 

170 This data structure class describes a person with name and email address. 

171 

172 This class is used to describe an author or committer in a version control system. 

173 """ 

174 

175 _name: str #: Name of the person. 

176 _email: str #: Email address of the person. 

177 

178 _public: ClassVar[Tuple[str, ...]] = ("name", "email") 

179 

180 def __init__(self, name: str, email: str) -> None: 

181 """ 

182 Initialize a person with name and email address. 

183 

184 :param name: The person's name. 

185 :param email: The person's email address. 

186 """ 

187 self._name = name 

188 self._email = email 

189 

190 @readonly 

191 def name(self) -> str: 

192 """ 

193 Read-only property to return the name of the person. 

194 

195 :return: Name of the person. 

196 """ 

197 return self._name 

198 

199 @readonly 

200 def email(self) -> str: 

201 """ 

202 Read-only property to return the email address of the person. 

203 

204 :return: Email address of the person. 

205 """ 

206 return self._email 

207 

208 def __str__(self) -> str: 

209 """ 

210 Return a string representation of a person (committer, author, ...). 

211 

212 :returns: The persons name and email address. 

213 """ 

214 return f"{self._name} <{self._email}>" 

215 

216 

217@export 

218class Commit(SelfDescriptive): 

219 """ 

220 This data structure class describes a Git commit with Hash, commit date and time, committer, author and commit description. 

221 """ 

222 

223 _hash: str 

224 _date: date 

225 _time: time 

226 _author: Person 

227 _committer: Person 

228 _comment: str 

229 _oneline: Union[str, bool] = False 

230 

231 _public: ClassVar[Tuple[str, ...]] = ("hash", "date", "time", "author", "committer", "comment", "oneline") 

232 

233 def __init__(self, hash: str, date: date, time: time, author: Person, committer: Person, comment: str) -> None: 

234 """ 

235 Initialize a Git commit. 

236 

237 :param hash: The commit's hash. 

238 :param date: The commit's date. 

239 :param time: The commit's time. 

240 :param author: The commit's author. 

241 :param committer: The commit's committer. 

242 :param comment: The commit's comment. 

243 """ 

244 self._hash = hash 

245 self._date = date 

246 self._time = time 

247 self._author = author 

248 self._committer = committer 

249 self._comment = comment 

250 

251 if comment != "": 251 ↛ exitline 251 didn't return from function '__init__' because the condition on line 251 was always true

252 self._oneline = comment.split("\n")[0] 

253 

254 @readonly 

255 def hash(self) -> str: 

256 """ 

257 Read-only property to return the hash of the commit. 

258 

259 :return: Hash of the commit as string. 

260 """ 

261 return self._hash 

262 

263 @readonly 

264 def date(self) -> date: 

265 """ 

266 Read-only property to return the date of the commit. 

267 

268 :return: Date of the commit as :class:`date`. 

269 """ 

270 return self._date 

271 

272 @readonly 

273 def time(self) -> time: 

274 """ 

275 Read-only property to return the time of the commit. 

276 

277 :return: Time of the commit as :class:`time`. 

278 """ 

279 return self._time 

280 

281 @readonly 

282 def author(self) -> Person: 

283 """ 

284 Read-only property to return the author of the commit. 

285 

286 :return: Author of the commit as :class:`Person`. 

287 """ 

288 return self._author 

289 

290 @readonly 

291 def committer(self) -> Person: 

292 """ 

293 Read-only property to return the committer of the commit. 

294 

295 :return: Committer of the commit as :class:`Person`. 

296 """ 

297 return self._committer 

298 

299 @readonly 

300 def comment(self) -> str: 

301 """ 

302 Read-only property to return the comment of the commit. 

303 

304 :return: Author of the comment as string. 

305 """ 

306 return self._comment 

307 

308 @readonly 

309 def oneline(self) -> Union[str, bool]: 

310 return self._oneline 

311 

312 def __str__(self) -> str: 

313 """ 

314 Return a string representation of a commit. 

315 

316 :returns: The date, time, committer, hash and one-liner description. 

317 """ 

318 return f"{self._date} {self._time} - {self._committer} - {self._hash} - {self._oneline}" 

319 

320 

321@export 

322class Git(SelfDescriptive): 

323 """ 

324 This data structure class describes all collected data from Git. 

325 """ 

326 _commit: Commit #: Git commit information 

327 _reference: str #: Git reference (branch or tag) 

328 _tag: str #: Git tag 

329 _branch: str #: Git branch 

330 _repository: str #: Git repository URL 

331 

332 _public: ClassVar[Tuple[str, ...]] = ("commit", "reference", "tag", "branch", "repository") 

333 

334 def __init__(self, commit: Commit, repository: str, tag: str = "", branch: str = "") -> None: 

335 self._commit = commit 

336 self._tag = tag 

337 self._branch = branch 

338 self._repository = repository 

339 

340 if tag != "": 340 ↛ 342line 340 didn't jump to line 342 because the condition on line 340 was always true

341 self._reference = tag 

342 elif branch != "": 

343 self._reference = branch 

344 else: 

345 self._reference = "[Detached HEAD]" 

346 

347 @readonly 

348 def commit(self) -> Commit: 

349 """ 

350 Read-only property to return the Git commit. 

351 

352 :return: The commit information as :class:`Commit`. 

353 """ 

354 return self._commit 

355 

356 @readonly 

357 def reference(self) -> str: 

358 """ 

359 Read-only property to return the Git reference. 

360 

361 A reference is either a branch or tag. 

362 

363 :return: The current Git reference as string. 

364 """ 

365 return self._reference 

366 

367 @readonly 

368 def tag(self) -> str: 

369 """ 

370 Read-only property to return the Git tag. 

371 

372 :return: The current Git tag name as string. 

373 """ 

374 return self._tag 

375 

376 @readonly 

377 def branch(self) -> str: 

378 """ 

379 Read-only property to return the Git branch. 

380 

381 :return: The current Git branch name as string. 

382 """ 

383 return self._branch 

384 

385 @readonly 

386 def repository(self) -> str: 

387 """ 

388 Read-only property to return the Git repository URL. 

389 

390 :return: The current Git repository URL as string. 

391 """ 

392 return self._repository 

393 

394 def __str__(self) -> str: 

395 """ 

396 Return a string representation of a repository. 

397 

398 :returns: The date, time, committer, hash and one-liner description. 

399 """ 

400 return f"{self._repository}:{self._reference} - {self._commit._hash} - {self._commit._oneline}" 

401 

402 

403@export 

404class Project(SelfDescriptive): 

405 """This data structure class describes a project.""" 

406 

407 _name: str #: Name of the project. 

408 _variant: str #: Variant of the project. 

409 _version: SemanticVersion #: Version of the project. 

410 

411 _public: ClassVar[Tuple[str, ...]] = ("name", "variant", "version") 

412 

413 def __init__(self, name: str, version: Union[str, SemanticVersion, None] = None, variant: Nullable[str] = None) -> None: 

414 """Assign fields and convert version string to a `Version` object.""" 

415 

416 self._name = name if name is not None else "" 

417 self._variant = variant if variant is not None else "" 

418 

419 if isinstance(version, SemanticVersion): 

420 self._version = version 

421 elif isinstance(version, str): 

422 self._version = SemanticVersion.Parse(version) 

423 elif version is None: 423 ↛ exitline 423 didn't return from function '__init__' because the condition on line 423 was always true

424 self._version = SemanticVersion.Parse("v0.0.0") 

425 

426 @readonly 

427 def name(self) -> str: 

428 """ 

429 Read-only property to return the name of the project. 

430 

431 :return: The project's name as string. 

432 """ 

433 return self._name 

434 

435 @readonly 

436 def variant(self) -> str: 

437 """ 

438 Read-only property to return the variant name of the project. 

439 

440 :return: The project's variant as string. 

441 """ 

442 return self._variant 

443 

444 @readonly 

445 def version(self) -> SemanticVersion: 

446 """ 

447 Read-only property to return the version of the project. 

448 

449 :return: The project's version. 

450 """ 

451 return self._version 

452 

453 def __str__(self) -> str: 

454 """ 

455 Return a string representation of a project. 

456 

457 :returns: The project name, project variant and project version. 

458 """ 

459 return f"{self._name} - {self._variant} {self._version}" 

460 

461 

462@export 

463class Compiler(SelfDescriptive): 

464 _name: str #: Name of the compiler. 

465 _version: SemanticVersion #: Version of the compiler. 

466 _configuration: str #: Compiler configuration. 

467 _options: str #: Compiler options (compiler flags). 

468 

469 _public: ClassVar[Tuple[str, ...]] = ("name", "version", "configuration", "options") 

470 

471 def __init__(self, name: str, version: Union[str, SemanticVersion] = "", configuration: str = "", options: str = "") -> None: 

472 """Assign fields and convert version string to a `Version` object.""" 

473 

474 self._name = name if name is not None else "" 

475 self._configuration = configuration if configuration is not None else "" 

476 self._options = options if options is not None else "" 

477 

478 if isinstance(version, SemanticVersion): 478 ↛ 480line 478 didn't jump to line 480 because the condition on line 478 was always true

479 self._version = version 

480 elif isinstance(version, str): 

481 self._version = SemanticVersion.Parse(version) 

482 elif version is None: 

483 self._version = SemanticVersion.Parse("v0.0.0") 

484 

485 @readonly 

486 def name(self) -> str: 

487 """ 

488 Read-only property to return the name of the compiler. 

489 

490 :return: The compiler's name as string. 

491 """ 

492 return self._name 

493 

494 @readonly 

495 def version(self) -> SemanticVersion: 

496 """ 

497 Read-only property to return the version of the compiler. 

498 

499 :return: The compiler's version. 

500 """ 

501 return self._version 

502 

503 @readonly 

504 def configuration(self) -> str: 

505 """ 

506 Read-only property to return the configuration of the compiler. 

507 

508 :return: The compiler's configuration. 

509 """ 

510 return self._configuration 

511 

512 @readonly 

513 def options(self) -> str: 

514 """ 

515 Read-only property to return the options of the compiler. 

516 

517 :return: The compiler's options. 

518 """ 

519 return self._options 

520 

521 def __str__(self) -> str: 

522 """ 

523 Return a string representation of a compiler. 

524 

525 :returns: The compiler name and compiler version. 

526 """ 

527 return f"{self._name} - {self._version}" 

528 

529 

530@export 

531class Build(SelfDescriptive): 

532 _date: date #: Build date. 

533 _time: time #: Build time. 

534 _compiler: Compiler #: Use compiler (and options) for the build. 

535 

536 _public: ClassVar[Tuple[str, ...]] = ("date", "time", "compiler") 

537 

538 def __init__(self, date: date, time: time, compiler: Compiler) -> None: 

539 self._date = date 

540 self._time = time 

541 self._compiler = compiler 

542 

543 @readonly 

544 def date(self) -> date: 

545 """ 

546 Read-only property to return the date of the build. 

547 

548 :return: Date of the build as :class:`date`. 

549 """ 

550 return self._date 

551 

552 @readonly 

553 def time(self) -> time: 

554 """ 

555 Read-only property to return the time of the build. 

556 

557 :return: Time of the build as :class:`date`. 

558 """ 

559 return self._time 

560 

561 @readonly 

562 def compiler(self) -> Compiler: 

563 return self._compiler 

564 

565 

566@export 

567class Platforms(Enum): 

568 """An enumeration of platforms supported by pyTooling.""" 

569 Workstation = auto() #: A local work station, server, PC or laptop. 

570 AppVeyor = auto() #: A CI service operated by `AppVeyor <https://www.appveyor.com/>`__. 

571 GitHub = auto() #: A CI service offered by `GitHub <https://github.com/>`__ called GitHub Actions. 

572 GitLab = auto() #: A CI service offered by `GitLab <https://about.gitlab.com/>`__. 

573 Travis = auto() #: A CI service operated by `Travis <https://www.travis-ci.com/>`__. 

574 

575 

576@export 

577class Platform(SelfDescriptive): 

578 """.. todo:: Platform needs documentation""" 

579 

580 _ciService: str 

581 _public: ClassVar[Tuple[str, ...]] = ('ci_service', ) 

582 

583 def __init__(self, ciService: str) -> None: 

584 self._ciService = ciService 

585 

586 @readonly 

587 def ci_service(self) -> str: 

588 return self._ciService 

589 

590 

591@export 

592class BaseService(metaclass=ExtendedType): 

593 """Base-class to collect platform and environment information from e.g. environment variables.""" 

594 

595 # @abstractmethod 

596 def GetPlatform(self) -> Platform: # type: ignore[empty-body] 

597 """ 

598 .. todo:: 

599 getPlatform needs documentation 

600 

601 """ 

602 

603 

604@export 

605class GitShowCommand(Enum): 

606 CommitDateTime = auto() 

607 CommitAuthorName = auto() 

608 CommitAuthorEmail = auto() 

609 CommitCommitterName = auto() 

610 CommitCommitterEmail = auto() 

611 CommitHash = auto() 

612 CommitComment = auto() 

613 

614 

615@export 

616class GitHelperMixin(metaclass=ExtendedType, mixin=True): 

617 __GIT_SHOW_COMMAND_TO_FORMAT_LOOKUP = { 

618 GitShowCommand.CommitHash: "%H", 

619 GitShowCommand.CommitDateTime: "%ct", 

620 GitShowCommand.CommitAuthorName: "%an", 

621 GitShowCommand.CommitAuthorEmail: "%ae", 

622 GitShowCommand.CommitCommitterName: "%cn", 

623 GitShowCommand.CommitCommitterEmail: "%ce", 

624 GitShowCommand.CommitComment: "%B", 

625 } 

626 

627 def ExecuteGitShow(self, cmd: GitShowCommand, ref: str = "HEAD") -> str: 

628 format = f"--format='{self.__GIT_SHOW_COMMAND_TO_FORMAT_LOOKUP[cmd]}'" 

629 

630 command = "git" 

631 arguments = ("show", "-s", format, ref) 

632 try: 

633 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE) 

634 except CalledProcessError as ex: 

635 raise ToolException(f"{command} {' '.join(arguments)}", str(ex)) 

636 

637 if completed.returncode == 0: 637 ↛ 641line 637 didn't jump to line 641 because the condition on line 637 was always true

638 comment = completed.stdout.decode("utf-8") 

639 return comment[1:-2] 

640 else: 

641 message = completed.stderr.decode("utf-8") 

642 raise ToolException(f"{command} {' '.join(arguments)}", message) 

643 

644 

645@export 

646class Versioning(ILineTerminal, GitHelperMixin): 

647 _variables: Dict[str, Any] 

648 _platform: Platforms = Platforms.Workstation 

649 _service: BaseService 

650 

651 def __init__(self, terminal: ILineTerminal) -> None: 

652 super().__init__(terminal) 

653 

654 self._variables = { 

655 "tool": Tool("pyVersioning", SemanticVersion.Parse(f"v{__version__}")) 

656 } 

657 

658 if "APPVEYOR" in environ: 658 ↛ 659line 658 didn't jump to line 659 because the condition on line 658 was never true

659 self._platform = Platforms.AppVeyor 

660 elif "GITHUB_ACTIONS" in environ: 660 ↛ 662line 660 didn't jump to line 662 because the condition on line 660 was always true

661 self._platform = Platforms.GitHub 

662 elif "GITLAB_CI" in environ: 

663 self._platform = Platforms.GitLab 

664 elif "TRAVIS" in environ: 

665 self._platform = Platforms.Travis 

666 else: 

667 self._platform = Platforms.Workstation 

668 

669 self.WriteDebug(f"Detected platform: {self._platform.name}") 

670 

671 @readonly 

672 def Variables(self) -> Dict[str, Any]: 

673 return self._variables 

674 

675 @readonly 

676 def Platform(self) -> Platforms: 

677 return self._platform 

678 

679 def LoadDataFromConfiguration(self, config: Configuration) -> None: 

680 """Preload versioning information from a configuration file.""" 

681 

682 self._variables["version"] = self.GetVersion(config.project) 

683 self._variables["project"] = self.GetProject(config.project) 

684 self._variables["build"] = self.GetBuild(config.build) 

685 

686 def CollectData(self) -> None: 

687 """Collect versioning information from environment including CI services (if available).""" 

688 

689 from pyVersioning.AppVeyor import AppVeyor 

690 from pyVersioning.CIService import WorkStation 

691 from pyVersioning.GitLab import GitLab 

692 from pyVersioning.GitHub import GitHub 

693 from pyVersioning.Travis import Travis 

694 

695 if self._platform is Platforms.AppVeyor: 695 ↛ 696line 695 didn't jump to line 696 because the condition on line 695 was never true

696 self._service = AppVeyor() 

697 self._variables["appveyor"] = self._service.GetEnvironment() 

698 elif self._platform is Platforms.GitHub: 698 ↛ 701line 698 didn't jump to line 701 because the condition on line 698 was always true

699 self._service = GitHub() 

700 self._variables["github"] = self._service.GetEnvironment() 

701 elif self._platform is Platforms.GitLab: 

702 self._service = GitLab() 

703 self._variables["gitlab"] = self._service.GetEnvironment() 

704 elif self._platform is Platforms.Travis: 

705 self._service = Travis() 

706 self._variables["travis"] = self._service.GetEnvironment() 

707 else: 

708 self._service = WorkStation() 

709 

710 self._variables["git"] = self.GetGitInformation() 

711 self._variables["platform"] = self._service.GetPlatform() 

712 self._variables["env"] = self.GetEnvironment() 

713 

714 self.CalculateData() 

715 

716 def CalculateData(self) -> None: 

717 if self._variables["git"].tag != "": 717 ↛ exitline 717 didn't return from function 'CalculateData' because the condition on line 717 was always true

718 pass 

719 

720 def GetVersion(self, config: Project) -> SemanticVersion: 

721 if config.version is not None: 721 ↛ 724line 721 didn't jump to line 724 because the condition on line 721 was always true

722 return config.version 

723 else: 

724 return SemanticVersion.Parse("0.0.0") 

725 

726 def GetGitInformation(self) -> Git: 

727 return Git( 

728 commit=self.GetLastCommit(), 

729 tag=self.GetGitTag(), 

730 branch=self.GetGitLocalBranch(), 

731 repository=self.GetGitRemoteURL() 

732 ) 

733 

734 def GetLastCommit(self) -> Commit: 

735 dt = self.GetCommitDate() 

736 

737 return Commit( 

738 hash=self.GetGitHash(), 

739 date=dt.date(), 

740 time=dt.time(), 

741 author=self.GetCommitAuthor(), 

742 committer=self.GetCommitCommitter(), 

743 comment=self.GetCommitComment() 

744 ) 

745 

746 def GetGitHash(self) -> str: 

747 if self._platform is not Platforms.Workstation: 747 ↛ 750line 747 didn't jump to line 750 because the condition on line 747 was always true

748 return self._service.GetGitHash() 

749 

750 return self.ExecuteGitShow(GitShowCommand.CommitHash) 

751 

752 def GetCommitDate(self) -> datetime: 

753 if self._platform is not Platforms.Workstation: 753 ↛ 756line 753 didn't jump to line 756 because the condition on line 753 was always true

754 return self._service.GetCommitDate() 

755 

756 datetimeString = self.ExecuteGitShow(GitShowCommand.CommitDateTime) 

757 return datetime.fromtimestamp(int(datetimeString)) 

758 

759 def GetCommitAuthor(self) -> Person: 

760 return Person( 

761 name=self.GetCommitAuthorName(), 

762 email=self.GetCommitAuthorEmail() 

763 ) 

764 

765 def GetCommitAuthorName(self) -> str: 

766 return self.ExecuteGitShow(GitShowCommand.CommitAuthorName) 

767 

768 def GetCommitAuthorEmail(self) -> str: 

769 return self.ExecuteGitShow(GitShowCommand.CommitAuthorEmail) 

770 

771 def GetCommitCommitter(self) -> Person: 

772 return Person( 

773 name=self.GetCommitCommitterName(), 

774 email=self.GetCommitCommitterEmail() 

775 ) 

776 

777 def GetCommitCommitterName(self) -> str: 

778 return self.ExecuteGitShow(GitShowCommand.CommitCommitterName) 

779 

780 def GetCommitCommitterEmail(self) -> str: 

781 return self.ExecuteGitShow(GitShowCommand.CommitCommitterEmail) 

782 

783 def GetCommitComment(self) -> str: 

784 return self.ExecuteGitShow(GitShowCommand.CommitComment) 

785 

786 def GetGitLocalBranch(self) -> str: 

787 if self._platform is not Platforms.Workstation: 787 ↛ 790line 787 didn't jump to line 790 because the condition on line 787 was always true

788 return self._service.GetGitBranch() 

789 

790 command = "git" 

791 arguments = ("branch", "--show-current") 

792 try: 

793 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE) 

794 except CalledProcessError: 

795 return "" 

796 

797 if completed.returncode == 0: 

798 return completed.stdout.decode("utf-8").split("\n")[0] 

799 else: 

800 message = completed.stderr.decode("utf-8") 

801 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}") 

802 

803 def GetGitRemoteBranch(self, localBranch: Nullable[str] = None) -> str: 

804 if self._platform is not Platforms.Workstation: 

805 return self._service.GetGitBranch() 

806 

807 if localBranch is None: 

808 localBranch = self.GetGitLocalBranch() 

809 

810 command = "git" 

811 arguments = ("config", f"branch.{localBranch}.merge") 

812 try: 

813 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE) 

814 except CalledProcessError: 

815 raise Exception() # XXX: needs error message 

816 

817 if completed.returncode == 0: 

818 return completed.stdout.decode("utf-8").split("\n")[0] 

819 else: 

820 message = completed.stderr.decode("utf-8") 

821 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}") 

822 raise Exception() # XXX: needs error message 

823 

824 def GetGitRemote(self, localBranch: Nullable[str] = None) -> str: 

825 if localBranch is None: 

826 localBranch = self.GetGitLocalBranch() 

827 

828 command = "git" 

829 arguments = ("config", f"branch.{localBranch}.remote") 

830 try: 

831 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE) 

832 except CalledProcessError: 

833 raise Exception() # XXX: needs error message 

834 

835 if completed.returncode == 0: 

836 return completed.stdout.decode("utf-8").split("\n")[0] 

837 elif completed.returncode == 1: 

838 self.WriteWarning(f"Branch '{localBranch}' is not pushed to a remote.") 

839 return f"(local) {localBranch}" 

840 else: 

841 message = completed.stderr.decode("utf-8") 

842 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}") 

843 raise Exception() # XXX: needs error message 

844 

845 def GetGitTag(self) -> str: 

846 if self._platform is not Platforms.Workstation: 846 ↛ 849line 846 didn't jump to line 849 because the condition on line 846 was always true

847 return self._service.GetGitTag() 

848 

849 command = "git" 

850 arguments = ("tag", "--points-at", "HEAD") 

851 try: 

852 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE) 

853 except CalledProcessError: 

854 raise Exception() # XXX: needs error message 

855 

856 if completed.returncode == 0: 

857 return completed.stdout.decode("utf-8").split("\n")[0] 

858 else: 

859 message = completed.stderr.decode("utf-8") 

860 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}") 

861 raise Exception() # XXX: needs error message 

862 

863 def GetGitRemoteURL(self, remote: Nullable[str] = None) -> str: 

864 if self._platform is not Platforms.Workstation: 864 ↛ 867line 864 didn't jump to line 867 because the condition on line 864 was always true

865 return self._service.GetGitRepository() 

866 

867 if remote is None: 

868 remote = self.GetGitRemote() 

869 

870 command = "git" 

871 arguments = ("config", f"remote.{remote}.url") 

872 try: 

873 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE) 

874 except CalledProcessError: 

875 raise Exception() # XXX: needs error message 

876 

877 if completed.returncode == 0: 

878 return completed.stdout.decode("utf-8").split("\n")[0] 

879 else: 

880 message = completed.stderr.decode("utf-8") 

881 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}") 

882 raise Exception() # XXX: needs error message 

883 

884 # self.WriteFatal(f"Message from '{command}': {message}") 

885 

886 def GetProject(self, config: Project) -> Project: 

887 return Project( 

888 name=config.name, 

889 version=config.version, 

890 variant=config.variant 

891 ) 

892 

893 def GetBuild(self, config: Build) -> Build: 

894 dt = datetime.now() 

895 return Build( 

896 date=dt.date(), 

897 time=dt.time(), 

898 compiler=self.GetCompiler(config.compiler) 

899 ) 

900 

901 def GetCompiler(self, config: Compiler) -> Compiler: 

902 return Compiler( 

903 name=config.name, 

904 version=SemanticVersion.Parse(config.version), 

905 configuration=config.configuration, 

906 options=config.options 

907 ) 

908 

909 def GetEnvironment(self) -> Any: 

910 env = {} 

911 for key, value in environ.items(): 

912 if not key.isidentifier(): 

913 self.WriteWarning(f"Skipping environment variable '{key}', because it's not a valid Python identifier.") 

914 continue 

915 key = key.replace("(", "_") 

916 key = key.replace(")", "_") 

917 key = key.replace(" ", "_") 

918 env[key] = value 

919 

920 def func(s) -> Generator[Tuple[str, Any], None, None]: 

921 for e in env.keys(): 

922 yield e, s.__getattribute__(e) 

923 

924 Environment = make_dataclass( 

925 "Environment", 

926 [(name, str) for name in env.keys()], 

927# bases=(SelfDescriptive,), 

928 namespace={ 

929 "as_dict": lambda self: env, 

930 "Keys": lambda self: env.keys(), 

931 "KeyValuePairs": lambda self: func(self) 

932 }, 

933 repr=True 

934 ) 

935 

936 return Environment(**env) 

937 

938 def FillOutTemplate(self, template: str, **kwargs) -> str: 

939 # apply variables 

940 try: 

941 return template.format(**self._variables, **kwargs) 

942 except AttributeError as ex: 

943 self.WriteFatal(f"Syntax error in template. Accessing field '{ex.name}' of '{ex.obj.__class__.__name__}'.")