complete documentation

add ability to exclude unknown object
fix the keyword behavior
This commit is contained in:
cclecle
2023-03-25 11:52:53 +00:00
parent 8477cf3bcd
commit 497a2a6eda
3 changed files with 262 additions and 104 deletions

View File

@@ -1,5 +1,23 @@
# Usage
## Theory
This lib will try to extract normalized changes from a given raw history.
_It's up to the user to provide this merged history._
To realize this job, parsing is done in two rounds:
- first round extrats formal changes messages: e.g.: <change_type>(<change_target>): <change_message>
- secound round extracts lines from remaining ones based on keywords dictionnaries
Note:
For formal (1) search, lines must contain at least 2 words
For keywords (2) search, lines must contain at least 3 words
Note:
lines with comment tags are ignored:
- `[space]*//`
- `[space]*#`
## Installation
From pypi repository (prefered):
@@ -15,10 +33,31 @@ From master git repository:
python -m pip install git+https://chacha.ddns.net/gitea/chacha/pychangelogfactory.git@master
## Use in your project
### Sample code
## Import in your project
#from pychangelogfactory import ChangeLogFormater
raw_changelog='''
feat: add a nice feature to the project
style: reindent the full Foo class
security: fix a security leak on the Foo2 component
'''
ChangeLogFormater.FactoryProcessFullChangelog(raw_changelog)
changelog = ChangeLogFormater.RenderFullChangelog()
print(changelog)
Add this line on the top of your python script:
### Output(Raw)
from pychangelogfactory import ChangeLogFormater
#### Features :sparkles::
> feat: add a nice feature to the project
#### Security :shield::
> security: fix a security leak on the Foo2 component
### Output
#### Features :sparkles::
> feat: add a nice feature to the project
#### Security :shield::
> security: fix a security leak on the Foo2 component

View File

@@ -9,7 +9,7 @@
# 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/>.
""" A simple changelog formater that consume merged commit message and produce nice pre-formated changelogs
"""#A simple changelog formater that consume merged commit message and produce nice pre-formated changelogs.
"""
@@ -20,21 +20,26 @@ from abc import ABC
def ChangeLogFormaterRecordType(Klass: type) -> type:
"""decorator helper function to register interface implementation"""
"""Decorator helper function to register interface implementation in factory
Args:
Klass: class to register in the factory
Returns:
untouched class"""
ChangeLogFormater.ar_Klass.append(Klass)
return Klass
class ChangeLogFormater(ABC):
"""the main changelog class that define nearly everythings.
This was supposed to be a very shorty script this is why it is all-in-one...
Factory and base-object are mixed.
"""The main changelog class that define nearly everythings.
this was supposed to be a very shorty script this is why it is all-in-one...
factory and base-object are mixed.
"""
ar_Klass: list[ChangeLogFormater] = []
ar_LinesResult: list[ChangeLogFormater] = []
prefix: str = ""
prefix: str = "^\s+"
title: str = "Others :"
checkCommentPattern: str = r"^[ \t]*(?:\/\/|#)"
keywords: list[str] = []
priority: int = 0
@@ -43,12 +48,18 @@ class ChangeLogFormater(ABC):
self._ChangelogString = ChangelogString.strip()
def RenderLine(self):
"""return a rendered line"""
"""Get a rendered line
Returns:
the rendered line
"""
return self._ChangelogString.strip()
@classmethod
def RenderLines(cls) -> str:
"""render all lines"""
"""Render all lines
Returns:
the rendered lines
"""
changelog_category: str = ""
lines = cls.GetLines()
if len(lines) > 0:
@@ -61,24 +72,41 @@ class ChangeLogFormater(ABC):
return changelog_category
def GetScope(self) -> str:
"""return the current scope (category)"""
"""Return the current scope (category)
Returns:
the current scope
"""
return self._scope if self._scope is not None else ""
@classmethod
def Clear(cls) -> None:
"""clear internal memory"""
"""Clear internal memory"""
ChangeLogFormater.ar_LinesResult = []
@classmethod
def CheckLine(cls, content: str) -> bool:
"""check if a line is in the current scope (lazy identification)"""
def CheckLine(cls, content: str) -> re.Match:
"""Check if a line is in the current scope (lazy identification)
only formal tags are parsed by this function
eg: <change_type>(<change_target>): <change_message>
Args:
content: line to parse
Returns:
match object
"""
regex = re.compile(r"^(?:-\s+)?(?:{0})(?:\((.*)\))?(?::)(?:\s*)([^\s].+)".format(cls.prefix))
_match = regex.match(content)
return _match
@classmethod
def CheckLine_keywords(cls, content: str) -> bool:
"""check if a line is in the current scope (deeper in-word identification)"""
"""Check if a line is in the current scope (deeper in-word identification)
any word in the message can be used to categorize this message.
this function test only for the current category.
Args:
content: line to parse
Returns:
True if a keyword has matched
"""
keyword_list = cls.keywords
for _keyword in keyword_list:
if (_keyword != "") and re.search(_keyword, content):
@@ -87,7 +115,14 @@ class ChangeLogFormater(ABC):
@classmethod
def FactoryProcessLineMain(cls, RawChangelogLine: str) -> ChangeLogFormater:
"""Process a line and look for identified ones"""
"""Process a line and look for identified ones
this function will try to apply every available formater for the 1st search round: formal search
order of search is set according to formater's configuration
Args:
RawChangelogLine: line to parse
Returns:
a corresponding ChangeLogFormater_XXX() object, or a ChangeLogFormater_others()
"""
for Klass in sorted(ChangeLogFormater.ar_Klass, key=lambda x: x.priority):
content = Klass.CheckLine(RawChangelogLine)
if content is not None:
@@ -96,7 +131,14 @@ class ChangeLogFormater(ABC):
@classmethod
def FactoryProcessLineSecond(cls, RawChangelogLine: str) -> ChangeLogFormater:
"""Process a line and look for non-identified ones"""
"""Process a line and look for non-identified ones
this function will try to apply every available formater for the 2ns search round: any keyword
order of search is set according to formater's configuration
Args:
RawChangelogLine: line to parse
Returns:
a corresponding ChangeLogFormater_XXX() object, or a ChangeLogFormater_others()
"""
for Klass in sorted(ChangeLogFormater.ar_Klass, key=lambda x: x.priority, reverse=True):
if Klass.CheckLine_keywords(RawChangelogLine):
return Klass(None, RawChangelogLine)
@@ -105,16 +147,29 @@ class ChangeLogFormater(ABC):
@classmethod
def FactoryProcessFullChangelog(cls, RawChangelogMessage: str) -> list[ChangeLogFormater]:
"""Process all input lines"""
"""Process all input lines
This function handle the main 2-round changes search algo.
Tt 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 #
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)
Returns:
a list of ChangeLogFormater_XXX() object
"""
LinesResult = []
Lines2ndRound = []
for line in RawChangelogMessage.split("\n"):
if line.strip() != "":
lineWordsCount = len(line.split())
if (lineWordsCount > 1) and (not re.match(cls.checkCommentPattern, line)):
res = cls.FactoryProcessLineMain(line)
if res is not ChangeLogFormater_others:
if type(res) is not ChangeLogFormater_others:
LinesResult.append(res)
else:
elif lineWordsCount > 2:
Lines2ndRound.append(line)
for line in Lines2ndRound:
@@ -125,71 +180,88 @@ class ChangeLogFormater(ABC):
@classmethod
def GetLinesOfType(cls, Klass: type) -> list[ChangeLogFormater]:
"""retrieve all lines of specified type"""
"""Retrieve all lines of specified formater type
Args:
Klass: type of formater to get
Returns:
a list of ChangeLogFormater_XXX() object
"""
return [_ for _ in ChangeLogFormater.ar_LinesResult if isinstance(_, Klass)]
@classmethod
def GetLines(cls) -> list[ChangeLogFormater]:
"""retrieve all lines for the current formater"""
"""Retrieve all lines for the current formater
Returns:
a list of ChangeLogFormater_XXX() object
"""
return ChangeLogFormater.GetLinesOfType(cls)
@classmethod
def RenderFullChangelog(cls) -> str:
"""render the main changelog"""
def RenderFullChangelog(cls, include_unknown: bool = False) -> str:
"""Render the main changelog
Args:
include_unknown: includes unknown lines in an Unknown category
Returns:
the final formated changelog
"""
full_changelog = ""
for Klass in sorted(ChangeLogFormater.ar_Klass, key=lambda x: x.priority, reverse=True):
if (include_unknown is False) and (Klass == ChangeLogFormater_others):
continue
full_changelog = full_changelog + Klass.RenderLines()
return full_changelog
# to avoid writing class, they are initialized with the following structure:
# creating category classes: '<NAME>': (priority, ['<prefix1>',...], '<header>')
# creating category classes: '<NAME>': ( priority, ['<prefix1>',...],
# '<header>'
# )
#
# => priority is both for ordering categories in final changelog
# and parsing commit to extract messages
#
for RecordType, Config in {
"break": (
20,
[],
":rotating_light: Breaking changes :rotating_light::",
),
"feat": (20, ["feat", "new", "create", "add"], "Features :sparkles::"),
"fix": (10, ["issue", "problem"], "Fixes :wrench::"),
"security": (20, ["safe", "leak"], "Security :shield::"),
"chore": (
20,
["task", "refactor", "build", "better", "improve"],
"Chore :building_construction::",
),
"perf": (
0,
[
"fast",
],
"Performance Enhancements :rocket::",
),
"wip": (
0,
[
"temp",
],
"Work in progress changes :construction::",
),
"docs": (
0,
[
"doc",
],
"Documentations :book::",
),
"style": (
5,
[
"beautify",
],
"Style :art::",
),
"refactor": (0, [], "Refactorings :recycle::"),
"ci": (0, ["jenkins", "git"], "Continuous Integration :cyclone::"),
"test": (15, ["unittest", "check", r"^(?:\s)*test(?:\s)*$"], "Testings :vertical_traffic_light::"),
"build": (0, ["compile", "version"], "Builds :package:"),
# fmt: off
"break": ( 20, ["break"],
":rotating_light: Breaking changes :rotating_light::",
),
"feat": ( 20, ["feat", "new", "create", "add"],
"Features :sparkles::"
),
"fix": ( 0, ["fix","issue", "problem"],
"Fixes :wrench::"
),
"security": ( 20, ["safe", "leak"],
"Security :shield::"
),
"chore": ( 20, ["task", "refactor", "build", "better", "improve"],
"Chore :building_construction::",
),
"perf": ( 0, ["fast", ],
"Performance Enhancements :rocket::",
),
"wip": ( 0, ["temp", ],
"Work in progress changes :construction::",
),
"docs": ( 0, [ "doc", ],
"Documentations :book::",
),
"style": ( 5, ["beautify", ],
"Style :art::",
),
"refactor": ( 0, [],
"Refactorings :recycle::"
),
"ci": ( 0, ["jenkins", "git"],
"Continuous Integration :cyclone::"
),
"test": ( -5, ["unittest", "check", r"^(?:\s)*test(?:\s)*$"],
"Testings :vertical_traffic_light::"
),
"build": ( 0, ["compile", "version"],
"Builds :package:"
),
# fmt: on
}.items():
# then we instantiate all of them
name = f"ChangeLogFormater_{RecordType}"
@@ -208,7 +280,7 @@ for RecordType, Config in {
@ChangeLogFormaterRecordType
class ChangeLogFormater_revert(ChangeLogFormater):
"""revert scope formater"""
"""Revert scope formater"""
prefix: str = "revert"
title: str = "Reverts :back::"
@@ -216,12 +288,16 @@ class ChangeLogFormater_revert(ChangeLogFormater):
priority: int = 0
def RenderLine(self) -> str:
"""an overloaded RenderLine implementation that adds surrounding '~~'
Returns:
the rendered pattern
"""
return "~~" + super().RenderLine() + "~~"
@ChangeLogFormaterRecordType
class ChangeLogFormater_others(ChangeLogFormater):
"""others / unknown scope formater"""
"""Others / unknown scope formater"""
prefix: str = "other"
title: str = "Others :question::"

View File

@@ -18,82 +18,125 @@ class Testtest_module(unittest.TestCase):
for test in teststrs:
self.assertIn(test, changelog)
def test_simplegeneration_ignored2(self):
raw = "break: testbreak break" + "\n" + "#docs: testdoc doc" + "\n" + "#style: teststyle beautify" + "\n" + "//test: testtest check"
pychangelogfactory.ChangeLogFormater.FactoryProcessFullChangelog(raw)
changelog = pychangelogfactory.ChangeLogFormater.RenderFullChangelog()
self.assertIn("testbreak", changelog)
self.assertNotIn("testdoc", changelog)
self.assertNotIn("teststyle", changelog)
self.assertNotIn("testtest", changelog)
def test_simplegeneration_ignored(self):
raw = "break: testbreak" + "\n" + "#docs: testdoc" + "\n" + "#style: teststyle" + "\n" + "//test: testtest"
pychangelogfactory.ChangeLogFormater.FactoryProcessFullChangelog(raw)
changelog = pychangelogfactory.ChangeLogFormater.RenderFullChangelog()
self.assertIn("testbreak", changelog)
self.assertNotIn("testdoc", changelog)
self.assertNotIn("teststyle", changelog)
self.assertNotIn("testtest", changelog)
def test_simplegeneration_order(self):
raw = "break: testbreak" + "\n" + "docs: testdoc" + "\n" + "style: teststyle" + "\n" + "test: testtest"
pychangelogfactory.ChangeLogFormater.FactoryProcessFullChangelog(raw)
changelog = pychangelogfactory.ChangeLogFormater.RenderFullChangelog().splitlines()
print(changelog)
self.assertIn("testbreak", changelog[1])
self.assertIn("testtest", changelog[3])
self.assertIn("teststyle", changelog[5])
self.assertIn("testdoc", changelog[7])
self.assertIn("teststyle", changelog[3])
self.assertIn("testdoc", changelog[5])
self.assertIn("testtest", changelog[7])
def test_simplegeneration_multiple(self):
raw = "break: testbreak" + "\n" + "docs: testdoc" + "\n" + "style: teststyle"
self.simplegeneration(raw, ["testbreak", "testdoc", "teststyle"])
def test_simplegeneration_breaking(self):
self.simplegeneration("break: teststring", ["teststring"])
self.simplegeneration("test break", ["test break"])
self.simplegeneration("test break dummy1 dummy2", ["test break"])
def test_simplegeneration_features(self):
self.simplegeneration("feat: teststring", ["teststring"])
self.simplegeneration("test feat", ["test feat"])
self.simplegeneration("test new", ["test new"])
self.simplegeneration("test create", ["test create"])
self.simplegeneration("test add", ["test add"])
self.simplegeneration("test feat dummy1 dummy2", ["test feat"])
self.simplegeneration("test new dummy1 dummy2", ["test new"])
self.simplegeneration("test create dummy1 dummy2", ["test create"])
self.simplegeneration("test add dummy1 dummy2", ["test add"])
def test_simplegeneration_fix(self):
self.simplegeneration("fix: teststring", ["teststring"])
self.simplegeneration("test fix", ["test fix"])
self.simplegeneration("test issue", ["test issue"])
self.simplegeneration("test problem", ["test problem"])
self.simplegeneration("test fix dummy1 dummy2", ["test fix"])
self.simplegeneration("test issue dummy1 dummy2", ["test issue"])
self.simplegeneration("test problem dummy1 dummy2", ["test problem"])
def test_simplegeneration_security(self):
self.simplegeneration("security: teststring", ["teststring"])
self.simplegeneration("test safe", ["test safe"])
self.simplegeneration("test leak", ["test leak"])
self.simplegeneration("test safe dummy1 dummy2", ["test safe"])
self.simplegeneration("test leak dummy1 dummy2", ["test leak"])
def test_simplegeneration_task(self):
self.simplegeneration("task: teststring", ["teststring"])
self.simplegeneration("test refactor", ["test refactor"])
self.simplegeneration("test build", ["test build"])
self.simplegeneration("test better", ["test better"])
self.simplegeneration("test improve", ["test improve"])
def test_simplegeneration_chore(self):
self.simplegeneration("chore: teststring", ["teststring"])
self.simplegeneration("chore refactor dummy1 dummy2", ["chore refactor"])
self.simplegeneration("chore build dummy1 dummy2", ["chore build"])
self.simplegeneration("chore better dummy1 dummy2", ["chore better"])
self.simplegeneration("chore improve dummy1 dummy2", ["chore improve"])
def test_simplegeneration_perf(self):
self.simplegeneration("perf: teststring", ["teststring"])
self.simplegeneration("test fast", ["test fast"])
self.simplegeneration("test fast dummy1 dummy2", ["test fast"])
def test_simplegeneration_wip(self):
self.simplegeneration("wip: teststring", ["teststring"])
self.simplegeneration("test temp", ["test temp"])
self.simplegeneration("test temp dummy1 dummy2", ["test temp"])
def test_simplegeneration_docs(self):
self.simplegeneration("docs: teststring", ["teststring"])
self.simplegeneration("test doc", ["test doc"])
self.simplegeneration("test doc dummy1 dummy2", ["test doc"])
def test_simplegeneration_style(self):
self.simplegeneration("style: teststring", ["teststring"])
self.simplegeneration("test beautify", ["test beautify"])
self.simplegeneration("test beautify dummy1 dummy2", ["test beautify"])
def test_simplegeneration_refactor(self):
self.simplegeneration("refactor: teststring", ["teststring"])
def test_simplegeneration_ci(self):
self.simplegeneration("ci: teststring", ["teststring"])
self.simplegeneration("test jenkins", ["test jenkins"])
self.simplegeneration("test git", ["test git"])
self.simplegeneration("test jenkins dummy1 dummy2", ["test jenkins"])
self.simplegeneration("test git dummy1 dummy2", ["test git"])
def test_simplegeneration_test(self):
self.simplegeneration("test: teststring", ["teststring"])
self.simplegeneration("test unittest", ["test unittest"])
self.simplegeneration("test check", ["test check"])
self.simplegeneration("test unittest dummy1 dummy2", ["test unittest"])
self.simplegeneration("test check dummy1 dummy2", ["test check"])
def test_simplegeneration_build(self):
self.simplegeneration("build: teststring", ["teststring"])
self.simplegeneration("test compile", ["test compile"])
self.simplegeneration("test version", ["test version"])
self.simplegeneration("test compile dummy1 dummy2", ["test compile"])
self.simplegeneration("test version dummy1 dummy2", ["test version"])
def test_simplegeneration_revert(self):
self.simplegeneration("revert: teststring", ["~~teststring~~"])
def test_sample(self):
raw_changelog = """
feat: add a nice feature to the project
style: reindent the full Foo class
security: fix a security leak on the Foo2 component
"""
pychangelogfactory.ChangeLogFormater.FactoryProcessFullChangelog(raw_changelog)
changelog = pychangelogfactory.ChangeLogFormater.RenderFullChangelog()
expected_formated = (
"#### Features :sparkles::"
+ "\n"
+ "> feat: add a nice feature to the project"
+ "\n"
+ "#### Security :shield::"
+ "\n"
+ "> security: fix a security leak on the Foo2 component"
+ "\n"
)
self.assertEqual(changelog, expected_formated)