|
|
|
|
@@ -15,36 +15,93 @@
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
from re import Match, search, compile as _compile, match
|
|
|
|
|
from abc import ABC
|
|
|
|
|
from copy import deepcopy
|
|
|
|
|
|
|
|
|
|
_savedFormaterList = set()
|
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
|
|
|
|
|
from typing import Generic, TypeVar, cast
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from typing import Optional, ClassVar, Type, Dict
|
|
|
|
|
|
|
|
|
|
T_ChangelogFormater = TypeVar("T_ChangelogFormater", bound="ChangelogFormater")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ChangelogFormaterRecordType(Klass: type) -> type:
|
|
|
|
|
class _ChangelogFormatersCtx(Generic[T_ChangelogFormater]):
|
|
|
|
|
"""Storage class that manage Formaters"""
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
"""Storage class init method"""
|
|
|
|
|
self._savedFormaterList: set[Type[T_ChangelogFormater]] = set()
|
|
|
|
|
|
|
|
|
|
def add(self, record: Type[T_ChangelogFormater]) -> None:
|
|
|
|
|
"""Add a Formater to the storage class
|
|
|
|
|
Args:
|
|
|
|
|
record: the Formater class to be added
|
|
|
|
|
"""
|
|
|
|
|
self._savedFormaterList.add(record)
|
|
|
|
|
|
|
|
|
|
def remove(self, record: Type[T_ChangelogFormater]) -> None:
|
|
|
|
|
"""Remove a Formater from the storage class
|
|
|
|
|
Args:
|
|
|
|
|
record: the Formater class to be removed
|
|
|
|
|
"""
|
|
|
|
|
self._savedFormaterList.remove(record)
|
|
|
|
|
|
|
|
|
|
def reset(self) -> None:
|
|
|
|
|
"""Reset the storage class"""
|
|
|
|
|
self._savedFormaterList = set()
|
|
|
|
|
|
|
|
|
|
def get(self) -> set[Type[T_ChangelogFormater]]:
|
|
|
|
|
"""Get the storage data set
|
|
|
|
|
Returns:
|
|
|
|
|
The internal storage class (set)
|
|
|
|
|
"""
|
|
|
|
|
return self._savedFormaterList
|
|
|
|
|
|
|
|
|
|
def __copy__(self) -> _ChangelogFormatersCtx[T_ChangelogFormater]:
|
|
|
|
|
"""Copy the class"""
|
|
|
|
|
cls = self.__class__
|
|
|
|
|
result = cls.__new__(cls)
|
|
|
|
|
result._savedFormaterList = self._savedFormaterList
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
def __deepcopy__(self, memo: Dict[int, object]) -> _ChangelogFormatersCtx[T_ChangelogFormater]:
|
|
|
|
|
"""Deep-Copy the class
|
|
|
|
|
Args:
|
|
|
|
|
memo: __deepcopy__ interface impl"""
|
|
|
|
|
result = self.__copy__()
|
|
|
|
|
memo[id(self)] = result
|
|
|
|
|
result._savedFormaterList = self._savedFormaterList.copy()
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ChangelogFormaterRecordType(Klass: Type[T_ChangelogFormater]) -> Type[T_ChangelogFormater]:
|
|
|
|
|
"""Decorator function that registers formater implementation in factory
|
|
|
|
|
Args:
|
|
|
|
|
Klass: class to register in the factory
|
|
|
|
|
Returns:
|
|
|
|
|
untouched class"""
|
|
|
|
|
ChangelogFactory.ar_FormaterKlass.add(Klass)
|
|
|
|
|
ChangelogFactory.RegisterBaseFormater(Klass)
|
|
|
|
|
return Klass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ChangelogFormaterRecordType(Klass: type) -> type:
|
|
|
|
|
def _ChangelogFormaterRecordType(Klass: Type[T_ChangelogFormater]) -> Type[T_ChangelogFormater]:
|
|
|
|
|
"""Internal decorator function that registers formater implementation in factory
|
|
|
|
|
Args:
|
|
|
|
|
Klass: class to register in the factory
|
|
|
|
|
Returns:
|
|
|
|
|
untouched class"""
|
|
|
|
|
_savedFormaterList.add(Klass)
|
|
|
|
|
cast(ChangelogFactory[ChangelogFormater], ChangelogFactory)._ar_SavedFormaterKlass.add(Klass)
|
|
|
|
|
return ChangelogFormaterRecordType(Klass)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChangelogFormater(ABC):
|
|
|
|
|
"""ChangelogFormater class
|
|
|
|
|
|
|
|
|
|
This class is the base formater class.
|
|
|
|
|
This class is the formater base class.
|
|
|
|
|
|
|
|
|
|
This class is for:
|
|
|
|
|
|
|
|
|
|
@@ -58,13 +115,13 @@ class ChangelogFormater(ABC):
|
|
|
|
|
///
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
prefix: None | str = None
|
|
|
|
|
title: None | str = None
|
|
|
|
|
keywords: None | list[str] = None
|
|
|
|
|
priority: int = 0
|
|
|
|
|
prefix: ClassVar[Optional[str]] = None
|
|
|
|
|
title: ClassVar[Optional[str]] = None
|
|
|
|
|
keywords: ClassVar[Optional[list[str]]] = None
|
|
|
|
|
priority: ClassVar[int] = 0
|
|
|
|
|
_lines: list[None | str] = []
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
"""ChangelogFormater class constructor"""
|
|
|
|
|
self._lines: list[None | str] = []
|
|
|
|
|
|
|
|
|
|
@@ -102,7 +159,7 @@ class ChangelogFormater(ABC):
|
|
|
|
|
return full_lines
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def CheckLine(cls, content: str) -> None | re.Match:
|
|
|
|
|
def CheckLine(cls, content: str) -> None | Match[str]:
|
|
|
|
|
"""Check if a line match the current formater (lazy identification)
|
|
|
|
|
|
|
|
|
|
/// warning
|
|
|
|
|
@@ -115,7 +172,7 @@ class ChangelogFormater(ABC):
|
|
|
|
|
Returns:
|
|
|
|
|
match object
|
|
|
|
|
"""
|
|
|
|
|
regex = re.compile(rf"^(?:-\s+)?(?:{cls.prefix})(?:\((.*)\))?(?::)(?:\s*)([^\s].+)")
|
|
|
|
|
regex = _compile(rf"^(?:-\s+)?(?:{cls.prefix})(?:\((.*)\))?(?::)(?:\s*)([^\s].+)")
|
|
|
|
|
_match = regex.match(content)
|
|
|
|
|
return _match
|
|
|
|
|
|
|
|
|
|
@@ -133,55 +190,67 @@ class ChangelogFormater(ABC):
|
|
|
|
|
keyword_list = cls.keywords
|
|
|
|
|
if keyword_list:
|
|
|
|
|
for _keyword in keyword_list:
|
|
|
|
|
if _keyword and re.search(_keyword, content):
|
|
|
|
|
if _keyword and search(_keyword, content):
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChangelogFactory:
|
|
|
|
|
class ChangelogFactory(Generic[T_ChangelogFormater]):
|
|
|
|
|
"""The main changelog class"""
|
|
|
|
|
|
|
|
|
|
ar_FormaterKlass: set[type[ChangelogFormater]] = set()
|
|
|
|
|
ar_Formater: dict[str, ChangelogFormater] = {}
|
|
|
|
|
_ar_SavedFormaterKlass: ClassVar[_ChangelogFormatersCtx[ChangelogFormater]] = _ChangelogFormatersCtx[ChangelogFormater]()
|
|
|
|
|
ar_FormaterKlass: _ChangelogFormatersCtx[T_ChangelogFormater] = _ChangelogFormatersCtx[T_ChangelogFormater]()
|
|
|
|
|
|
|
|
|
|
ar_Formater: Dict[str, T_ChangelogFormater] = {}
|
|
|
|
|
checkCommentPattern: str = r"^[ \t]*(?:\/\/|#)"
|
|
|
|
|
|
|
|
|
|
def __init__(self, ChangelogString: None | str = None):
|
|
|
|
|
def __init__(self, ChangelogString: Optional[str] = None) -> None:
|
|
|
|
|
"""Main ChangelogFormater class constructor
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
ChangelogString: optionnal input string to start with
|
|
|
|
|
ChangelogString: optional input string to be processed
|
|
|
|
|
"""
|
|
|
|
|
self.ar_Formater: dict[str, ChangelogFormater] = {}
|
|
|
|
|
self.ar_FormaterKlass = self.ar_FormaterKlass.copy()
|
|
|
|
|
self.ar_Formater: Dict[str, T_ChangelogFormater] = {}
|
|
|
|
|
self.ar_FormaterKlass = deepcopy(type(self).ar_FormaterKlass)
|
|
|
|
|
|
|
|
|
|
for FormaterKlass in self.ar_FormaterKlass:
|
|
|
|
|
for FormaterKlass in self.ar_FormaterKlass.get():
|
|
|
|
|
self.ar_Formater[FormaterKlass.__name__] = FormaterKlass()
|
|
|
|
|
|
|
|
|
|
# missing mypy coverage here because of internal bad isinstance() handling
|
|
|
|
|
# could be fixed using 'type(ChangelogString) is str' but then quality check will bad
|
|
|
|
|
# so let quality advise
|
|
|
|
|
if isinstance(ChangelogString, str):
|
|
|
|
|
self.ProcessFullChangelog(ChangelogString)
|
|
|
|
|
|
|
|
|
|
def ResetFormaterList(self: None | ChangelogFactory = None) -> None | ChangelogFactory:
|
|
|
|
|
"""Reset the formater class list to original
|
|
|
|
|
|
|
|
|
|
This method can be call both from class or from instance.
|
|
|
|
|
|
|
|
|
|
> If call from class it will reset the whole list.
|
|
|
|
|
|
|
|
|
|
> If call from instance only the instance will be reseted.
|
|
|
|
|
|
|
|
|
|
def ResetFormaterList(self) -> ChangelogFactory[T_ChangelogFormater]:
|
|
|
|
|
"""Reset the formater class list to original (Instance wise)
|
|
|
|
|
Returns:
|
|
|
|
|
self for convenience or None if call from class
|
|
|
|
|
self for convenience
|
|
|
|
|
"""
|
|
|
|
|
if self is not None:
|
|
|
|
|
self.ar_FormaterKlass = _savedFormaterList.copy()
|
|
|
|
|
self.ar_Formater = {}
|
|
|
|
|
for FormaterKlass in self.ar_FormaterKlass:
|
|
|
|
|
self.ar_Formater[FormaterKlass.__name__] = FormaterKlass()
|
|
|
|
|
return self
|
|
|
|
|
ChangelogFactory.ar_FormaterKlass = _savedFormaterList.copy()
|
|
|
|
|
self.ar_FormaterKlass: T_ChangelogFormater = deepcopy(
|
|
|
|
|
cast(_ChangelogFormatersCtx[T_ChangelogFormater], ChangelogFactory._ar_SavedFormaterKlass)
|
|
|
|
|
)
|
|
|
|
|
self.ar_Formater = {}
|
|
|
|
|
for FormaterKlass in self.ar_FormaterKlass.get():
|
|
|
|
|
self.ar_Formater[FormaterKlass.__name__] = FormaterKlass()
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def RegisterBaseFormater(cls, FormaterKlass: Type[T_ChangelogFormater]) -> None:
|
|
|
|
|
"""Register a new formater in the current instance
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
FormaterKlass: class of the formater to be added
|
|
|
|
|
"""
|
|
|
|
|
cls.ar_FormaterKlass.add(FormaterKlass)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def ResetBaseFormaterList(cls) -> None:
|
|
|
|
|
"""Reset the formater class list to original (BaseClass wise)"""
|
|
|
|
|
cls.ar_FormaterKlass = deepcopy(cls._ar_SavedFormaterKlass)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def RegisterFormater(self, FormaterKlass: type[ChangelogFormater]) -> ChangelogFactory:
|
|
|
|
|
def RegisterFormater(self, FormaterKlass: Type[T_ChangelogFormater]) -> ChangelogFactory[T_ChangelogFormater]:
|
|
|
|
|
"""Register a new formater in the current instance
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
@@ -193,7 +262,7 @@ class ChangelogFactory:
|
|
|
|
|
self.ar_Formater[FormaterKlass.__name__] = FormaterKlass()
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def unRegisterFormater(self, FormaterKlass: type[ChangelogFormater]) -> ChangelogFactory:
|
|
|
|
|
def unRegisterFormater(self, FormaterKlass: Type[T_ChangelogFormater]) -> ChangelogFactory[T_ChangelogFormater]:
|
|
|
|
|
"""unRegister a new formater in the current instance
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
@@ -205,7 +274,7 @@ class ChangelogFactory:
|
|
|
|
|
del self.ar_Formater[FormaterKlass.__name__]
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def Clear(self) -> ChangelogFactory:
|
|
|
|
|
def Clear(self) -> ChangelogFactory[T_ChangelogFormater]:
|
|
|
|
|
"""Clear internal memory
|
|
|
|
|
Returns:
|
|
|
|
|
self for convenience
|
|
|
|
|
@@ -221,14 +290,16 @@ class ChangelogFactory:
|
|
|
|
|
If a matching formater is found, line is inserted.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
RawChangelogLine: line to parse
|
|
|
|
|
RawChangelogLine: line to process
|
|
|
|
|
Returns:
|
|
|
|
|
True if successfully matched, False otherwise
|
|
|
|
|
"""
|
|
|
|
|
for formater in sorted(self.ar_Formater.values(), key=lambda x: x.priority):
|
|
|
|
|
content = formater.CheckLine(RawChangelogLine)
|
|
|
|
|
if content is not None:
|
|
|
|
|
formater.PushLine(content.group(2))
|
|
|
|
|
content: Optional[Match[str]] = formater.CheckLine(RawChangelogLine)
|
|
|
|
|
# missing mypy coverage here because of internal bad isinstance() handling AND Match type
|
|
|
|
|
if isinstance(content, Match) and (len(content.groups()) == 2):
|
|
|
|
|
res: str = content.group(2)
|
|
|
|
|
formater.PushLine(res)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
@@ -240,7 +311,7 @@ class ChangelogFactory:
|
|
|
|
|
If a matching formater is found, line is inserted.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
RawChangelogLine: line to parse
|
|
|
|
|
RawChangelogLine: line to process
|
|
|
|
|
Returns:
|
|
|
|
|
True if successfully matched, False otherwise
|
|
|
|
|
"""
|
|
|
|
|
@@ -252,19 +323,22 @@ class ChangelogFactory:
|
|
|
|
|
self.ar_Formater[ChangelogFormater_others.__name__].PushLine(RawChangelogLine)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def ProcessFullChangelog(self, RawChangelogMessage: str) -> ChangelogFactory:
|
|
|
|
|
def ProcessFullChangelog(self, RawChangelogMessage: str) -> ChangelogFactory[T_ChangelogFormater]:
|
|
|
|
|
"""Process all input lines
|
|
|
|
|
|
|
|
|
|
This function handles the main 2-round changes search algo.
|
|
|
|
|
It takes care of search-order and automatically skip any non-relevants message line.
|
|
|
|
|
A non relevant line can be a commented one, or a to short one.
|
|
|
|
|
Available comment patterns are: // and #
|
|
|
|
|
|
|
|
|
|
Available comment patterns are: `// and #`
|
|
|
|
|
|
|
|
|
|
A relevant commit line must contain:
|
|
|
|
|
|
|
|
|
|
- at least 2 words for formal
|
|
|
|
|
- at least 3 words for keywords
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
RawChangelogMessage: The full raw changelog (merged commit-history)
|
|
|
|
|
RawChangelogMessage: The full raw changelog to be processed
|
|
|
|
|
Returns:
|
|
|
|
|
self for convenience
|
|
|
|
|
"""
|
|
|
|
|
@@ -273,7 +347,7 @@ class ChangelogFactory:
|
|
|
|
|
|
|
|
|
|
for line in RawChangelogMessage.split("\n"):
|
|
|
|
|
lineWordsCount = len(line.split())
|
|
|
|
|
if (lineWordsCount > 1) and (not re.match(self.checkCommentPattern, line)):
|
|
|
|
|
if (lineWordsCount > 1) and (not match(self.checkCommentPattern, line)):
|
|
|
|
|
if self._ProcessLineMain(line) is True:
|
|
|
|
|
continue
|
|
|
|
|
if lineWordsCount > 2:
|
|
|
|
|
@@ -293,6 +367,7 @@ class ChangelogFactory:
|
|
|
|
|
"""
|
|
|
|
|
full_changelog = ""
|
|
|
|
|
for formater in sorted(self.ar_Formater.values(), key=lambda x: x.priority, reverse=True):
|
|
|
|
|
# missing mypy coverage here because of internal bad isinstance() handling
|
|
|
|
|
if (include_unknown is False) and (isinstance(formater, ChangelogFormater_others)):
|
|
|
|
|
continue
|
|
|
|
|
full_changelog = full_changelog + formater.Render()
|
|
|
|
|
@@ -353,7 +428,9 @@ for RecordType, Config in {
|
|
|
|
|
}.items():
|
|
|
|
|
# then we instantiate all of them
|
|
|
|
|
_name = f"ChangelogFormater_{RecordType}"
|
|
|
|
|
_tmp = globals()[_name] = type(
|
|
|
|
|
|
|
|
|
|
# can not change globals definition so mypy will keep complaining
|
|
|
|
|
_tmp = type(
|
|
|
|
|
_name,
|
|
|
|
|
(ChangelogFormater,),
|
|
|
|
|
{
|
|
|
|
|
@@ -363,18 +440,19 @@ for RecordType, Config in {
|
|
|
|
|
"priority": Config[0],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
ChangelogFactory.ar_FormaterKlass.add(_tmp)
|
|
|
|
|
_savedFormaterList.add(_tmp)
|
|
|
|
|
globals()[_name] = _tmp
|
|
|
|
|
cast(ChangelogFactory[ChangelogFormater], ChangelogFactory).RegisterBaseFormater(_tmp)
|
|
|
|
|
cast(ChangelogFactory[ChangelogFormater], ChangelogFactory)._ar_SavedFormaterKlass.add(_tmp)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@_ChangelogFormaterRecordType
|
|
|
|
|
class ChangelogFormater_revert(ChangelogFormater):
|
|
|
|
|
"""Revert scope formater"""
|
|
|
|
|
|
|
|
|
|
prefix: str = "revert"
|
|
|
|
|
title: str = "Reverts :back: :"
|
|
|
|
|
keywords: list[str] = ["revert", "fallback"]
|
|
|
|
|
priority: int = 0
|
|
|
|
|
prefix: ClassVar[Optional[str]] = "revert"
|
|
|
|
|
title: ClassVar[Optional[str]] = "Reverts :back: :"
|
|
|
|
|
keywords: ClassVar[Optional[list[str]]] = ["revert", "fallback"]
|
|
|
|
|
priority: ClassVar[int] = 0
|
|
|
|
|
|
|
|
|
|
def RenderLines(self) -> str:
|
|
|
|
|
"""Render all lines
|
|
|
|
|
@@ -391,7 +469,7 @@ class ChangelogFormater_revert(ChangelogFormater):
|
|
|
|
|
class ChangelogFormater_others(ChangelogFormater):
|
|
|
|
|
"""Others / unknown scope formater"""
|
|
|
|
|
|
|
|
|
|
prefix: str = "other"
|
|
|
|
|
title: str = "Others :question: :"
|
|
|
|
|
keywords: list[str] = [""]
|
|
|
|
|
priority: int = -20
|
|
|
|
|
prefix: ClassVar[Optional[str]] = "other"
|
|
|
|
|
title: ClassVar[Optional[str]] = "Others :question: :"
|
|
|
|
|
keywords: ClassVar[Optional[list[str]]] = [""]
|
|
|
|
|
priority: ClassVar[int] = -20
|
|
|
|
|
|