Coverage for pyVersioning/__init__.py: 71%
462 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-05-30 22:21 +0000
« 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"]
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
46from pyTooling.Decorators import export, readonly
47from pyTooling.MetaClasses import ExtendedType
48from pyTooling.Versioning import SemanticVersion
49from pyTooling.TerminalUI import ILineTerminal
51from pyVersioning.Configuration import Configuration, Project, Compiler, Build
54@export
55class VersioningException(Exception):
56 """Base-exception for all exceptions thrown by pyVersioning."""
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]
63 def add_note(self, message: str) -> None:
64 try:
65 self.__notes__.append(message)
66 except AttributeError:
67 self.__notes__ = [message]
70@export
71class ToolException(VersioningException):
72 """Exception thrown when a data collection tools has an error."""
74 command: str #: Command that caused the exception.
75 errorMessage: str #: Error message returned by the tool.
77 def __init__(self, command: str, errorMessage: str) -> None:
78 """
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
88@export
89class SelfDescriptive(metaclass=ExtendedType, slots=True, mixin=True):
90 # TODO: could this be filled with a decorator?
91 _public: ClassVar[Tuple[str, ...]]
93 def Keys(self) -> Generator[str, None, None]:
94 for element in self._public:
95 yield element
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
103@export
104class Tool(SelfDescriptive):
105 """This data structure class describes the tool name and version of pyVersioning."""
107 _name: str #: Name of pyVersioning
108 _version: SemanticVersion #: Version of the pyVersioning
110 _public: ClassVar[Tuple[str, ...]] = ("name", "version")
112 def __init__(self, name: str, version: SemanticVersion) -> None:
113 """
114 Initialize a tool with name and version.
116 :param name: The tool's name.
117 :param version: The tool's version.
118 """
119 self._name = name
120 self._version = version
122 @readonly
123 def name(self) -> str:
124 """
125 Read-only property to return the name of the tool.
127 :return: Name of the tool.
128 """
129 return self._name
131 @readonly
132 def version(self) -> SemanticVersion:
133 """
134 Read-only property to return the version of the tool.
136 :return: Version of the tool as :class:`SemanticVersion`.
137 """
138 return self._version
140 def __str__(self) -> str:
141 """
142 Return a string representation of this tool description.
144 :returns: The tool's name and version.
145 """
146 return f"{self._name} - {self._version}"
149@export
150class Date(date, SelfDescriptive):
151 """
152 This data structure class describes a date (dd.mm.yyyy).
153 """
155 _public: ClassVar[Tuple[str, ...]] = ("day", "month", "year")
158@export
159class Time(time, SelfDescriptive):
160 """
161 This data structure class describes a time (hh:mm:ss).
162 """
164 _public: ClassVar[Tuple[str, ...]] = ("hour", "minute", "second")
167@export
168class Person(SelfDescriptive):
169 """
170 This data structure class describes a person with name and email address.
172 This class is used to describe an author or committer in a version control system.
173 """
175 _name: str #: Name of the person.
176 _email: str #: Email address of the person.
178 _public: ClassVar[Tuple[str, ...]] = ("name", "email")
180 def __init__(self, name: str, email: str) -> None:
181 """
182 Initialize a person with name and email address.
184 :param name: The person's name.
185 :param email: The person's email address.
186 """
187 self._name = name
188 self._email = email
190 @readonly
191 def name(self) -> str:
192 """
193 Read-only property to return the name of the person.
195 :return: Name of the person.
196 """
197 return self._name
199 @readonly
200 def email(self) -> str:
201 """
202 Read-only property to return the email address of the person.
204 :return: Email address of the person.
205 """
206 return self._email
208 def __str__(self) -> str:
209 """
210 Return a string representation of a person (committer, author, ...).
212 :returns: The persons name and email address.
213 """
214 return f"{self._name} <{self._email}>"
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 """
223 _hash: str
224 _date: date
225 _time: time
226 _author: Person
227 _committer: Person
228 _comment: str
229 _oneline: Union[str, bool] = False
231 _public: ClassVar[Tuple[str, ...]] = ("hash", "date", "time", "author", "committer", "comment", "oneline")
233 def __init__(self, hash: str, date: date, time: time, author: Person, committer: Person, comment: str) -> None:
234 """
235 Initialize a Git commit.
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
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]
254 @readonly
255 def hash(self) -> str:
256 """
257 Read-only property to return the hash of the commit.
259 :return: Hash of the commit as string.
260 """
261 return self._hash
263 @readonly
264 def date(self) -> date:
265 """
266 Read-only property to return the date of the commit.
268 :return: Date of the commit as :class:`date`.
269 """
270 return self._date
272 @readonly
273 def time(self) -> time:
274 """
275 Read-only property to return the time of the commit.
277 :return: Time of the commit as :class:`time`.
278 """
279 return self._time
281 @readonly
282 def author(self) -> Person:
283 """
284 Read-only property to return the author of the commit.
286 :return: Author of the commit as :class:`Person`.
287 """
288 return self._author
290 @readonly
291 def committer(self) -> Person:
292 """
293 Read-only property to return the committer of the commit.
295 :return: Committer of the commit as :class:`Person`.
296 """
297 return self._committer
299 @readonly
300 def comment(self) -> str:
301 """
302 Read-only property to return the comment of the commit.
304 :return: Author of the comment as string.
305 """
306 return self._comment
308 @readonly
309 def oneline(self) -> Union[str, bool]:
310 return self._oneline
312 def __str__(self) -> str:
313 """
314 Return a string representation of a commit.
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}"
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
332 _public: ClassVar[Tuple[str, ...]] = ("commit", "reference", "tag", "branch", "repository")
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
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]"
347 @readonly
348 def commit(self) -> Commit:
349 """
350 Read-only property to return the Git commit.
352 :return: The commit information as :class:`Commit`.
353 """
354 return self._commit
356 @readonly
357 def reference(self) -> str:
358 """
359 Read-only property to return the Git reference.
361 A reference is either a branch or tag.
363 :return: The current Git reference as string.
364 """
365 return self._reference
367 @readonly
368 def tag(self) -> str:
369 """
370 Read-only property to return the Git tag.
372 :return: The current Git tag name as string.
373 """
374 return self._tag
376 @readonly
377 def branch(self) -> str:
378 """
379 Read-only property to return the Git branch.
381 :return: The current Git branch name as string.
382 """
383 return self._branch
385 @readonly
386 def repository(self) -> str:
387 """
388 Read-only property to return the Git repository URL.
390 :return: The current Git repository URL as string.
391 """
392 return self._repository
394 def __str__(self) -> str:
395 """
396 Return a string representation of a repository.
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}"
403@export
404class Project(SelfDescriptive):
405 """This data structure class describes a project."""
407 _name: str #: Name of the project.
408 _variant: str #: Variant of the project.
409 _version: SemanticVersion #: Version of the project.
411 _public: ClassVar[Tuple[str, ...]] = ("name", "variant", "version")
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."""
416 self._name = name if name is not None else ""
417 self._variant = variant if variant is not None else ""
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")
426 @readonly
427 def name(self) -> str:
428 """
429 Read-only property to return the name of the project.
431 :return: The project's name as string.
432 """
433 return self._name
435 @readonly
436 def variant(self) -> str:
437 """
438 Read-only property to return the variant name of the project.
440 :return: The project's variant as string.
441 """
442 return self._variant
444 @readonly
445 def version(self) -> SemanticVersion:
446 """
447 Read-only property to return the version of the project.
449 :return: The project's version.
450 """
451 return self._version
453 def __str__(self) -> str:
454 """
455 Return a string representation of a project.
457 :returns: The project name, project variant and project version.
458 """
459 return f"{self._name} - {self._variant} {self._version}"
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).
469 _public: ClassVar[Tuple[str, ...]] = ("name", "version", "configuration", "options")
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."""
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 ""
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")
485 @readonly
486 def name(self) -> str:
487 """
488 Read-only property to return the name of the compiler.
490 :return: The compiler's name as string.
491 """
492 return self._name
494 @readonly
495 def version(self) -> SemanticVersion:
496 """
497 Read-only property to return the version of the compiler.
499 :return: The compiler's version.
500 """
501 return self._version
503 @readonly
504 def configuration(self) -> str:
505 """
506 Read-only property to return the configuration of the compiler.
508 :return: The compiler's configuration.
509 """
510 return self._configuration
512 @readonly
513 def options(self) -> str:
514 """
515 Read-only property to return the options of the compiler.
517 :return: The compiler's options.
518 """
519 return self._options
521 def __str__(self) -> str:
522 """
523 Return a string representation of a compiler.
525 :returns: The compiler name and compiler version.
526 """
527 return f"{self._name} - {self._version}"
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.
536 _public: ClassVar[Tuple[str, ...]] = ("date", "time", "compiler")
538 def __init__(self, date: date, time: time, compiler: Compiler) -> None:
539 self._date = date
540 self._time = time
541 self._compiler = compiler
543 @readonly
544 def date(self) -> date:
545 """
546 Read-only property to return the date of the build.
548 :return: Date of the build as :class:`date`.
549 """
550 return self._date
552 @readonly
553 def time(self) -> time:
554 """
555 Read-only property to return the time of the build.
557 :return: Time of the build as :class:`date`.
558 """
559 return self._time
561 @readonly
562 def compiler(self) -> Compiler:
563 return self._compiler
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/>`__.
576@export
577class Platform(SelfDescriptive):
578 """.. todo:: Platform needs documentation"""
580 _ciService: str
581 _public: ClassVar[Tuple[str, ...]] = ('ci_service', )
583 def __init__(self, ciService: str) -> None:
584 self._ciService = ciService
586 @readonly
587 def ci_service(self) -> str:
588 return self._ciService
591@export
592class BaseService(metaclass=ExtendedType):
593 """Base-class to collect platform and environment information from e.g. environment variables."""
595 # @abstractmethod
596 def GetPlatform(self) -> Platform: # type: ignore[empty-body]
597 """
598 .. todo::
599 getPlatform needs documentation
601 """
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()
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 }
627 def ExecuteGitShow(self, cmd: GitShowCommand, ref: str = "HEAD") -> str:
628 format = f"--format='{self.__GIT_SHOW_COMMAND_TO_FORMAT_LOOKUP[cmd]}'"
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))
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)
645@export
646class Versioning(ILineTerminal, GitHelperMixin):
647 _variables: Dict[str, Any]
648 _platform: Platforms = Platforms.Workstation
649 _service: BaseService
651 def __init__(self, terminal: ILineTerminal) -> None:
652 super().__init__(terminal)
654 self._variables = {
655 "tool": Tool("pyVersioning", SemanticVersion.Parse(f"v{__version__}"))
656 }
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
669 self.WriteDebug(f"Detected platform: {self._platform.name}")
671 @readonly
672 def Variables(self) -> Dict[str, Any]:
673 return self._variables
675 @readonly
676 def Platform(self) -> Platforms:
677 return self._platform
679 def LoadDataFromConfiguration(self, config: Configuration) -> None:
680 """Preload versioning information from a configuration file."""
682 self._variables["version"] = self.GetVersion(config.project)
683 self._variables["project"] = self.GetProject(config.project)
684 self._variables["build"] = self.GetBuild(config.build)
686 def CollectData(self) -> None:
687 """Collect versioning information from environment including CI services (if available)."""
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
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()
710 self._variables["git"] = self.GetGitInformation()
711 self._variables["platform"] = self._service.GetPlatform()
712 self._variables["env"] = self.GetEnvironment()
714 self.CalculateData()
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
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")
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 )
734 def GetLastCommit(self) -> Commit:
735 dt = self.GetCommitDate()
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 )
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()
750 return self.ExecuteGitShow(GitShowCommand.CommitHash)
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()
756 datetimeString = self.ExecuteGitShow(GitShowCommand.CommitDateTime)
757 return datetime.fromtimestamp(int(datetimeString))
759 def GetCommitAuthor(self) -> Person:
760 return Person(
761 name=self.GetCommitAuthorName(),
762 email=self.GetCommitAuthorEmail()
763 )
765 def GetCommitAuthorName(self) -> str:
766 return self.ExecuteGitShow(GitShowCommand.CommitAuthorName)
768 def GetCommitAuthorEmail(self) -> str:
769 return self.ExecuteGitShow(GitShowCommand.CommitAuthorEmail)
771 def GetCommitCommitter(self) -> Person:
772 return Person(
773 name=self.GetCommitCommitterName(),
774 email=self.GetCommitCommitterEmail()
775 )
777 def GetCommitCommitterName(self) -> str:
778 return self.ExecuteGitShow(GitShowCommand.CommitCommitterName)
780 def GetCommitCommitterEmail(self) -> str:
781 return self.ExecuteGitShow(GitShowCommand.CommitCommitterEmail)
783 def GetCommitComment(self) -> str:
784 return self.ExecuteGitShow(GitShowCommand.CommitComment)
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()
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 ""
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}")
803 def GetGitRemoteBranch(self, localBranch: Nullable[str] = None) -> str:
804 if self._platform is not Platforms.Workstation:
805 return self._service.GetGitBranch()
807 if localBranch is None:
808 localBranch = self.GetGitLocalBranch()
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
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
824 def GetGitRemote(self, localBranch: Nullable[str] = None) -> str:
825 if localBranch is None:
826 localBranch = self.GetGitLocalBranch()
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
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
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()
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
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
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()
867 if remote is None:
868 remote = self.GetGitRemote()
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
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
884 # self.WriteFatal(f"Message from '{command}': {message}")
886 def GetProject(self, config: Project) -> Project:
887 return Project(
888 name=config.name,
889 version=config.version,
890 variant=config.variant
891 )
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 )
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 )
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
920 def func(s) -> Generator[Tuple[str, Any], None, None]:
921 for e in env.keys():
922 yield e, s.__getattribute__(e)
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 )
936 return Environment(**env)
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__}'.")