Coverage for pyVersioning/__init__.py: 71%
462 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-17 22:22 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-17 22:22 +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.3"
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
221 description.
222 """
224 _hash: str
225 _date: date
226 _time: time
227 _author: Person
228 _committer: Person
229 _comment: str
230 _oneline: Union[str, bool] = False
232 _public: ClassVar[Tuple[str, ...]] = ("hash", "date", "time", "author", "committer", "comment", "oneline")
234 def __init__(self, hash: str, date: date, time: time, author: Person, committer: Person, comment: str) -> None:
235 """
236 Initialize a Git commit.
238 :param hash: The commit's hash.
239 :param date: The commit's date.
240 :param time: The commit's time.
241 :param author: The commit's author.
242 :param committer: The commit's committer.
243 :param comment: The commit's comment.
244 """
245 self._hash = hash
246 self._date = date
247 self._time = time
248 self._author = author
249 self._committer = committer
250 self._comment = comment
252 if comment != "": 252 ↛ exitline 252 didn't return from function '__init__' because the condition on line 252 was always true
253 self._oneline = comment.split("\n")[0]
255 @readonly
256 def hash(self) -> str:
257 """
258 Read-only property to return the hash of the commit.
260 :return: Hash of the commit as string.
261 """
262 return self._hash
264 @readonly
265 def date(self) -> date:
266 """
267 Read-only property to return the date of the commit.
269 :return: Date of the commit as :class:`date`.
270 """
271 return self._date
273 @readonly
274 def time(self) -> time:
275 """
276 Read-only property to return the time of the commit.
278 :return: Time of the commit as :class:`time`.
279 """
280 return self._time
282 @readonly
283 def author(self) -> Person:
284 """
285 Read-only property to return the author of the commit.
287 :return: Author of the commit as :class:`Person`.
288 """
289 return self._author
291 @readonly
292 def committer(self) -> Person:
293 """
294 Read-only property to return the committer of the commit.
296 :return: Committer of the commit as :class:`Person`.
297 """
298 return self._committer
300 @readonly
301 def comment(self) -> str:
302 """
303 Read-only property to return the comment of the commit.
305 :return: Author of the comment as string.
306 """
307 return self._comment
309 @readonly
310 def oneline(self) -> Union[str, bool]:
311 return self._oneline
313 def __str__(self) -> str:
314 """
315 Return a string representation of a commit.
317 :returns: The date, time, committer, hash and one-liner description.
318 """
319 return f"{self._date} {self._time} - {self._committer} - {self._hash} - {self._oneline}"
322@export
323class Git(SelfDescriptive):
324 """
325 This data structure class describes all collected data from Git.
326 """
327 _commit: Commit #: Git commit information
328 _reference: str #: Git reference (branch or tag)
329 _tag: str #: Git tag
330 _branch: str #: Git branch
331 _repository: str #: Git repository URL
333 _public: ClassVar[Tuple[str, ...]] = ("commit", "reference", "tag", "branch", "repository")
335 def __init__(self, commit: Commit, repository: str, tag: str = "", branch: str = "") -> None:
336 self._commit = commit
337 self._tag = tag
338 self._branch = branch
339 self._repository = repository
341 if tag != "": 341 ↛ 343line 341 didn't jump to line 343 because the condition on line 341 was always true
342 self._reference = tag
343 elif branch != "":
344 self._reference = branch
345 else:
346 self._reference = "[Detached HEAD]"
348 @readonly
349 def commit(self) -> Commit:
350 """
351 Read-only property to return the Git commit.
353 :return: The commit information as :class:`Commit`.
354 """
355 return self._commit
357 @readonly
358 def reference(self) -> str:
359 """
360 Read-only property to return the Git reference.
362 A reference is either a branch or tag.
364 :return: The current Git reference as string.
365 """
366 return self._reference
368 @readonly
369 def tag(self) -> str:
370 """
371 Read-only property to return the Git tag.
373 :return: The current Git tag name as string.
374 """
375 return self._tag
377 @readonly
378 def branch(self) -> str:
379 """
380 Read-only property to return the Git branch.
382 :return: The current Git branch name as string.
383 """
384 return self._branch
386 @readonly
387 def repository(self) -> str:
388 """
389 Read-only property to return the Git repository URL.
391 :return: The current Git repository URL as string.
392 """
393 return self._repository
395 def __str__(self) -> str:
396 """
397 Return a string representation of a repository.
399 :returns: The date, time, committer, hash and one-liner description.
400 """
401 return f"{self._repository}:{self._reference} - {self._commit._hash} - {self._commit._oneline}"
404@export
405class Project(SelfDescriptive):
406 """This data structure class describes a project."""
408 _name: str #: Name of the project.
409 _variant: str #: Variant of the project.
410 _version: SemanticVersion #: Version of the project.
412 _public: ClassVar[Tuple[str, ...]] = ("name", "variant", "version")
414 def __init__(
415 self,
416 name: str,
417 version: Union[str, SemanticVersion, None] = None,
418 variant: Nullable[str] = None
419 ) -> None:
420 """Assign fields and convert version string to a `Version` object."""
422 self._name = name if name is not None else ""
423 self._variant = variant if variant is not None else ""
425 if isinstance(version, SemanticVersion):
426 self._version = version
427 elif isinstance(version, str):
428 self._version = SemanticVersion.Parse(version)
429 elif version is None: 429 ↛ exitline 429 didn't return from function '__init__' because the condition on line 429 was always true
430 self._version = SemanticVersion.Parse("v0.0.0")
432 @readonly
433 def name(self) -> str:
434 """
435 Read-only property to return the name of the project.
437 :return: The project's name as string.
438 """
439 return self._name
441 @readonly
442 def variant(self) -> str:
443 """
444 Read-only property to return the variant name of the project.
446 :return: The project's variant as string.
447 """
448 return self._variant
450 @readonly
451 def version(self) -> SemanticVersion:
452 """
453 Read-only property to return the version of the project.
455 :return: The project's version.
456 """
457 return self._version
459 def __str__(self) -> str:
460 """
461 Return a string representation of a project.
463 :returns: The project name, project variant and project version.
464 """
465 return f"{self._name} - {self._variant} {self._version}"
468@export
469class Compiler(SelfDescriptive):
470 _name: str #: Name of the compiler.
471 _version: SemanticVersion #: Version of the compiler.
472 _configuration: str #: Compiler configuration.
473 _options: str #: Compiler options (compiler flags).
475 _public: ClassVar[Tuple[str, ...]] = ("name", "version", "configuration", "options")
477 def __init__(
478 self,
479 name: str,
480 version: Union[str, SemanticVersion] = "",
481 configuration: str = "",
482 options: str = ""
483 ) -> None:
484 """Assign fields and convert version string to a `Version` object."""
486 self._name = name if name is not None else ""
487 self._configuration = configuration if configuration is not None else ""
488 self._options = options if options is not None else ""
490 if isinstance(version, SemanticVersion): 490 ↛ 492line 490 didn't jump to line 492 because the condition on line 490 was always true
491 self._version = version
492 elif isinstance(version, str):
493 self._version = SemanticVersion.Parse(version)
494 elif version is None:
495 self._version = SemanticVersion.Parse("v0.0.0")
497 @readonly
498 def name(self) -> str:
499 """
500 Read-only property to return the name of the compiler.
502 :return: The compiler's name as string.
503 """
504 return self._name
506 @readonly
507 def version(self) -> SemanticVersion:
508 """
509 Read-only property to return the version of the compiler.
511 :return: The compiler's version.
512 """
513 return self._version
515 @readonly
516 def configuration(self) -> str:
517 """
518 Read-only property to return the configuration of the compiler.
520 :return: The compiler's configuration.
521 """
522 return self._configuration
524 @readonly
525 def options(self) -> str:
526 """
527 Read-only property to return the options of the compiler.
529 :return: The compiler's options.
530 """
531 return self._options
533 def __str__(self) -> str:
534 """
535 Return a string representation of a compiler.
537 :returns: The compiler name and compiler version.
538 """
539 return f"{self._name} - {self._version}"
542@export
543class Build(SelfDescriptive):
544 _date: date #: Build date.
545 _time: time #: Build time.
546 _compiler: Compiler #: Use compiler (and options) for the build.
548 _public: ClassVar[Tuple[str, ...]] = ("date", "time", "compiler")
550 def __init__(self, date: date, time: time, compiler: Compiler) -> None:
551 self._date = date
552 self._time = time
553 self._compiler = compiler
555 @readonly
556 def date(self) -> date:
557 """
558 Read-only property to return the date of the build.
560 :return: Date of the build as :class:`date`.
561 """
562 return self._date
564 @readonly
565 def time(self) -> time:
566 """
567 Read-only property to return the time of the build.
569 :return: Time of the build as :class:`date`.
570 """
571 return self._time
573 @readonly
574 def compiler(self) -> Compiler:
575 return self._compiler
578@export
579class Platforms(Enum):
580 """An enumeration of platforms supported by pyTooling."""
581 Workstation = auto() #: A local work station, server, PC or laptop.
582 AppVeyor = auto() #: A CI service operated by `AppVeyor <https://www.appveyor.com/>`__.
583 GitHub = auto() #: A CI service offered by `GitHub <https://github.com/>`__ called GitHub Actions.
584 GitLab = auto() #: A CI service offered by `GitLab <https://about.gitlab.com/>`__.
585 Travis = auto() #: A CI service operated by `Travis <https://www.travis-ci.com/>`__.
588@export
589class Platform(SelfDescriptive):
590 """.. todo:: Platform needs documentation"""
592 _ciService: str
593 _public: ClassVar[Tuple[str, ...]] = ('ci_service', )
595 def __init__(self, ciService: str) -> None:
596 self._ciService = ciService
598 @readonly
599 def ci_service(self) -> str:
600 return self._ciService
603@export
604class BaseService(metaclass=ExtendedType):
605 """Base-class to collect platform and environment information from e.g. environment variables."""
607 # @abstractmethod
608 def GetPlatform(self) -> Platform: # type: ignore[empty-body]
609 """
610 .. todo::
611 getPlatform needs documentation
613 """
616@export
617class GitShowCommand(Enum):
618 CommitDateTime = auto()
619 CommitAuthorName = auto()
620 CommitAuthorEmail = auto()
621 CommitCommitterName = auto()
622 CommitCommitterEmail = auto()
623 CommitHash = auto()
624 CommitComment = auto()
627@export
628class GitHelperMixin(metaclass=ExtendedType, mixin=True):
629 __GIT_SHOW_COMMAND_TO_FORMAT_LOOKUP = {
630 GitShowCommand.CommitHash: "%H",
631 GitShowCommand.CommitDateTime: "%ct",
632 GitShowCommand.CommitAuthorName: "%an",
633 GitShowCommand.CommitAuthorEmail: "%ae",
634 GitShowCommand.CommitCommitterName: "%cn",
635 GitShowCommand.CommitCommitterEmail: "%ce",
636 GitShowCommand.CommitComment: "%B",
637 }
639 def ExecuteGitShow(self, cmd: GitShowCommand, ref: str = "HEAD") -> str:
640 format = f"--format='{self.__GIT_SHOW_COMMAND_TO_FORMAT_LOOKUP[cmd]}'"
642 command = "git"
643 arguments = ("show", "-s", format, ref)
644 try:
645 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE)
646 except CalledProcessError as ex:
647 raise ToolException(f"{command} {' '.join(arguments)}", str(ex))
649 if completed.returncode == 0: 649 ↛ 653line 649 didn't jump to line 653 because the condition on line 649 was always true
650 comment = completed.stdout.decode("utf-8")
651 return comment[1:-2]
652 else:
653 message = completed.stderr.decode("utf-8")
654 raise ToolException(f"{command} {' '.join(arguments)}", message)
657@export
658class Versioning(ILineTerminal, GitHelperMixin):
659 _variables: Dict[str, Any]
660 _platform: Platforms = Platforms.Workstation
661 _service: BaseService
663 def __init__(self, terminal: ILineTerminal) -> None:
664 super().__init__(terminal)
666 self._variables = {
667 "tool": Tool("pyVersioning", SemanticVersion.Parse(f"v{__version__}"))
668 }
670 if "APPVEYOR" in environ: 670 ↛ 671line 670 didn't jump to line 671 because the condition on line 670 was never true
671 self._platform = Platforms.AppVeyor
672 elif "GITHUB_ACTIONS" in environ: 672 ↛ 674line 672 didn't jump to line 674 because the condition on line 672 was always true
673 self._platform = Platforms.GitHub
674 elif "GITLAB_CI" in environ:
675 self._platform = Platforms.GitLab
676 elif "TRAVIS" in environ:
677 self._platform = Platforms.Travis
678 else:
679 self._platform = Platforms.Workstation
681 self.WriteDebug(f"Detected platform: {self._platform.name}")
683 @readonly
684 def Variables(self) -> Dict[str, Any]:
685 return self._variables
687 @readonly
688 def Platform(self) -> Platforms:
689 return self._platform
691 def LoadDataFromConfiguration(self, config: Configuration) -> None:
692 """Preload versioning information from a configuration file."""
694 self._variables["version"] = self.GetVersion(config.project)
695 self._variables["project"] = self.GetProject(config.project)
696 self._variables["build"] = self.GetBuild(config.build)
698 def CollectData(self) -> None:
699 """Collect versioning information from environment including CI services (if available)."""
701 from pyVersioning.AppVeyor import AppVeyor
702 from pyVersioning.CIService import WorkStation
703 from pyVersioning.GitLab import GitLab
704 from pyVersioning.GitHub import GitHub
705 from pyVersioning.Travis import Travis
707 if self._platform is Platforms.AppVeyor: 707 ↛ 708line 707 didn't jump to line 708 because the condition on line 707 was never true
708 self._service = AppVeyor()
709 self._variables["appveyor"] = self._service.GetEnvironment()
710 elif self._platform is Platforms.GitHub: 710 ↛ 713line 710 didn't jump to line 713 because the condition on line 710 was always true
711 self._service = GitHub()
712 self._variables["github"] = self._service.GetEnvironment()
713 elif self._platform is Platforms.GitLab:
714 self._service = GitLab()
715 self._variables["gitlab"] = self._service.GetEnvironment()
716 elif self._platform is Platforms.Travis:
717 self._service = Travis()
718 self._variables["travis"] = self._service.GetEnvironment()
719 else:
720 self._service = WorkStation()
722 self._variables["git"] = self.GetGitInformation()
723 self._variables["platform"] = self._service.GetPlatform()
724 self._variables["env"] = self.GetEnvironment()
726 self.CalculateData()
728 def CalculateData(self) -> None:
729 if self._variables["git"].tag != "": 729 ↛ exitline 729 didn't return from function 'CalculateData' because the condition on line 729 was always true
730 pass
732 def GetVersion(self, config: Project) -> SemanticVersion:
733 if config.version is not None: 733 ↛ 736line 733 didn't jump to line 736 because the condition on line 733 was always true
734 return config.version
735 else:
736 return SemanticVersion.Parse("0.0.0")
738 def GetGitInformation(self) -> Git:
739 return Git(
740 commit=self.GetLastCommit(),
741 tag=self.GetGitTag(),
742 branch=self.GetGitLocalBranch(),
743 repository=self.GetGitRemoteURL()
744 )
746 def GetLastCommit(self) -> Commit:
747 dt = self.GetCommitDate()
749 return Commit(
750 hash=self.GetGitHash(),
751 date=dt.date(),
752 time=dt.time(),
753 author=self.GetCommitAuthor(),
754 committer=self.GetCommitCommitter(),
755 comment=self.GetCommitComment()
756 )
758 def GetGitHash(self) -> str:
759 if self._platform is not Platforms.Workstation: 759 ↛ 762line 759 didn't jump to line 762 because the condition on line 759 was always true
760 return self._service.GetGitHash()
762 return self.ExecuteGitShow(GitShowCommand.CommitHash)
764 def GetCommitDate(self) -> datetime:
765 if self._platform is not Platforms.Workstation: 765 ↛ 768line 765 didn't jump to line 768 because the condition on line 765 was always true
766 return self._service.GetCommitDate()
768 datetimeString = self.ExecuteGitShow(GitShowCommand.CommitDateTime)
769 return datetime.fromtimestamp(int(datetimeString))
771 def GetCommitAuthor(self) -> Person:
772 return Person(
773 name=self.GetCommitAuthorName(),
774 email=self.GetCommitAuthorEmail()
775 )
777 def GetCommitAuthorName(self) -> str:
778 return self.ExecuteGitShow(GitShowCommand.CommitAuthorName)
780 def GetCommitAuthorEmail(self) -> str:
781 return self.ExecuteGitShow(GitShowCommand.CommitAuthorEmail)
783 def GetCommitCommitter(self) -> Person:
784 return Person(
785 name=self.GetCommitCommitterName(),
786 email=self.GetCommitCommitterEmail()
787 )
789 def GetCommitCommitterName(self) -> str:
790 return self.ExecuteGitShow(GitShowCommand.CommitCommitterName)
792 def GetCommitCommitterEmail(self) -> str:
793 return self.ExecuteGitShow(GitShowCommand.CommitCommitterEmail)
795 def GetCommitComment(self) -> str:
796 return self.ExecuteGitShow(GitShowCommand.CommitComment)
798 def GetGitLocalBranch(self) -> str:
799 if self._platform is not Platforms.Workstation: 799 ↛ 802line 799 didn't jump to line 802 because the condition on line 799 was always true
800 return self._service.GetGitBranch()
802 command = "git"
803 arguments = ("branch", "--show-current")
804 try:
805 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE)
806 except CalledProcessError:
807 return ""
809 if completed.returncode == 0:
810 return completed.stdout.decode("utf-8").split("\n")[0]
811 else:
812 message = completed.stderr.decode("utf-8")
813 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}")
815 def GetGitRemoteBranch(self, localBranch: Nullable[str] = None) -> str:
816 if self._platform is not Platforms.Workstation:
817 return self._service.GetGitBranch()
819 if localBranch is None:
820 localBranch = self.GetGitLocalBranch()
822 command = "git"
823 arguments = ("config", f"branch.{localBranch}.merge")
824 try:
825 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE)
826 except CalledProcessError:
827 raise Exception() # XXX: needs error message
829 if completed.returncode == 0:
830 return completed.stdout.decode("utf-8").split("\n")[0]
831 else:
832 message = completed.stderr.decode("utf-8")
833 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}")
834 raise Exception() # XXX: needs error message
836 def GetGitRemote(self, localBranch: Nullable[str] = None) -> str:
837 if localBranch is None:
838 localBranch = self.GetGitLocalBranch()
840 command = "git"
841 arguments = ("config", f"branch.{localBranch}.remote")
842 try:
843 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE)
844 except CalledProcessError:
845 raise Exception() # XXX: needs error message
847 if completed.returncode == 0:
848 return completed.stdout.decode("utf-8").split("\n")[0]
849 elif completed.returncode == 1:
850 self.WriteWarning(f"Branch '{localBranch}' is not pushed to a remote.")
851 return f"(local) {localBranch}"
852 else:
853 message = completed.stderr.decode("utf-8")
854 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}")
855 raise Exception() # XXX: needs error message
857 def GetGitTag(self) -> str:
858 if self._platform is not Platforms.Workstation: 858 ↛ 861line 858 didn't jump to line 861 because the condition on line 858 was always true
859 return self._service.GetGitTag()
861 command = "git"
862 arguments = ("tag", "--points-at", "HEAD")
863 try:
864 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE)
865 except CalledProcessError:
866 raise Exception() # XXX: needs error message
868 if completed.returncode == 0:
869 return completed.stdout.decode("utf-8").split("\n")[0]
870 else:
871 message = completed.stderr.decode("utf-8")
872 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}")
873 raise Exception() # XXX: needs error message
875 def GetGitRemoteURL(self, remote: Nullable[str] = None) -> str:
876 if self._platform is not Platforms.Workstation: 876 ↛ 879line 876 didn't jump to line 879 because the condition on line 876 was always true
877 return self._service.GetGitRepository()
879 if remote is None:
880 remote = self.GetGitRemote()
882 command = "git"
883 arguments = ("config", f"remote.{remote}.url")
884 try:
885 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE)
886 except CalledProcessError:
887 raise Exception() # XXX: needs error message
889 if completed.returncode == 0:
890 return completed.stdout.decode("utf-8").split("\n")[0]
891 else:
892 message = completed.stderr.decode("utf-8")
893 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}")
894 raise Exception() # XXX: needs error message
896 # self.WriteFatal(f"Message from '{command}': {message}")
898 def GetProject(self, config: Project) -> Project:
899 return Project(
900 name=config.name,
901 version=config.version,
902 variant=config.variant
903 )
905 def GetBuild(self, config: Build) -> Build:
906 dt = datetime.now()
907 return Build(
908 date=dt.date(),
909 time=dt.time(),
910 compiler=self.GetCompiler(config.compiler)
911 )
913 def GetCompiler(self, config: Compiler) -> Compiler:
914 return Compiler(
915 name=config.name,
916 version=SemanticVersion.Parse(config.version),
917 configuration=config.configuration,
918 options=config.options
919 )
921 def GetEnvironment(self) -> Any:
922 env = {}
923 for key, value in environ.items():
924 if not key.isidentifier():
925 self.WriteWarning(f"Skipping environment variable '{key}', because it's not a valid Python identifier.")
926 continue
927 key = key.replace("(", "_")
928 key = key.replace(")", "_")
929 key = key.replace(" ", "_")
930 env[key] = value
932 def func(s) -> Generator[Tuple[str, Any], None, None]:
933 for e in env.keys():
934 yield e, s.__getattribute__(e)
936 Environment = make_dataclass(
937 "Environment",
938 [(name, str) for name in env.keys()],
939# bases=(SelfDescriptive,),
940 namespace={
941 "as_dict": lambda self: env,
942 "Keys": lambda self: env.keys(),
943 "KeyValuePairs": lambda self: func(self)
944 },
945 repr=True
946 )
948 return Environment(**env)
950 def FillOutTemplate(self, template: str, **kwargs) -> str:
951 # apply variables
952 try:
953 return template.format(**self._variables, **kwargs)
954 except AttributeError as ex:
955 self.WriteFatal(f"Syntax error in template. Accessing field '{ex.name}' of '{ex.obj.__class__.__name__}'.")