Coverage for pyVersioning / __init__.py: 71%

461 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-13 23:48 +0000

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

2# __ __ _ _ # 

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

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

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

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

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

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

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

14# Copyright 2020-2026 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-2026, Patrick Lehmann" 

34__license__ = "Apache License, Version 2.0" 

35__version__ = "0.18.4" 

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 typing import Union, Any, Dict, Tuple, ClassVar, Generator, Optional as Nullable, List 

44 

45from pyTooling.Decorators import export, readonly 

46from pyTooling.MetaClasses import ExtendedType 

47from pyTooling.Versioning import SemanticVersion 

48from pyTooling.TerminalUI import ILineTerminal 

49 

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

51 

52 

53@export 

54class VersioningException(Exception): 

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

56 

57 

58@export 

59class ToolException(VersioningException): 

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

61 

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

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

64 

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

66 """ 

67 

68 :param command: Command that caused the exception. 

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

70 """ 

71 super().__init__(errorMessage) 

72 self.command = command 

73 self.errorMessage = errorMessage 

74 

75 

76@export 

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

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

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

80 

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

82 for element in self._public: 

83 yield element 

84 

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

86 for element in self._public: 

87 value = self.__getattribute__(element) 

88 yield element, value 

89 

90 

91@export 

92class Tool(SelfDescriptive): 

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

94 

95 _name: str #: Name of pyVersioning 

96 _version: SemanticVersion #: Version of the pyVersioning 

97 

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

99 

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

101 """ 

102 Initialize a tool with name and version. 

103 

104 :param name: The tool's name. 

105 :param version: The tool's version. 

106 """ 

107 self._name = name 

108 self._version = version 

109 

110 @readonly 

111 def name(self) -> str: 

112 """ 

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

114 

115 :return: Name of the tool. 

116 """ 

117 return self._name 

118 

119 @readonly 

120 def version(self) -> SemanticVersion: 

121 """ 

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

123 

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

125 """ 

126 return self._version 

127 

128 def __str__(self) -> str: 

129 """ 

130 Return a string representation of this tool description. 

131 

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

133 """ 

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

135 

136 

137@export 

138class Date(date, SelfDescriptive): 

139 """ 

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

141 """ 

142 

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

144 

145 

146@export 

147class Time(time, SelfDescriptive): 

148 """ 

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

150 """ 

151 

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

153 

154 

155@export 

156class Person(SelfDescriptive): 

157 """ 

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

159 

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

161 """ 

162 

163 _name: str #: Name of the person. 

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

165 

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

167 

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

169 """ 

170 Initialize a person with name and email address. 

171 

172 :param name: The person's name. 

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

174 """ 

175 self._name = name 

176 self._email = email 

177 

178 @readonly 

179 def name(self) -> str: 

180 """ 

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

182 

183 :return: Name of the person. 

184 """ 

185 return self._name 

186 

187 @readonly 

188 def email(self) -> str: 

189 """ 

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

191 

192 :return: Email address of the person. 

193 """ 

194 return self._email 

195 

196 def __str__(self) -> str: 

197 """ 

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

199 

200 :returns: The persons name and email address. 

201 """ 

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

203 

204 

205@export 

206class Commit(SelfDescriptive): 

207 """ 

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

209 description. 

210 """ 

211 

212 _hash: str 

213 _date: date 

214 _time: time 

215 _author: Person 

216 _committer: Person 

217 _comment: str 

218 _oneline: Union[str, bool] = False 

219 

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

221 

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

223 """ 

224 Initialize a Git commit. 

225 

226 :param hash: The commit's hash. 

227 :param date: The commit's date. 

228 :param time: The commit's time. 

229 :param author: The commit's author. 

230 :param committer: The commit's committer. 

231 :param comment: The commit's comment. 

232 """ 

233 self._hash = hash 

234 self._date = date 

235 self._time = time 

236 self._author = author 

237 self._committer = committer 

238 self._comment = comment 

239 

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

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

242 

243 @readonly 

244 def hash(self) -> str: 

245 """ 

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

247 

248 :return: Hash of the commit as string. 

249 """ 

250 return self._hash 

251 

252 @readonly 

253 def date(self) -> date: 

254 """ 

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

256 

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

258 """ 

259 return self._date 

260 

261 @readonly 

262 def time(self) -> time: 

263 """ 

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

265 

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

267 """ 

268 return self._time 

269 

270 @readonly 

271 def author(self) -> Person: 

272 """ 

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

274 

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

276 """ 

277 return self._author 

278 

279 @readonly 

280 def committer(self) -> Person: 

281 """ 

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

283 

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

285 """ 

286 return self._committer 

287 

288 @readonly 

289 def comment(self) -> str: 

290 """ 

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

292 

293 :return: Author of the comment as string. 

294 """ 

295 return self._comment 

296 

297 @readonly 

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

299 return self._oneline 

300 

301 def __str__(self) -> str: 

302 """ 

303 Return a string representation of a commit. 

304 

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

306 """ 

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

308 

309 

310@export 

311class Git(SelfDescriptive): 

312 """ 

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

314 """ 

315 _commit: Commit #: Git commit information 

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

317 _tag: str #: Git tag 

318 _branch: str #: Git branch 

319 _repository: str #: Git repository URL 

320 

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

322 

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

324 self._commit = commit 

325 self._tag = tag 

326 self._branch = branch 

327 self._repository = repository 

328 

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

330 self._reference = tag 

331 elif branch != "": 

332 self._reference = branch 

333 else: 

334 self._reference = "[Detached HEAD]" 

335 

336 @readonly 

337 def commit(self) -> Commit: 

338 """ 

339 Read-only property to return the Git commit. 

340 

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

342 """ 

343 return self._commit 

344 

345 @readonly 

346 def reference(self) -> str: 

347 """ 

348 Read-only property to return the Git reference. 

349 

350 A reference is either a branch or tag. 

351 

352 :return: The current Git reference as string. 

353 """ 

354 return self._reference 

355 

356 @readonly 

357 def tag(self) -> str: 

358 """ 

359 Read-only property to return the Git tag. 

360 

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

362 """ 

363 return self._tag 

364 

365 @readonly 

366 def branch(self) -> str: 

367 """ 

368 Read-only property to return the Git branch. 

369 

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

371 """ 

372 return self._branch 

373 

374 @readonly 

375 def repository(self) -> str: 

376 """ 

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

378 

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

380 """ 

381 return self._repository 

382 

383 def __str__(self) -> str: 

384 """ 

385 Return a string representation of a repository. 

386 

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

388 """ 

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

390 

391 

392@export 

393class Project(SelfDescriptive): 

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

395 

396 _name: str #: Name of the project. 

397 _variant: str #: Variant of the project. 

398 _version: SemanticVersion #: Version of the project. 

399 

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

401 

402 def __init__( 

403 self, 

404 name: str, 

405 version: Union[str, SemanticVersion, None] = None, 

406 variant: Nullable[str] = None 

407 ) -> None: 

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

409 

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

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

412 

413 if isinstance(version, SemanticVersion): 

414 self._version = version 

415 elif isinstance(version, str): 

416 self._version = SemanticVersion.Parse(version) 

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

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

419 

420 @readonly 

421 def name(self) -> str: 

422 """ 

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

424 

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

426 """ 

427 return self._name 

428 

429 @readonly 

430 def variant(self) -> str: 

431 """ 

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

433 

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

435 """ 

436 return self._variant 

437 

438 @readonly 

439 def version(self) -> SemanticVersion: 

440 """ 

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

442 

443 :return: The project's version. 

444 """ 

445 return self._version 

446 

447 def __str__(self) -> str: 

448 """ 

449 Return a string representation of a project. 

450 

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

452 """ 

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

454 

455 

456@export 

457class Compiler(SelfDescriptive): 

458 _name: str #: Name of the compiler. 

459 _version: SemanticVersion #: Version of the compiler. 

460 _configuration: str #: Compiler configuration. 

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

462 

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

464 

465 def __init__( 

466 self, 

467 name: str, 

468 version: Union[str, SemanticVersion] = "", 

469 configuration: str = "", 

470 options: str = "" 

471 ) -> 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 # slots=True, 

934 repr=True 

935 ) 

936 

937 return Environment(**env) 

938 

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

940 # apply variables 

941 try: 

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

943 except AttributeError as ex: 

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