fix all typing warnings and maximize typing coverage

This commit is contained in:
cclecle
2023-03-26 20:21:22 +01:00
parent 198337e877
commit 407282f70e
4 changed files with 149 additions and 71 deletions

View File

@@ -210,7 +210,7 @@ print(changelog)
### Revert changes
#### Reset to original list class-wise (all modules):
``` py
ChangelogFactory.ResetFormaterList()
ChangelogFactory.ResetBaseFormaterList()
...
```
#### Reset to original list instance-wise:

View File

@@ -27,10 +27,10 @@ class types_check(helper_withresults_base):
"-p",
"src.pychangelogfactory",
# analysis configuration
"--show-traceback",
# "--show-traceback",
"--explicit-package-bases",
"--strict-equality",
"--check-untyped-defs",
# "--strict-equality",
# "--check-untyped-defs",
# reports generation
"--cobertura-xml-report",
str(cls.get_result_dir()),

View File

@@ -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

View File

@@ -13,7 +13,7 @@ from src.pychangelogfactory import ChangelogFormater, ChangelogFactory, Changelo
class Testtest_module(unittest.TestCase):
def setUp(self):
ChangelogFactory.ResetFormaterList()
ChangelogFactory.ResetBaseFormaterList()
def simplegeneration(self, inputstr, teststrs: list[str], withunknown: bool = False):
hdlr = ChangelogFactory()
@@ -182,7 +182,7 @@ class Testtest_module(unittest.TestCase):
class Testtest_module_othercontext(unittest.TestCase):
def setUp(self):
ChangelogFactory.ResetFormaterList()
ChangelogFactory.ResetBaseFormaterList()
def test_custom(self):
"""
@@ -275,7 +275,7 @@ class Testtest_module_othercontext(unittest.TestCase):
"""
5th PART: reseting class list globally
"""
ChangelogFactory.ResetFormaterList()
ChangelogFactory.ResetBaseFormaterList()
hdlr = ChangelogFactory()
hdlr.ProcessFullChangelog(raw_changelog)
changelog = hdlr.RenderFullChangelog(include_unknown=True)
@@ -292,7 +292,7 @@ class Testtest_module_othercontext(unittest.TestCase):
class Testtest_module_othercontext2(unittest.TestCase):
def setUp(self):
ChangelogFactory.ResetFormaterList()
ChangelogFactory.ResetBaseFormaterList()
def test_custom2(self):
class ChangelogFormater_TEST2(ChangelogFormater):