Compare commits

...

18 Commits

Author SHA1 Message Date
cclecle
04578b3066 fix typing + add formated_output option to bump
add more unittest
2023-03-18 19:34:26 +00:00
cclecle
2598a41227 fix kwargs 2023-03-18 09:25:43 +00:00
cclecle
4e43629dfd test docstrings 2023-03-18 02:26:12 +00:00
cclecle
05093566d6 fix doc + try **kwargs 2023-03-18 01:59:21 +00:00
cclecle
f3f3459ccf improve quality 2023-03-18 01:42:53 +00:00
cclecle
df74016945 improve quality 2023-03-18 01:39:25 +00:00
cclecle
bce35f0a60 fix quality 2023-03-18 01:32:44 +00:00
cclecle
39e7d8236c update: update: quality column length check 120 -> 140 2023-03-18 01:23:33 +00:00
cclecle
6a1331d1bf improve quality 2023-03-18 01:22:32 +00:00
cclecle
96250682ec fix missing user/mail for git 2023-03-18 00:59:35 +00:00
cclecle
014e8ac0d2 fix + more unittests 2023-03-18 00:49:33 +00:00
cclecle
b74269b39b more tests and features 2023-03-18 00:10:00 +00:00
cclecle
46ee6dec57 fix: quality score 2023-03-17 18:54:36 +00:00
cclecle
c0d5e4480c fix: code quality 2023-03-17 18:48:18 +00:00
cclecle
2d718d2e4c fix: wrong cls usage in self class 2023-03-17 18:32:38 +00:00
cclecle
525200b1fc trial fix tag_filter 2023-03-17 18:26:11 +00:00
cclecle
70a1c6ef8c start fixing pylint warnings + add docstrings 2023-03-17 18:09:34 +00:00
cclecle
89695decf6 add project files 2023-03-17 08:54:24 +00:00
8 changed files with 1633 additions and 79 deletions

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>{{project_name}}</name>
<name>gitversionhelper</name>
<comment></comment>
<projects>
</projects>

View File

@@ -61,7 +61,7 @@ class quality_check(helper_withresults_base):
'--ignore=_version.py',
'--reports=y',
'--score=yes',
'--max-line-length=120',
'--max-line-length=140',
'src.' + cls.pyproject['project']['name']], exit=False)
with open(cls.get_result_dir()/"report.json","w+", encoding='utf-8') as Outfile:

View File

@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
[tool.setuptools-git-versioning]
enabled = true
dev_template = "{tag}.post{ccount}"
#tag_filter = "^\\d+\\.\\d+\\.\\d+$"
tag_filter = "^\\d+\\.\\d+\\.\\d+$"
[project]
name = "pygitversionhelper"

View File

@@ -19,4 +19,4 @@ except PackageNotFoundError: # pragma: no cover
warnings.warn("can not read __version__, assuming local test context, setting it to ?.?.?")
__version__ = "?.?.?"
from .test_module import test_function
from .gitversionhelper import gitversionhelper, gitversionhelperException

View File

@@ -0,0 +1,539 @@
# pygitversionhelper (c) by chacha
#
# pygitversionhelper is licensed under a
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License.
#
# You should have received a copy of the license along with this
# work. If not, see <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
"""
This project try to help doing handy operations with git when
dealing with project versioning and tags on python project -
or at leat for project using PEP440 versionning or SemVer.
Goal is to keep it compact and not covering too much other things.
This is the reason it is one single file with nested classes.
=> Design is on purpose poorly expandable to keep the scope maintainable.
This library is maid for repository using tag as version.
This module is the main gitversionhelper file, containing all the code.
Read the read me for more information.
Check the unittest s for usage samples.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import os
import subprocess
import re
from copy import copy
import logging
from packaging.version import VERSION_PATTERN as packaging_VERSION_PATTERN
# Only imports the below statements during type checking
if TYPE_CHECKING:
from typing import Union
def _exec(cmd: str, root: str | os.PathLike | None = None) -> list[str]:
"""
helper function to handle system cmd execution
Args:
cmd: command line to be executed
root: root directory where the command need to be executed
Returns:
a list of command's return lines
"""
p = subprocess.run(cmd.split(), text=True, cwd=root, capture_output=True, check=False, timeout=2)
if re.search("not a git repository",p.stderr):
raise gitversionhelper.repository.notAGitRepository()
if re.search("fatal:",p.stderr):
raise gitversionhelper.unknownGITFatalError(p.stderr)
if int(p.returncode) < 0:
raise gitversionhelper.unknownGITError(p.stderr)
lines = p.stdout.splitlines()
return [line.rstrip() for line in lines if line.rstrip()]
class gitversionhelperException(Exception):
"""
general Module Exception
"""
class gitversionhelper: # pylint: disable=too-few-public-methods
"""
main gitversionhelper class
"""
class wrongArguments(gitversionhelperException):
"""
wrong argument generic exception
"""
class unknownGITError(gitversionhelperException):
"""
unknown git error generic exception
"""
class unknownGITFatalError(unknownGITError):
"""
unknown fatal git error generic exception
"""
class repository:
"""
class containing methods focusing on repository
"""
class repositoryException(gitversionhelperException):
"""
generic repository exeption
"""
class notAGitRepository(repositoryException):
"""
not a git repository exception
"""
class repositoryDirty(repositoryException):
"""
dirty repository exception
"""
@classmethod
def isDirty(cls) -> bool:
"""
check if the repository is in dirty state
Returns:
True if it is dirty
"""
return bool(_exec("git status --short"))
class tag:
"""
class containing methods focusing on tags
"""
__OptDict = {"same_branch": "same_branch"}
__validGitTagSort=["","v:refname","-v:refname","taggerdate","committerdate","-taggerdate","-committerdate"]
class tagException(gitversionhelperException):
"""
generic tag exception
"""
class tagNotFound(tagException):
"""
tag not found exception
"""
class moreThanOneTag(tagException):
"""
more than one tag exception
"""
@classmethod
def getTags(cls,sort:str = "taggerdate",**kwargs) -> list[str]:
"""
retrieve all tags from a repository
Args:
sort: sorting constraints (git format)
Returns:
the tags list
"""
if sort not in cls.__validGitTagSort:
raise gitversionhelper.wrongArguments("sort option not in allowed list")
if ((cls.__OptDict["same_branch"] in kwargs) and (kwargs[cls.__OptDict["same_branch"]] is True)):
currentBranch = _exec("git rev-parse --abbrev-ref HEAD")
return list(reversed(_exec(f"git tag --merged {currentBranch[0]} --sort={sort}")))
return list(reversed(_exec(f"git tag -l --sort={sort}")))
@classmethod
def getLastTag(cls,**kwargs) -> str | None:
"""
retrieve the last tag from a repository
Keyword Arguments:
same_branch(bool): force searching only in the same branch
Returns:
the tag
"""
if ((cls.__OptDict["same_branch"] in kwargs) and (kwargs[cls.__OptDict["same_branch"]] is True)):
res = _exec("git describe --tags --first-parent --abbrev=0")
else:
res = _exec("git rev-list --tags --date-order --max-count=1")
if len(res)==1:
res = _exec(f"git describe --tags {res[0]}")
if len(res)==0:
raise cls.tagNotFound("no tag found in commit history")
if len(res)!=1:
raise cls.moreThanOneTag("multiple tags on same commit is unsupported")
return res[0]
@classmethod
def getDistanceFromTag(cls,tag:str=None,**kwargs) -> int:
"""
retrieve the distance between HEAD and tag in the repository
Arguments:
tag: reference tag, if None the most recent one will be used
Keyword Arguments:
same_branch(bool): force searching only in the same branch
Returns:
the tag
"""
if tag is None:
tag = cls.getLastTag(**kwargs)
return int(_exec(f"git rev-list {tag}..HEAD --count")[0])
class version:
"""
class containing methods focusing on versions
"""
__OptDict = { "version_std": "version_std",
"formated_output": "formated_output",
"output_format": "output_format",
"ignore_unknown_tags": "ignore_unknown_tags"}
DefaultInputFormat = "Auto"
VersionStds = { "SemVer" : { "regex" : r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)"\
r"(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"\
r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"\
r"(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$",
"regex_preversion_num": r"(?:\.)(?P<num>(?:\d+(?!\w))+)",
"regex_build_num" : r"(?:\.)(?P<num>(?:\d+(?!\w))+)"
},
"PEP440" : { "regex" : packaging_VERSION_PATTERN,
"Auto" : None
}
}
__versionReseted = False
class versionException(gitversionhelperException):
"""
generic version exception
"""
class noValidVersion(versionException):
"""
no valid version found exception
"""
class MetaVersion:
"""
generic version object
"""
__OptDict = { "bump_type": "bump_type",
"bump_dev_strategy": "bump_dev_strategy",
"formated_output": "formated_output"}
DefaultBumpType = "patch"
BumpTypes = ["major","minor","patch","dev"]
DefaultBumpDevStrategy = "post"
BumpDevStrategys = ["post","pre-patch","pre-minor","pre-major"]
version_std: str = "None"
major: int = 0
minor: int = 1
patch: int = 0
pre_count:int = 0
post_count:int = 0
raw:str = "0.1.0"
def __init__(self,version_std,major=0,minor=1,patch=0,pre_count=0,post_count=0,raw="0.1.0"): #pylint: disable=R0913
self.version_std = version_std
self.major = major
self.minor = minor
self.patch = patch
self.pre_count = pre_count
self.post_count = post_count
self.raw = raw
@classmethod
def _getBumpDevStrategy(cls,**kwargs) -> str:
"""
get selected bump_dev_strategy
Keyword Arguments:
bump_dev_strategy(str): the given bump_dev_strategy (can be None)
Returns:
Kwargs given bump_dev_strategy or the default one.
"""
BumpDevStrategy = cls.DefaultBumpDevStrategy
if cls.__OptDict["bump_dev_strategy"] in kwargs:
if kwargs[cls.__OptDict["bump_dev_strategy"]] in cls.BumpDevStrategys:
BumpDevStrategy = kwargs[cls.__OptDict["bump_dev_strategy"]]
else:
raise gitversionhelper.wrongArguments(f"invalid {cls.__OptDict['bump_type']} requested")
return BumpDevStrategy
@classmethod
def _getBumpType(cls,**kwargs) -> str:
"""
get selected bump_type
Keyword Arguments:
bump_type(str): the given bump_type (can be None)
Returns:
Kwargs given bump_type or the default one.
"""
BumpType = cls.DefaultBumpType
if cls.__OptDict["bump_type"] in kwargs:
if kwargs[cls.__OptDict["bump_type"]] in cls.BumpTypes:
BumpType = kwargs[cls.__OptDict["bump_type"]]
else:
raise gitversionhelper.wrongArguments(f"invalid {cls.__OptDict['bump_type']} requested")
return BumpType
def bump(self,amount:int=1,**kwargs) -> MetaVersion | str : # pylint: disable=R0912
"""
bump the version to the next one
Keyword Arguments:
bump_type(str): the given bump_type (can be None)
bump_dev_strategy(str): the given bump_dev_strategy (can be None)
Returns:
the bumped version
"""
BumpType = self._getBumpType(**kwargs)
BumpDevStrategy=self._getBumpDevStrategy(**kwargs)
_v=copy(self)
if BumpType == "dev":
if BumpDevStrategy == "post":
if _v.pre_count > 0:
_v.pre_count = _v.pre_count + amount
else:
_v.post_count = _v.post_count + amount
elif BumpDevStrategy in ["pre-patch","pre-minor","pre-major"]:
if _v.post_count > 0:
_v.post_count = _v.post_count + amount
else:
if _v.pre_count == 0:
if BumpDevStrategy == "pre-patch":
_v.patch = _v.patch + 1
elif BumpDevStrategy == "pre-minor":
_v.minor = _v.minor + 1
_v.patch = 0
elif BumpDevStrategy == "pre-major":
_v.major = _v.major + 1
_v.minor = 0
_v.patch = 0
_v.pre_count = _v.pre_count + amount
else:
if BumpType == "major":
_v.major = _v.major + amount
elif BumpType == "minor":
_v.minor = _v.minor + amount
elif BumpType == "patch":
_v.patch = _v.patch + amount
_v.pre_count=0
_v.post_count=0
_v.raw=_v.doFormatVersion(**kwargs)
if ((self.__OptDict["formated_output"] in kwargs) and (kwargs[self.__OptDict["formated_output"]] is True)):
return _v.doFormatVersion(**kwargs)
return _v
def doFormatVersion(self,**kwargs) -> str:
"""
output a formated version string
Returns:
formated version string
"""
return gitversionhelper.version.doFormatVersion(self,**kwargs)
@classmethod
def _getVersionStd(cls,**kwargs) -> str:
"""
get selected version_std
Keyword Arguments:
version_std(str): the given version_std (can be None)
Returns:
Kwargs given version_std or the default one.
"""
VersionStd = cls.DefaultInputFormat
if cls.__OptDict["version_std"] in kwargs:
if kwargs[cls.__OptDict["version_std"]] in cls.VersionStds:
VersionStd = kwargs[cls.__OptDict["version_std"]]
else:
raise gitversionhelper.wrongArguments(f"invalid {cls.__OptDict['version_std']} requested")
return VersionStd
@classmethod
def getCurrentVersion(cls,**kwargs) -> MetaVersion | str :
"""
get the current version or bump depending of repository state
Keyword Arguments:
version_std(str): the given version_std (can be None)
same_branch(bool): force searching only in the same branch
formated_output(bool) : output a formated version string
Returns:
the last version
"""
if gitversionhelper.repository.isDirty() is not False:
raise gitversionhelper.repository.repositoryDirty( "The repository is dirty and a current version" \
" can not be generated.")
saved_kwargs = copy(kwargs)
if "formated_output" in kwargs:
del saved_kwargs["formated_output"]
_v = cls.getLastVersion(**saved_kwargs)
if not cls.__versionReseted:
amount = gitversionhelper.tag.getDistanceFromTag(_v.raw,**kwargs)
_v = _v.bump(amount,**saved_kwargs)
if ((cls.__OptDict["formated_output"] in kwargs) and (kwargs[cls.__OptDict["formated_output"]] is True)):
return _v.doFormatVersion(**kwargs)
return _v
@classmethod
def _parseTag(cls,tag,**kwargs):
"""get the last version from tags
Arguments:
tag: the tag to be parsed
Keyword Arguments:
version_std(str): the given version_std (can be None)
ignore_unknown_tags(bool): skip tags with not decoded versions (default to False)
Returns:
the last version
"""
VersionStd = cls._getVersionStd(**kwargs)
bAutoVersionStd = False
if VersionStd == "Auto":
bAutoVersionStd = True
bFound = False
if VersionStd == "SemVer" or (bAutoVersionStd is True) :
_r=re.compile(r"^\s*" + cls.VersionStds["SemVer"]["regex"] + r"\s*$", re.VERBOSE | \
re.IGNORECASE)
_m = re.match(_r,tag)
if not _m:
pass
else:
major, minor, patch = int(_m.group("major")),\
int(_m.group("minor")),\
int(_m.group("patch"))
pre_count = 0
if _pre := _m.group("prerelease"):
if (_match := re.search (cls.VersionStds["SemVer"]["regex_preversion_num"],_pre)) is not None:
pre_count = int(_match.group("num"))
else:
pre_count = 1
post_count = 0
if _post := _m.group("buildmetadata"):
if (_match := re.search (cls.VersionStds["SemVer"]["regex_build_num"],_post)) is not None:
post_count = int(_match.group("num"))
else:
post_count = 1
bFound = True
VersionStd = "SemVer"
if VersionStd == "PEP440" or ( (bAutoVersionStd is True) and (bFound is not True)):
_r=re.compile(r"^\s*" + cls.VersionStds["PEP440"]["regex"] + r"\s*$", re.VERBOSE | \
re.IGNORECASE)
_m = re.match(_r,tag)
if not _m:
pass
else:
ver=_m.group("release").split(".")
ver += ["0"] * (3 - len(ver))
ver[0]=int(ver[0])
ver[1]=int(ver[1])
ver[2]=int(ver[2])
major, minor, patch = tuple(ver)
pre_count = int(_m.group("pre_n")) if _m.group("pre_n") else 0
post_count = int(_m.group("post_n2")) if _m.group("post_n2") else 0
bFound = True
VersionStd = "PEP440"
if not bFound :
raise gitversionhelper.version.noValidVersion("no valid version found in tags")
return cls.MetaVersion(VersionStd, major, minor, patch, pre_count, post_count, tag)
@classmethod
def getLastVersion(cls,**kwargs) -> MetaVersion | str : # pylint: disable=R0914, R0912, R0915
"""get the last version from tags
Keyword Arguments:
version_std(str): the given version_std (can be None)
same_branch(bool): force searching only in the same branch
formated_output(bool) : output a formated version string
ignore_unknown_tags(bool): skip tags with not decoded versions (default to False)
Returns:
the last version
"""
lastTag=cls.MetaVersion.raw
cls.__versionReseted = False
try:
lastTag = gitversionhelper.tag.getLastTag(**kwargs)
except gitversionhelper.tag.tagNotFound:
logging.warning('tag not found, reseting versionning')
cls.__versionReseted = True
_v=None
try:
_v=cls._parseTag(lastTag,**kwargs)
except gitversionhelper.version.noValidVersion:
if ((cls.__OptDict["ignore_unknown_tags"] in kwargs) and (kwargs[cls.__OptDict["ignore_unknown_tags"]] is True)):
tags = gitversionhelper.tag.getTags(sort= "taggerdate",**kwargs)
_v=None
for _tag in tags:
try:
_v=cls._parseTag(_tag,**kwargs)
break;
except:
continue
if _v is None:
raise gitversionhelper.version.noValidVersion()
if ((cls.__OptDict["formated_output"] in kwargs) and (kwargs[cls.__OptDict["formated_output"]] is True)):
return _v.doFormatVersion(**kwargs)
return _v
@classmethod
def doFormatVersion(cls,inputversion:MetaVersion,**kwargs) -> str:
"""
output a formated version string
Args:
inputversion: version to be rendered
Returns:
formated version string
"""
VersionStd = cls._getVersionStd(**kwargs)
if VersionStd=="Auto" :
VersionStd = inputversion.version_std
OutputFormat = None
revpattern=""
revcount=""
post_count = inputversion.post_count
pre_count = inputversion.pre_count
patch = inputversion.patch
if cls.__OptDict["output_format"] in kwargs:
OutputFormat=kwargs[cls.__OptDict["output_format"]]
if OutputFormat is None:
OutputFormat = "{major}.{minor}.{patch}{revpattern}{revcount}"
if post_count > 0 and pre_count > 0:
raise RuntimeError("pre and post release can not be present at the same time")
if VersionStd == "PEP440":
if post_count > 0:
revpattern=".post"
revcount=f"{post_count}"
elif pre_count > 0:
revpattern=".pre"
revcount=f"{pre_count}"
elif VersionStd == "SemVer":
if post_count > 0:
revpattern="+post"
revcount=f".{post_count}"
elif pre_count > 0:
revpattern="-pre"
revcount=f".{pre_count}"
return OutputFormat.format( major=inputversion.major, \
minor=inputversion.minor, \
patch=patch, \
revpattern=revpattern, \
revcount=revcount)

View File

@@ -1,43 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pygitversionhelper (c) by chacha
#
# pygitversionhelper is licensed under a
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License.
#
# You should have received a copy of the license along with this
# work. If not, see <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
"""Phasellus tellus lectus, volutpat eu dapibus ut, suscipit vel augue.
Tips:
Aliquam non leo vel libero sagittis viverra. Quisque lobortis nunc sit amet augue euismod laoreet.
Note:
Maecenas volutpat porttitor pretium. Aliquam suscipit quis nisi non imperdiet.
Note:
Vivamus et efficitur lorem, eget imperdiet tortor. Integer vel interdum sem.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING: # Only imports the below statements during type checking
pass
def test_function(testvar: int) -> int:
""" A test function that return testvar+1 and print "Hello world !"
Proin eget sapien eget ipsum efficitur mollis nec ac nibh.
Note:
Morbi id lectus maximus, condimentum nunc eget, porta felis. In tristique velit tortor.
Args:
testvar: any integer
Returns:
testvar+1
"""
print("Hello world !")
return testvar+1

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +0,0 @@
# pygitversionhelper (c) by chacha
#
# pygitversionhelper is licensed under a
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License.
#
# You should have received a copy of the license along with this
# work. If not, see <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
import unittest
from io import StringIO
from contextlib import redirect_stdout,redirect_stderr
print(__name__)
print(__package__)
from src import pygitversionhelper
class Testtest_module(unittest.TestCase):
def test_version(self):
self.assertNotEqual(pygitversionhelper.__version__,"?.?.?")
def test_test_module(self):
with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr:
self.assertEqual(pygitversionhelper.test_function(41),42)
self.assertEqual(len(capted_stderr.getvalue()),0)
self.assertEqual(capted_stdout.getvalue().strip(),"Hello world !")
self.assertEqual(len(capted_stderr.getvalue()),0)