Compare commits

...

55 Commits

Author SHA1 Message Date
27eaa18d9a Merge pull request 'Update Jenkinsfile' (#13) from dev into master
Reviewed-on: https://chacha.ddns.net/gitea/chacha/pychangelogfactory/pulls/13
new-tag:2.2.5
2024-10-12 17:30:41 +02:00
2bdc7a19ab Update Jenkinsfile 2024-10-12 16:40:37 +02:00
64a994de20 Merge pull request 'dev' (#12) from dev into master
Reviewed-on: https://chacha.ddns.net/gitea/chacha/pychangelogfactory/pulls/12
new-tag:2.2.4
2023-11-06 16:17:28 +01:00
cclecle
f1f7046c49 update from last project template 2023-11-06 15:11:39 +00:00
cclecle
0cd8b1404a split quality & types .launch scripts 2023-11-06 15:11:01 +00:00
81dd317aae Merge pull request 'chore: remove useless data dir' (#11) from dev into master
Reviewed-on: https://chacha.ddns.net/gitea/chacha/pychangelogfactory/pulls/11
new-tag:2.2.3
2023-09-30 01:54:20 +02:00
cclecle
3391c7282c chore: remove useless data dir 2023-09-30 00:19:15 +01:00
023638a049 Merge pull request 'fix: switch to pypi version of chacha-cicd-helper' (#10) from dev into master
Reviewed-on: https://chacha.ddns.net/gitea/chacha/pychangelogfactory/pulls/10
new-tag:2.2.2
2023-09-30 00:53:48 +02:00
cclecle
225a700d10 fix: switch to pypi version of chacha-cicd-helper 2023-09-29 23:51:03 +01:00
1fc662bcdb Merge pull request 'chore: switch from local helpers to chacha_cicd_helper' (#9) from dev into master
Reviewed-on: https://chacha.ddns.net/gitea/chacha/pychangelogfactory/pulls/9
new-tag:2.2.1
2023-09-30 00:05:16 +02:00
cclecle
778938a28a chore: switch from local helpers to chacha_cicd_helper 2023-09-29 23:01:09 +01:00
227db21f8d Merge pull request 'dev' (#7) from dev into master
Reviewed-on: https://chacha.ddns.net/gitea/chacha/pychangelogfactory/pulls/7
new-tag:2.2.0
2023-09-24 19:08:51 +01:00
cclecle
48eacc4d69 fix doc gen & cicd helpers 2023-09-24 18:58:07 +01:00
cclecle
807e564380 fix: repository name 2023-09-23 22:17:09 +01:00
cclecle
236efcc258 chore: add py.typed + registering it it pyptoject.toml to enable ext
mypy type hint check
2023-09-23 21:33:02 +01:00
4c10ed1ecd Merge pull request 'apply changes from pygitversionhelper helper' (#6) from dev into master
Reviewed-on: https://chacha.ddns.net/gitea/chacha/pychangelogfactory/pulls/6
new-tag: 2.1.1
2023-03-28 09:49:26 +02:00
cclecle
e4df9f7d77 apply changes from pygitversionhelper helper 2023-03-28 08:45:04 +01:00
e14e867205 Merge pull request 'dev' (#5) from dev into master
Reviewed-on: https://chacha.ddns.net/gitea/chacha/pychangelogfactory/pulls/5
new-tag: 2.1.0
2023-03-27 03:00:07 +02:00
cclecle
418eec803d use typing List type (not list) 2023-03-26 20:43:54 +01:00
cclecle
8a01ad04bb fix quality warnings 2023-03-26 20:34:45 +01:00
cclecle
1be6751d5c fix 2023-03-26 20:25:28 +01:00
cclecle
407282f70e fix all typing warnings and maximize typing coverage 2023-03-26 20:21:22 +01:00
cclecle
198337e877 fix typing coverage and completion 2023-03-26 11:15:20 +01:00
cclecle
d1a34fafe3 add type check ! 2023-03-26 03:14:03 +01:00
cclecle
010145a0a8 applying quality warning messages 2023-03-26 02:38:12 +01:00
cclecle
1ed87d05f6 add class names 2023-03-26 02:35:40 +01:00
cclecle
2d8d2f2a74 fix introduced error in ResetFormaterList() 2023-03-26 02:28:51 +01:00
cclecle
8d68edd4ab fix ChangeLog => Changelog everywhere 2023-03-26 02:25:08 +01:00
cclecle
f0bdbfcdbf complete unit tests coverage
complete all quality warnings
2023-03-26 02:22:46 +01:00
cclecle
408a811b82 improve test and doc 2023-03-26 02:12:13 +01:00
cclecle
28fff91cbe fix wrong name in readme 2023-03-26 00:12:15 +00:00
b919b2e04b Merge pull request 'dev' (#4) from dev into master
Reviewed-on: https://chacha.ddns.net/gitea/chacha/pychangelogfactory/pulls/4
new-tag: 2.0.0
2023-03-26 01:02:53 +01:00
cclecle
792cdd019e fix and complete unittest
update documentation: samples + custom formaters
2023-03-25 23:56:32 +00:00
cclecle
6014d5408a Rework architecture to split formater and fatory
Make factory not static anymore to allow multiple uses
Update doc
2023-03-25 22:41:56 +00:00
4deaa933ee Merge pull request 'dev' (#3) from dev into master
Reviewed-on: https://chacha.ddns.net/gitea/chacha/pychangelogfactory/pulls/3
new-tag: 1.0.2
2023-03-25 21:04:35 +01:00
cclecle
7b8728b6f2 project config 2023-03-25 19:58:57 +00:00
cclecle
c474b8eb82 update launch scripts
remove unused path in eclipse pydev prj
2023-03-25 19:47:06 +00:00
cclecle
49461fa32f add eclipse project run configurations 2023-03-25 17:52:02 +00:00
cclecle
8acb97b7d6 fix sample 2023-03-25 14:54:07 +00:00
cclecle
de26b3ffb8 improve doc generation
make a run configuration for doc
2023-03-25 14:40:26 +00:00
cclecle
8cafaa5ccb fix doc 2023-03-25 12:38:22 +00:00
cclecle
cf81c5f5f7 test doc 2023-03-25 12:33:21 +00:00
cclecle
5d98ed441f test doc 2023-03-25 12:28:33 +00:00
cclecle
1e121fc9c5 test rework doc 2023-03-25 12:21:26 +00:00
cclecle
497a2a6eda complete documentation
add ability to exclude unknown object
fix the keyword behavior
2023-03-25 11:52:53 +00:00
dd37858daa Merge pull request 'dev' (#2) from dev into master
Reviewed-on: https://chacha.ddns.net/gitea/chacha/pychangelogfactory/pulls/2
new-tag: 0.1.1
2023-03-25 00:00:05 +01:00
cclecle
8477cf3bcd fix missing helper fct 2023-03-24 22:54:24 +00:00
cclecle
e2e045ffbd backport features from pygitversionhelper
update readme
create usage
2023-03-24 22:48:48 +00:00
cclecle
d91599b010 update helpers from pygitversionhelper 2023-03-23 01:17:18 +00:00
cclecle
cbfc9949b2 changer path in init 2023-03-23 01:14:21 +00:00
cclecle
e194e9d3c7 typo 2023-03-23 01:05:07 +00:00
cclecle
8339ab5a81 improve code quality 2023-03-23 01:04:55 +00:00
cclecle
a97c521f88 add unittests 2023-03-23 00:47:33 +00:00
cclecle
dc5984d445 apply black 2023-03-22 23:22:29 +00:00
cclecle
9526f3a87a update from pygitversionhelper 2023-03-22 23:05:49 +00:00
27 changed files with 1382 additions and 944 deletions

2
.gitignore vendored
View File

@@ -41,3 +41,5 @@ docs
helpers-results
.coverage
/.mypy_cache/
.coverage
.mypy_cache

View File

@@ -1,13 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python interpreter</pydev_property>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/${PROJECT_DIR_NAME}/src</path>
<path>/${PROJECT_DIR_NAME}</path>
</pydev_pathproperty>
</pydev_project>

228
Jenkinsfile vendored
View File

@@ -6,7 +6,13 @@
// 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 groovy.xml.XmlUtil
import static javax.xml.xpath.XPathConstants.*
import javax.xml.xpath.*
import groovy.xml.DOMBuilder
import groovy.xml.dom.DOMCategory
import java.math.RoundingMode
import java.math.BigDecimal
// configurable settings:
// use to send email if workflow problem
@@ -20,11 +26,18 @@ def _bPreRelease = false
def _bDraft = false
// release content / changelog management
def _bAutoChangelog = true //Not supported yet
def _ReleaseContent_Title = "_CI/CD Automatic Release_"
def _ReleaseContent_Title = "# _CI/CD Automatic Release_"
def bPushMasterOnPypi = true
// full rebuild toogle
def _bFullRebuilt = true
def _MkDocsWebURL = "dabauto--mkdocs-web.dmz.chacha.home/mkdocs-web/"
def _MkDocsWebCredentials = "2c5b684e-3787-4b37-8aca-b3dd4a383fe2"
def _PypiCredentials = "Pypi"
def badge_coverage = addEmbeddableBadgeConfiguration(id: "coverage", subject: "coverage")
def badge_maintainability = addEmbeddableBadgeConfiguration(id: "maintainability", subject: "maintainability")
def badge_quality = addEmbeddableBadgeConfiguration(id: "quality", subject: "quality score")
// commands Helper: /!\ Made for GITEA /!\
String determineRepoUserName() {
@@ -61,6 +74,46 @@ String ExtractBaseVersion(inVersion) {
return matcher[0][1]
}
int GetCoverageValue(String CoverageFilePath,String XPath)
{
//File file = new File(CoverageFilePath)
//coverageReportRaw = file.getText('UTF-8')
coverageReportRaw = readFile(CoverageFilePath)
coverageReport = DOMBuilder.parse(new StringReader(coverageReportRaw), false, false)
coverageReportRoot = coverageReport.documentElement
def xpath = XPathFactory.newInstance().newXPath()
res = xpath.evaluate(XPath,coverageReportRoot,NUMBER)
return res
}
String getColorScale(BigDecimal value)
{
if( value >9) { return "Goldenrod"}
else if( value >6) { return "seagreen"}
else if( value >4) { return "orange"}
else if( value >2) { return "darkred"}
else { return "dimgrey"}
}
String getColorScale_reversed(BigDecimal value)
{
if( value >9) { return "dimgrey"}
else if( value >6) { return "darkred"}
else if( value >4) { return "orange"}
else if( value >2) { return "seagreen"}
else { return "Goldenrod"}
}
int GetCoverageValue_lines_valid(String CoverageFilePath) { return GetCoverageValue(CoverageFilePath,"/coverage/@lines-valid") }
int GetCoverageValue_lines_covered(String CoverageFilePath) { return GetCoverageValue(CoverageFilePath,"/coverage/@lines-covered") }
int GetCoverageValue_line_rate(String CoverageFilePath) { return GetCoverageValue(CoverageFilePath,"/coverage/@line-rate") }
int GetCoverageValue_branches_valid(String CoverageFilePath) { return GetCoverageValue(CoverageFilePath,"/coverage/@branches-valid") }
int GetCoverageValue_branches_covered(String CoverageFilePath) { return GetCoverageValue(CoverageFilePath,"/coverage/@branches-covered") }
int GetCoverageValue_branch_rate(String CoverageFilePath) { return GetCoverageValue(CoverageFilePath,"/coverage/@branch-rate") }
int GetCoverageValue_complexity(String CoverageFilePath) { return GetCoverageValue(CoverageFilePath,"/coverage/@complexity") }
pipeline {
// for Docker based build (preferable)
@@ -95,6 +148,7 @@ pipeline {
PY_PROJECT_NAME = "__NOTSET__"
PY_PROJECT_VERSION = "__NOTSET__"
PY_PROJECT_VERSION_STRIPPED = "__NOTSET__"
CHANGELOG = "__NOTSET__"
}
stages {
@@ -129,22 +183,25 @@ pipeline {
sh("virtualenv --pip=embed --setuptools=embed --wheel=embed --no-periodic-update --activators bash,python TEST_ENV")
sh("virtualenv --pip=embed --setuptools=embed --wheel=embed --no-periodic-update --activators bash,python TOOLS_ENV")
sh(". ~/BUILD_ENV/bin/activate && pip install --upgrade setuptools build pip copier jinja2-slug toml")
sh(". ~/BUILD_ENV/bin/activate && pip install --upgrade setuptools build pip")
sh(". ~/BUILD_ENV/bin/activate && pip install --upgrade 'copier==9.*' jinja2-slug toml")
sh(". ~/TEST_ENV/bin/activate && pip install --upgrade pip")
sh(". ~/TOOLS_ENV/bin/activate && pip install simple_rest_client requests")
sh(". ~/TOOLS_ENV/bin/activate && pip install --upgrade pip")
sh(". ~/TOOLS_ENV/bin/activate && pip install simple_rest_client requests twine packaging")
script {
if(_PROJECT_NAME!="pygitversionhelper") {
sh(". ~/TOOLS_ENV/bin/activate && pip install git+https://chacha.ddns.net/gitea/chacha/pygitversionhelper.git@master")
sh(". ~/TOOLS_ENV/bin/activate && pip install pygitversionhelper")
}
else
{
//TODO: need to install pygitversionhelper deps from a better way...
sh(". ~/TOOLS_ENV/bin/activate && pip install packaging")
if(_PROJECT_NAME!="pychangelogfactory") {
sh(". ~/TOOLS_ENV/bin/activate && pip install pychangelogfactory")
}
}
sh("git config --global user.email $_MaintainerEmail")
sh("git config --global user.name $_MaintainerName")
sh("git config --global init.defaultBranch master")
}
}
@@ -163,6 +220,32 @@ pipeline {
withCredentials([usernamePassword(credentialsId: _SCMCredentials, passwordVariable: 'GIT_PASSWORD', usernameVariable: 'GIT_USERNAME')]) {
sh("git remote set-url origin https://${GIT_USERNAME}:${GIT_PASSWORD}@chacha.ddns.net/gitea/${_PROJECT_USER_NAME}/${_PROJECT_NAME}.git")
}
CHANGELOG = sh(script: """#!/bin/sh -
|. ~/TOOLS_ENV/bin/activate
|exec python - << '__EOWRAPPER__'
|
|import re
|
|try:
| from pychangelogfactory import ChangelogFactory
|except ImportError:
| from src.pychangelogfactory import ChangelogFactory
|
|try:
| from pygitversionhelper import gitversionhelper
|except ImportError:
| from src.pygitversionhelper import gitversionhelper
|
|
|LastTag=gitversionhelper.tag.getLastTag(same_branch=True)
|CommitHistory=gitversionhelper.commit.getMessagesSinceTag(LastTag, merged_output=True, ignore_merged=True)
|Changelog = ChangelogFactory(CommitHistory).RenderFullChangelog(include_unknown=True)
|print(Changelog.replace("\\n","\\n\\n"))
|
|__EOWRAPPER__
""".stripMargin(),
returnStdout: true).trim()
if(_GIT_BRANCH=="master") {
if(sh(returnStdout: true, script: "git tag --points-at HEAD").trim().isEmpty()) {
@@ -262,7 +345,7 @@ pipeline {
|'''.strip()
|
|import copier
|copier.run_auto("./", "../_gitrepo",vcs_ref="HEAD",use_prereleases=True,defaults=True,cleanup_on_error=False)
|copier.run_copy("./", "../_gitrepo",vcs_ref="HEAD",use_prereleases=True,defaults=True,cleanup_on_error=False,unsafe=True)
|
|__EOWRAPPER__
""".stripMargin())
@@ -321,7 +404,7 @@ pipeline {
def wheelPath = findFiles(glob: "**/dist/*.whl")[0]
echo "wheel artifact path: $wheelPath"
// install the package, with *test* optionnal packages, as user
sh(". ~/TEST_ENV/bin/activate && pip install --find-links dist/ ${PY_PROJECT_NAME} .[test,coverage-check,quality-check,type-check,doc-gen]")
sh(". ~/TEST_ENV/bin/activate && pip install --find-links dist/ ${PY_PROJECT_NAME} .[test,coverage-check,quality-check,type-check,doc-gen,complexity-check]")
}
}
}
@@ -330,19 +413,44 @@ pipeline {
stage("CheckCode") {
steps {
dir("gitrepo") {
sh(". ~/TEST_ENV/bin/activate && python -m helpers --type-check --quality-check")
sh(". ~/TEST_ENV/bin/activate && python -m chacha_cicd_helper --typecheck --qualitycheck")
script {
def jsonObj = readJSON file: "helpers-results/cl_quality_check/metrics.json"
quality_score = new BigDecimal(jsonObj["GlobalScore"])
sz_quality_score = quality_score.setScale(2, RoundingMode.HALF_EVEN).toString()
badge_quality.setStatus(sz_quality_score)
badge_quality.setColor(getColorScale(quality_score))
}
sh(". ~/TEST_ENV/bin/activate && python -m chacha_cicd_helper --complexitycheck")
}
}
post {
always {
dir("gitrepo") {
publishHTML([
reportDir: "helpers-results/quality_check",
reportFiles: "report.html",
reportName: "quality-report",
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true])
dir("gitrepo") {
//publish coverage
recordCoverage( sourceDirectories: [[path: 'src']],
tools: [[parser: 'COBERTURA', pattern: 'helpers-results/cl_types_check/cobertura.xml']],
id: 'COBERTURA', name: 'COBERTURA Coverage',
sourceCodeRetention: 'EVERY_BUILD',)
//add type check to junit result set
junit 'helpers-results/cl_types_check/junit.xml'
//publish html reports files
publishHTML([
reportDir: "helpers-results/cl_quality_check",
reportFiles: "report.html",
reportName: "quality-report",
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true])
publishHTML([
reportDir: "helpers-results/cl_types_check",
reportFiles: "index.html",
reportName: "types_check",
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true])
}
}
}
@@ -351,7 +459,7 @@ pipeline {
steps {
plot([ csvFileName: 'plot-df7f03dc-8146-11ed-a1eb-0242ac120002.csv',
csvSeries: [[ file: 'gitrepo/helpers-results/quality_check/metrics_GlobalScore.csv', inclusionFlag: 'OFF', url: '']],
csvSeries: [[ file: 'gitrepo/helpers-results/cl_quality_check/metrics_GlobalScore.csv', inclusionFlag: 'OFF', url: '']],
group: 'metrics',
title: 'code quality score',
style: 'line',
@@ -361,7 +469,7 @@ pipeline {
yaxisMinimum: '0'])
plot([ csvFileName: 'plot-c731cc84-8145-11ed-a1eb-0242ac120002.csv',
csvSeries: [[ file: 'gitrepo/helpers-results/quality_check/metrics_rawpercent.csv', inclusionFlag: 'OFF', url: '']],
csvSeries: [[ file: 'gitrepo/helpers-results/cl_quality_check/metrics_rawpercent.csv', inclusionFlag: 'OFF', url: '']],
group: 'metrics',
title: 'code composition (%)',
style: 'stackedArea',
@@ -371,7 +479,7 @@ pipeline {
yaxisMinimum: '0'])
plot([ csvFileName: 'plot-cac33982-8145-11ed-a1eb-0242ac120002.csv',
csvSeries: [[ file: 'gitrepo/helpers-results/quality_check/metrics_Statistics.csv', inclusionFlag: 'OFF', url: '']],
csvSeries: [[ file: 'gitrepo/helpers-results/cl_quality_check/metrics_Statistics.csv', inclusionFlag: 'OFF', url: '']],
group: 'metrics',
title: 'general statistics',
style: 'line',
@@ -379,35 +487,68 @@ pipeline {
numBuilds: ''])
plot([ csvFileName: 'plot-cddaced2-8145-11ed-a1eb-0242ac120002.csv',
csvSeries: [[ file: 'gitrepo/helpers-results/quality_check/metrics_MessagesCat.csv', inclusionFlag: 'OFF', url: '']],
csvSeries: [[ file: 'gitrepo/helpers-results/cl_quality_check/metrics_MessagesCat.csv', inclusionFlag: 'OFF', url: '']],
group: 'metrics',
title: 'quality warnings',
style: 'stackedArea',
keepRecords: true,
numBuilds: ''])
plot([ csvFileName: 'plot-4ceb9ee2-ca78-11ed-afa1-0242ac120002.csv',
csvSeries: [[ file: 'gitrepo/helpers-results/cl_complexity_check/MI.csv', inclusionFlag: 'INCLUDE_BY_STRING',exclusionValues: 'MeanMaintainability', url: '']],
group: 'metrics',
title: 'maintainability',
style: 'stackedArea',
keepRecords: true,
numBuilds: ''])
}
}
stage("RunUnitTests") {
steps {
dir("gitrepo") {
sh(". ~/TEST_ENV/bin/activate && python -m helpers --unit-test --coverage-check")
sh(". ~/TEST_ENV/bin/activate && python -m chacha_cicd_helper --unittest --coveragecheck")
script {
unit_test_full_name__html=findFiles(glob: "helpers-results/unit_test_full/*.html")[0].getName()
unit_test_full_name__html=findFiles(glob: "helpers-results/cl_unit_test_full/*.html")[0].getName()
println unit_test_full_name__html
unit_test_full_name__xml=findFiles(glob: "helpers-results/unit_test_full/*.xml")[0].getName()
unit_test_full_name__xml=findFiles(glob: "helpers-results/cl_unit_test_full/*.xml")[0].getName()
println unit_test_full_name__xml
coverage_report_path = "helpers-results/cl_unit_test_coverage/test_coverage.xml"
println GetCoverageValue_lines_valid(coverage_report_path)
println GetCoverageValue_lines_covered(coverage_report_path)
println GetCoverageValue_line_rate(coverage_report_path)
println GetCoverageValue_branches_valid(coverage_report_path)
println GetCoverageValue_branches_covered(coverage_report_path)
println GetCoverageValue_branch_rate(coverage_report_path)
println GetCoverageValue_complexity(coverage_report_path)
full_rate = new BigDecimal( 10*(GetCoverageValue_line_rate(coverage_report_path) + GetCoverageValue_branch_rate(coverage_report_path)) / 2 )
sz_full_rate = full_rate.setScale(2, RoundingMode.HALF_EVEN).toString()
badge_coverage.setStatus(sz_full_rate)
badge_coverage.setColor(getColorScale(full_rate))
//badge_maintainability
records = readCSV file: 'helpers-results/cl_complexity_check/MI.csv'
maintainability = records[1][1]
badge_maintainability.setStatus(maintainability)
if ( maintainability == 'D') { badge_maintainability.setColor( "dimgrey")}
else if( maintainability == 'C') { badge_maintainability.setColor( "darkred")}
else if( maintainability == 'B') { badge_maintainability.setColor( "orange")}
else if( maintainability == 'A') { badge_maintainability.setColor( "seagreen")}
else if( maintainability == 'A+') { badge_maintainability.setColor( "Goldenrod")}
}
}
}
post {
always {
dir("gitrepo") {
junit 'helpers-results/unit_test/*.xml'
junit 'helpers-results/cl_unit_test/*.xml'
// using cobertura format (= coverage xml format)
publishCoverage adapters: [cobertura(mergeToOneReport: true, path: "helpers-results/unit_test_coverage/test_coverage.xml")]
recordCoverage(tools: [[parser: 'COBERTURA', pattern: 'helpers-results/cl_unit_test_coverage/test_coverage.xml']])
publishHTML([
reportDir: "helpers-results/unit_test_coverage",
reportDir: "helpers-results/cl_unit_test_coverage",
reportFiles: "index.html",
reportName: "coverage-report-html",
allowMissing: false,
@@ -415,7 +556,7 @@ pipeline {
keepAll: true])
publishHTML([
reportDir: "helpers-results/unit_test_full",
reportDir: "helpers-results/cl_unit_test_full",
reportFiles: unit_test_full_name__html,
reportName: "test-reports-full",
allowMissing: false,
@@ -429,15 +570,14 @@ pipeline {
stage("GenDOC") {
steps {
dir("gitrepo") {
//--doc-gen-pdf
sh(". ~/TEST_ENV/bin/activate && python -m helpers --doc-gen --doc-gen-pdf")
sh(". ~/TEST_ENV/bin/activate && python -m chacha_cicd_helper --docgen --docgenpdf")
}
}
post {
always {
dir("gitrepo") {
publishHTML([
reportDir: "helpers-results/doc_gen/site",
reportDir: "helpers-results/cl_doc_gen/site",
reportFiles: "index.html",
reportName: "doc-html",
allowMissing: false,
@@ -469,6 +609,7 @@ pipeline {
|from simple_rest_client.api import API
|from simple_rest_client.resource import Resource
|
|
|try:
| from pygitversionhelper import gitversionhelper
|except ImportError:
@@ -515,6 +656,13 @@ pipeline {
|ReleaseContent = "${_ReleaseContent_Title}" + "\\n" \\
| + "\\n" \\
| + "Reference documentation: [mkdocs page](https://chacha.ddns.net/mkdocs-web/${_PROJECT_USER_NAME}/${PY_PROJECT_NAME}/${_GIT_BRANCH}/${PY_PROJECT_VERSION_STRIPPED}/) "
|
|Changelog='''${CHANGELOG}'''
|
|ReleaseContent = ReleaseContent + "\\n"+ "\\n"+ "## Changelog:\\n" + Changelog
|
|if not Changelog:
| ReleaseContent = ReleaseContent + "code/project maintainance"
|
|data={
| "body": ReleaseContent,
@@ -534,11 +682,11 @@ pipeline {
|
|data = {
| "name": "Documentation (pdf)",
| 'attachment': ("${PY_PROJECT_NAME}_${PY_PROJECT_VERSION}_UserManual.pdf", open("helpers-results/doc_gen/site/pdf/manual.pdf", 'rb')),
| 'attachment': ("${PY_PROJECT_NAME}_${PY_PROJECT_VERSION}_UserManual.pdf", open("helpers-results/cl_doc_gen/site/pdf/manual.pdf", 'rb')),
|}
|GiteaApi.assets.post("${_PROJECT_USER_NAME}","${PY_PROJECT_NAME}",new_release_id,files=data)
|
|shutil.make_archive("doc", 'zip', "helpers-results/doc_gen/site")
|shutil.make_archive("doc", 'zip', "helpers-results/cl_doc_gen/site")
|reqData={
| "SECRET": "${MKDOCSTOKEN}",
| "USER": "${_PROJECT_USER_NAME}",
@@ -554,6 +702,14 @@ pipeline {
|__EOWRAPPER__
""".stripMargin())
}
if((_GIT_BRANCH=="master") && (bPushMasterOnPypi)) {
withCredentials([usernamePassword( credentialsId: _PypiCredentials, passwordVariable: 'PYPI_PASSWORD', usernameVariable: 'PYPI_USERNAME')]) {
sh(script: """#!/bin/sh -
|. ~/TOOLS_ENV/bin/activate
|exec twine upload -u ${PYPI_USERNAME} -p ${PYPI_PASSWORD} --non-interactive --disable-progress-bar dist/*
""".stripMargin())
}
}
}
}
}

View File

@@ -1,48 +1,16 @@
![](https://chacha.ddns.net/jenkins/buildStatus/icon?subject=status&status=active&color=seagreen)
![](https://chacha.ddns.net/jenkins/buildStatus/icon?subject=doc&status=MkDocs&color=blue)
![](https://chacha.ddns.net/jenkins/buildStatus/icon?subject=jenkins-unittest&job={{repository}}-{{branch}})
![](https://chacha.ddns.net/jenkins/buildStatus/icon?subject=licence&status=CC%20BY-NC-SA%204.0&color=blue)
![](https://chacha.ddns.net/jenkins/buildStatus/icon?job={{repository}}-{{branch}}&build=0&config=coverage)
![](https://chacha.ddns.net/jenkins/buildStatus/icon?job={{repository}}-{{branch}}&build=0&config=maintainability)
![](https://chacha.ddns.net/jenkins/buildStatus/icon?job={{repository}}-{{branch}}&build=0&config=quality)
![](https://chacha.ddns.net/jenkins/buildStatus/icon?subject=licence&status=CC%20BY-NC-SA%204.0&color=teal)
![](docs-static/Library.jpg)
# Python project template
# pyChangelogFactory
A nice template to start blank python projets.
This template automate a lot of handy things and allow CI/CD automatic releases generation.
A simple changelog formater that consume raw changes list text and produce nice pre-formated changelogs.
The input data mainly aim to be a merged commit report.
It is also collectings data to feed Jenkins build.
Checkout [Latest Documentation](https://chacha.ddns.net/mkdocs-web/chacha/pychachadummyproject/{{branch}}/latest/).
## Features
### Generic pipeline skeleton:
- Prepare
- GetCode
- BuildPackage
- Install
- CheckCode
- PlotMetrics
- RunUnitTests
- GenDOC
- PostRelease
### CI/CD Environment
- Jenkins
- Gitea (with patch for dynamic Readme variables: https://chacha.ddns.net/gitea/chacha/GiteaMarkupVariable)
- Docker
- MkDocsWeb
### CI/CD Helper libs
- VirtualEnv
- Changelog generation based on commits
- copier
- pylint + pylint_json2html
- mypy
- unittest + xmlrunner + junitparser + junit2htmlreport
- mkdocs
### Python project
- Full .toml implementation
- .whl automatic generation
- dynamic versionning using git repository
- embedded unit-test
Checkout [Latest Documentation](https://chacha.ddns.net/mkdocs-web/chacha/pychangelogfactory/master/latest/).

17
RUN_complexity.launch Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.python.pydev.debug.regularLaunchConfigurationType">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/pychangelogfactory/helpers_proxy"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="2"/>
</listAttribute>
<stringAttribute key="org.eclipse.debug.ui.ATTR_CONSOLE_ENCODING" value="UTF-8"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:pychangelogfactory/helpers_proxy}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_OTHER_WORKING_DIRECTORY" value=""/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="--complexitycheck"/>
<stringAttribute key="org.python.pydev.debug.ATTR_INTERPRETER" value="__default"/>
<stringAttribute key="org.python.pydev.debug.ATTR_PROJECT" value="pychangelogfactory"/>
<stringAttribute key="process_factory_id" value="org.python.pydev.debug.processfactory.PyProcessFactory"/>
</launchConfiguration>

17
RUN_mkdocs.launch Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.python.pydev.debug.regularLaunchConfigurationType">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/pychangelogfactory/helpers_proxy"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="2"/>
</listAttribute>
<stringAttribute key="org.eclipse.debug.ui.ATTR_CONSOLE_ENCODING" value="UTF-8"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:pychangelogfactory/helpers_proxy}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_OTHER_WORKING_DIRECTORY" value=""/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="--docgen --docgenpdf"/>
<stringAttribute key="org.python.pydev.debug.ATTR_INTERPRETER" value="__default"/>
<stringAttribute key="org.python.pydev.debug.ATTR_PROJECT" value="pychangelogfactory"/>
<stringAttribute key="process_factory_id" value="org.python.pydev.debug.processfactory.PyProcessFactory"/>
</launchConfiguration>

17
RUN_quality.launch Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.python.pydev.debug.regularLaunchConfigurationType">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/pychangelogfactory/helpers_proxy"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="2"/>
</listAttribute>
<stringAttribute key="org.eclipse.debug.ui.ATTR_CONSOLE_ENCODING" value="UTF-8"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:pychangelogfactory/helpers_proxy}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_OTHER_WORKING_DIRECTORY" value=""/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="--qualitycheck"/>
<stringAttribute key="org.python.pydev.debug.ATTR_INTERPRETER" value="__default"/>
<stringAttribute key="org.python.pydev.debug.ATTR_PROJECT" value="pychangelogfactory"/>
<stringAttribute key="process_factory_id" value="org.python.pydev.debug.processfactory.PyProcessFactory"/>
</launchConfiguration>

17
RUN_types.launch Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.python.pydev.debug.regularLaunchConfigurationType">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/pychangelogfactory/helpers_proxy"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="2"/>
</listAttribute>
<stringAttribute key="org.eclipse.debug.ui.ATTR_CONSOLE_ENCODING" value="UTF-8"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:pychangelogfactory/helpers_proxy}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_OTHER_WORKING_DIRECTORY" value=""/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="--typecheck"/>
<stringAttribute key="org.python.pydev.debug.ATTR_INTERPRETER" value="__default"/>
<stringAttribute key="org.python.pydev.debug.ATTR_PROJECT" value="pychangelogfactory"/>
<stringAttribute key="process_factory_id" value="org.python.pydev.debug.processfactory.PyProcessFactory"/>
</launchConfiguration>

17
RUN_unittest.launch Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.python.pydev.debug.regularLaunchConfigurationType">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/pychangelogfactory/helpers_proxy"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="2"/>
</listAttribute>
<stringAttribute key="org.eclipse.debug.ui.ATTR_CONSOLE_ENCODING" value="UTF-8"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:pychangelogfactory/helpers_proxy}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_OTHER_WORKING_DIRECTORY" value=""/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="--unittest --coveragecheck"/>
<stringAttribute key="org.python.pydev.debug.ATTR_INTERPRETER" value="__default"/>
<stringAttribute key="org.python.pydev.debug.ATTR_PROJECT" value="pychangelogfactory"/>
<stringAttribute key="process_factory_id" value="org.python.pydev.debug.processfactory.PyProcessFactory"/>
</launchConfiguration>

View File

@@ -1,16 +1,230 @@
# Usage
## Pulvinar dolor
Donec dapibus est fermentum justo volutpat condimentum. Integer quis nunc neque. Donec dictum vehicula justo, in facilisis ex tincidunt in.
Vivamus sollicitudin sem dui, id mollis orci facilisis ut. Proin sed pulvinar dolor. Donec volutpat commodo urna imperdiet pulvinar. Fusce eget aliquam risus.
Vivamus viverra luctus ex, in finibus mi. Nullam elementum dapibus mollis. Ut suscipit volutpat ex, quis feugiat lacus consectetur eu.
## Theory
## Condimentum faucibus
Quisque auctor egestas sem, luctus suscipit ex maximus vitae. Duis facilisis augue et condimentum faucibus.
Donec cursus, enim a sagittis egestas, lectus lorem eleifend libero, at tincidunt leo magna at libero.
Nunc eros velit, suscipit luctus tempor vel, finibus et est. Curabitur efficitur pretium pulvinar.
Donec urna lectus, vulputate quis turpis sed, placerat congue urna. Phasellus aliquet fermentum quam, non auctor elit porta nec. Morbi eu ligula at nisl ultricies condimentum vitae id ante.
This lib will try to extract normalized changes from a given raw history.
## Aliquam lacinia
In volutpat lorem ex, et fringilla nibh faucibus quis. Mauris et arcu elementum, auctor dui vitae, egestas arcu. Duis sit amet aliquam quam.
Phasellus a odio turpis. Etiam tristique mi eu enim varius, eget facilisis est vestibulum. Aliquam lacinia nec purus sed luctus. Cras at laoreet erat.
_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
/// warning | searching policy
When formal search (1), _lines must contain at least 2 words_
When keywords search (2), _lines must contain at least 3 words_
///
/// note | ignored lines
lines with comment tags are ignored:
- `[0..N space]//`
- `[0..N space]#`
///
## Installation
From pypi repository (prefered):
python -m pip install pychangelogfactory
From downloaded .whl file:
python -m pip install pychangelogfactory-<VERSION>-py3-none-any.whl
From master git repository:
python -m pip install git+https://chacha.ddns.net/gitea/chacha/pychangelogfactory.git@master
## Use in your project
### Sample code
``` py
from pychangelogfactory import ChangelogFactory
raw_changelog = (
"feat: add a nice feature to the project\n"
"style: reindent the full Foo class\n"
"security: fix a security issue on the Foo2 component\n"
"security: fix another security problem on the Foo2 component\n"
"improve core performances by reducing complexity\n"
"some random changes in the text content\n"
)
hdlr = ChangelogFactory()
hdlr.ProcessFullChangelog(self.raw_changelog)
changelog = hdlr.RenderFullChangelog()
print(changelog)
```
#### Or shorted version:
``` py
hdlr = ChangelogFactory(self.raw_changelog)
changelog = hdlr.RenderFullChangelog()
```
#### Or one-liner version:
``` py
changelog = ChangelogFactory(self.raw_changelog).RenderFullChangelog()
```
### Output(Raw)
#### Features :sparkles: :
> add a nice feature to the project
#### Security :shield: :
> security: fix a security issue on the Foo2 component
> security: fix another security problem on the Foo2 component
#### Performance Enhancements :rocket: :
> improve core performances by reducing complexity
#### Style :art: :
> reindent the full Foo class
### Output (rendered)
#### Features :sparkles: :
> add a nice feature to the project
#### Security :shield: :
> security: fix a security issue on the Foo2 component
> security: fix another security problem on the Foo2 component
#### Performance Enhancements :rocket: :
> improve core performances by reducing complexity
#### Style :art: :
> reindent the full Foo class
### Options
#### Display unknown messages types
``` py
from pychangelogfactory import ChangelogFormater
raw_changelog = (
"feat: add a nice feature to the project\n"
"style: reindent the full Foo class\n"
"security: fix a security issue on the Foo2 component\n"
"security: fix another security problem on the Foo2 component\n"
"improve core performances by reducing complexity\n"
"some random changes in the text content\n"
)
changelog = ChangelogFactory(self.raw_changelog).RenderFullChangelog(include_unknown=True)
print(changelog)
```
### Output (rendered)
#### Features :sparkles::
> add a nice feature to the project
#### Security :shield::
> fix a security issue on the Foo2 component
> fix another security problem on the Foo2 component
#### Performance Enhancements :rocket::
> improve core performances by reducing complexity
#### Style :art::
> reindent the full Foo class
#### Others :question::
> some random changes in the text content
## Supported types
| Type/Tag | Priority | Keywords | Title | Class Name |
|-----------|----------|----------------------------------------|-------------------------------------------------------|-------------------------------|
| break | 20 | break | :rotating_light: Breaking changes :rotating_light: : | `ChangelogFormater_break` |
| feat | 20 | feat, new, create, add | Features :sparkles: : | `ChangelogFormater_feat` |
| fix | 0 | fix, issue, problem | Fixes :wrench: : | `ChangelogFormater_fix` |
| security | 20 | safe, leak | Security :shield: : | `ChangelogFormater_security` |
| chore | 10 | task, refactor, build, better, improve | Chore :building_construction: : | `ChangelogFormater_chore` |
| perf | 15 | fast, perf | Performance Enhancements :rocket: : | `ChangelogFormater_perf` |
| wip | 0 | temp | Work in progress changes :construction: : | `ChangelogFormater_wip` |
| doc | 0 | doc, manual | Documentations :book: : | `ChangelogFormater_wip` |
| style | 5 | beautify | Style :art: : | `ChangelogFormater_style` |
| refactor | 0 | | Refactorings :recycle: : | `ChangelogFormater_refactor` |
| ci | 0 | jenkins, git | Continuous Integration :cyclone: : | `ChangelogFormater_ci` |
| test | -5 | unittest, check, testing | Testings :vertical_traffic_light: : | `ChangelogFormater_test` |
| build | 0 | compile, version | Builds :package: : | `ChangelogFormater_build` |
| revert | 0 | revert, fallback | Reverts :back: : | `ChangelogFormater_revert` |
| other | -20 | | Others :question: : | `ChangelogFormater_others` |
## Add new types
New formaters can be easily added by subclassing `ChangelogFormater`:
### Inject custom formater locally (prefered way)
``` py
from pychangelogfactory import ChangelogFormater,ChangelogFactory
class ChangelogFormater_others(ChangelogFormater):
"""My formater"""
prefix: str = "mytag"
title: str = "My Title :"
keywords: list[str] = ["foo","42"]
priority: int = 10
hdlr = ChangelogFactory()
hdlr.RegisterFormater(ChangelogFormater_others)
...
```
### Inject custom formater module-wide
``` py
from pychangelogfactory import ChangelogFormater,ChangelogFormaterRecordType
@ChangelogFormaterRecordType
class ChangelogFormater_others(ChangelogFormater):
"""My formater"""
prefix: str = "mytag"
title: str = "My Title :"
keywords: list[str] = ["foo","42"]
priority: int = 10
hdlr = ChangelogFactory()
...
```
/// note | Scope
This will register your new formater for all next new factories, maybe not only in your own code !
///
### Test
``` py
raw_changelog = ("mytag: add a nice feature to the project\n"
"foo modification in my file\n"
"need 42 coffee\n"
)
hdlr = ChangelogFactory(raw_changelog)
changelog = hdlr.RenderFullChangelog(include_unknown=True)
print(changelog)
```
### Output
#### My Title :
> add a nice feature to the project
> foo modification in my file
> need 42 coffee
### Revert changes
#### Reset to original list class-wise (all modules):
``` py
ChangelogFactory.ResetBaseFormaterList()
...
```
#### Reset to original list instance-wise:
``` py
hdlr = ChangelogFactory()
hdlr.ResetFormaterList()
...
```
#### Removing a specific formater:
``` py
hdlr = ChangelogFactory()
hdlr.unRegisterFormater(ChangelogFormater_others)
...
```
/// warning
There is no way to remove a specific formater class-wise (all modules) except using ResetFormaterList().
///

1
helpers/.gitignore vendored
View File

@@ -1 +0,0 @@
/.mypy_cache/

View File

@@ -1,7 +0,0 @@
# pyChaChaDummyProject (c) by chacha
#
# pyChaChaDummyProject 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/>.

View File

@@ -1,104 +0,0 @@
# pyChaChaDummyProject (c) by chacha
#
# pyChaChaDummyProject 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/>.
from __future__ import annotations
from typing import TYPE_CHECKING
from pathlib import Path
import tomli
import argparse
import os
import logging
import sys
if __package__=="helpers":
# when calling the module from: > python -m helpers
from .types_check import types_check
from .quality_check import quality_check
from .unit_test import unit_test
from .doc_gen import doc_gen
from .changelog_gen import changelog_gen
else:
# when calling the __main__.py file (from IDE)
from helpers.types_check import types_check
from helpers.quality_check import quality_check
from helpers.unit_test import unit_test
from helpers.doc_gen import doc_gen
from helpers.changelog_gen import changelog_gen
logging.getLogger().setLevel(logging.INFO)
if __name__ == "__main__":
project_rootdir_path=Path(__file__).parent.parent.absolute()
with open(project_rootdir_path / "pyproject.toml", mode="rb") as fp:
pyproject = tomli.load(fp)
parser = argparse.ArgumentParser( prog = 'continuous-integration-helper',
description = 'A tiny set of scripts to help continous integration on python')
parser.add_argument('-tc', '--type-check', dest='typecheck', action='store_true', help='enable static typing check')
parser.add_argument('-ut', '--unit-test', dest='unittest', action='store_true', help='enable unit-test')
parser.add_argument('-cc', '--coverage-check', dest='coveragecheck', action='store_true', help='enable unit-test coverage check (requires unit-test)')
parser.add_argument('-qc', '--quality-check', dest='qualitycheck', action='store_true', help='enable code quality check')
parser.add_argument('-dg', '--doc-gen', dest='docgen', action='store_true', help='enable documentation generation using MkDoc')
parser.add_argument('-pdf', '--doc-gen-pdf', dest='docgenpdf', action='store_true', help='enable pdf documentation export (requires doc-gen)')
parser.add_argument('-clg', '--changelog-gen', dest='changeloggen', action='store_true', help='enable changelog generation')
args = parser.parse_args()
##################################
# Dev / Debug forced toogles
#
#--------------------------------
#
#args.typecheck = True
#args.qualitycheck = True
#args.unittest = True
#args.coveragecheck = True
#args.docgen = True
#args.docgenpdf = True
#args.changeloggen = True
helpers = []
if args.typecheck == True:
helpers.append(types_check)
if args.unittest == True:
helpers.append(unit_test)
if args.coveragecheck == True:
if args.unittest == True:
unit_test.enable_coverage_check = True
else:
raise RuntimeError("unit-test is required to enable coverage-check")
if args.qualitycheck == True:
helpers.append(quality_check)
if args.docgen == True:
helpers.append(doc_gen)
if args.docgenpdf==True:
if args.docgen == True:
doc_gen.enable_gen_pdf = True
else:
raise RuntimeError("doc-gen is required to enable doc-gen-pdf")
if args.changeloggen == True:
helpers.append(changelog_gen)
for helper in helpers:
helper.set_context(project_rootdir_path,pyproject)
helper.reset_result_dir()
helper.do_job()

View File

@@ -1,24 +0,0 @@
# pyChaChaDummyProject (c) by chacha
#
# pyChaChaDummyProject 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/>.
from __future__ import annotations
from typing import TYPE_CHECKING
#from pathlib import Path
#import os
#import datetime
from .helper_base import helper_base
class changelog_gen(helper_base):
@classmethod
def do_job(cls):
pass

View File

@@ -1,103 +0,0 @@
# pyChaChaDummyProject (c) by chacha
#
# pyChaChaDummyProject 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/>.
from __future__ import annotations
from typing import TYPE_CHECKING
import shutil
import os
import sys
import subprocess
from pathlib import Path
from distutils.dir_util import copy_tree
import yaml
try:
from yaml import CLoader as Loader, CDumper as Dumper
except ImportError:
from yaml import Loader, Dumper
from .helper_base import helper_withresults_base
class doc_gen(helper_withresults_base):
enable_gen_pdf: bool = False
@classmethod
def do_job(cls):
print(cls.project_rootdir_path)
print()
# create doc root dir
doc_path = cls.project_rootdir_path/"docs"
cls._reset_dir(doc_path)
site_path = cls.get_result_dir()/"site"
cls._reset_dir(site_path)
# copy files from main project dir
shutil.copyfile(str(cls.project_rootdir_path/"README.md"),str(doc_path/"README.md"))
shutil.copyfile(str(cls.project_rootdir_path/"LICENSE.md"),str(doc_path/"LICENSE.md"))
# copy files from static-doc dir
copy_tree(str(cls.project_rootdir_path/"docs-static"),str(doc_path))
# generating API doc + nav from python docstrings
reference_path = doc_path / "reference"
cls._reset_dir(reference_path)
for path in sorted((cls.project_rootdir_path / "src").rglob("*.py")):
module_path = path.relative_to(cls.project_rootdir_path/ "src").with_suffix("")
doc_path = path.relative_to(cls.project_rootdir_path/ "src").with_suffix(".md")
full_doc_path = Path(reference_path, doc_path)
parts = list(module_path.parts)
if parts[-1] == "__init__":
parts = parts[:-1]
elif parts[-1] == "__main__":
continue
cls._reset_dir(os.path.dirname(full_doc_path))
with open(full_doc_path, "w+") as fd:
identifier = "src."+".".join(parts)
print("::: " + identifier, file=fd)
cmdopts = [f"{sys.executable}","-m","mkdocs","-v","build","--site-dir",str(site_path),"--clean"]
# little hack here, to enable / disable pdf generation using own class config
# => reason is mkdocs seems to try loading the plugin even if we disable it, so we need to
# manually process the configuration file.
mkdocsCfg=None
with open(cls.project_rootdir_path / "mkdocs.yml",'r') as mkdocsCfgFile:
mkdocsCfg = yaml.load(mkdocsCfgFile, Loader=yaml.SafeLoader)
if cls.enable_gen_pdf==True:
mkdocsCfg['plugins'].append({ 'with-pdf':{
'cover_subtitle': 'User Manual',
'cover_logo': str(cls.project_rootdir_path / 'docs-static' / 'Library.jpg'),
'verbose': False,
'media_type': 'print',
'exclude_pages': ['LICENSE'],
'output_path': str(site_path / 'pdf' / 'manual.pdf')
}})
else:
for subelem in mkdocsCfg['plugins']:
if isinstance(subelem,dict) :
if 'with-pdf' in subelem.keys():
mkdocsCfg['plugins'].remove(subelem)
break
with open(cls.project_rootdir_path / "mkdocs.yml",'w') as mkdocsCfgFile:
mkdocsCfgFile.write(yaml.dump(mkdocsCfg, Dumper=Dumper,default_flow_style=False, sort_keys=False))
res=cls.run_cmd(cmdopts)
print(res.decode())
print(' !! done')

View File

@@ -1,72 +0,0 @@
# pyChaChaDummyProject (c) by chacha
#
# pyChaChaDummyProject 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/>.
from __future__ import annotations
from typing import TYPE_CHECKING
from abc import ABC,abstractmethod
import os
from pathlib import Path
import subprocess
if TYPE_CHECKING: # Only imports the below statements during type checking
from typing import Union
class helper_base(ABC):
project_rootdir_path: Union[Path,None] = None
pyproject: Union[dict,None] = None
current_dir: Union[Path,None] = None
@classmethod
def set_context(cls,project_rootdir_path:Path, pyproject:dict):
cls.project_rootdir_path = project_rootdir_path
cls.pyproject = pyproject
cls.current_dir = Path(__file__).parent.absolute()
@classmethod
def get_result_dir(cls):
return None
@staticmethod
def _reset_dir(dirpath:Path):
dirpath = Path(dirpath)
if not os.path.exists(dirpath):
os.makedirs(dirpath)
[f.unlink() for f in Path(dirpath).glob("*") if f.is_file()]
@classmethod
def reset_result_dir(cls):
result_dir = cls.get_result_dir()
if result_dir != None:
cls._reset_dir(result_dir)
@classmethod
@abstractmethod
def do_job(cls):
raise NotImplementedError()
@classmethod
def run_cmd_(cls, cmdarray):
process = subprocess.run(cmdarray, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, check=True)
return process.stdout
@classmethod
def run_cmd(cls, cmdarray):
p = subprocess.run(cmdarray, capture_output=True)
print(p.stdout)
print(p.stderr)
return p.stdout
class helper_withresults_base(helper_base):
helper_results_dir: Union[Path,None] = None
@classmethod
def get_result_dir(cls):
if cls.helper_results_dir==None:
cls.helper_results_dir=cls.__name__
return Path(__file__).parent.parent.absolute() / "helpers-results" / cls.helper_results_dir

View File

@@ -1,221 +0,0 @@
# pyChaChaDummyProject (c) by chacha
#
# pyChaChaDummyProject 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/>.
from __future__ import annotations
from typing import TYPE_CHECKING
from contextlib import redirect_stdout
from io import StringIO
import re
import json
from enum import Enum
from contextlib import suppress
import sys
import pandas
import csv
import copy
from pylint.lint import Run as pylint_Run
import pylint_json2html
from .helper_base import helper_withresults_base
class PyLintMetricNotFound(Warning):
pass
class quality_check(helper_withresults_base):
PylintMessageList=dict()
@classmethod
def GetPylintMessageList(cls):
Messagelist=dict()
regex = r"^:([a-zA-Z-]+) \(([^\)]+)\)"
for line in cls.run_cmd([sys.executable,"-m","pylint","--list-msgs"]).splitlines():
if res:=re.search(regex,line.decode()):
Messagelist[res.group(1)]=res.group(2)
cls.PylintMessageList = Messagelist
@staticmethod
def TryExtractPYReportMetric(line: str,tag: str):
regex=f"^(?:\|{tag}\s*\|)(\d+)(?=\s*|)"
if res:=re.search(regex,line):
return float(res.group(1))
raise PyLintMetricNotFound()
@classmethod
def do_job(cls):
print("checking code quality ...")
cls.GetPylintMessageList()
RES_all=dict()
with StringIO() as StdOutput:
JsonContent=""
with redirect_stdout(StdOutput):
pylint_Run(['--output-format=json,parseable',
'--disable=invalid-name',
'--ignore=_version.py',
'--reports=y',
'--score=yes',
'--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:
# hacky way of exctracting json + having overall score...
class TScanState(Enum):
TEXT_REPORT = 1
JSON_REPORT = 2
OTHER_REPORT_START = 3
OTHER_REPORT_STATISTICS = 4
OTHER_REPORT_METRICS = 5
OTHER_REPORT_DUPLICATION = 6
OTHER_REPORT_MESSAGES_CAT = 7
OTHER_REPORT_MESSAGES = 8
OTHER_REPORT_END = 99
RES_all['Statistics'] = dict()
RES_all['RawMetrics'] = dict()
RES_all['RawMetricsPercent'] = dict()
RES_all['Duplication'] = dict()
RES_all['MessagesCat'] = dict()
RES_all['Messages'] = dict()
RES_all['GlobalScore'] = -999
RES_all['NbAnalysedStatments'] = -999
RES_all['NbAnalysedLines'] = -999
ScanState = TScanState.TEXT_REPORT
for line in StdOutput.getvalue().split('\n'):
print(line)
if ScanState == TScanState.TEXT_REPORT:
# ignoring this part, we need json
if line=='[':
JsonContent+=line
ScanState = TScanState.JSON_REPORT
elif line=='[]':
JsonContent+=line
ScanState = TScanState.OTHER_REPORT_START
elif ScanState == TScanState.JSON_REPORT:
JsonContent+=line
if line==']':
ScanState = TScanState.OTHER_REPORT_START
elif ScanState == TScanState.OTHER_REPORT_START:
if res:=re.search(r"^(\d+)(?= statements analysed.)",line):
RES_all['NbAnalysedStatments'] = float(res.group(1))
if line == "Statistics by type":
ScanState = TScanState.OTHER_REPORT_STATISTICS
elif ScanState == TScanState.OTHER_REPORT_STATISTICS:
if res:=re.search(r"^(\d+)(?= lines have been analyzed)",line):
RES_all['NbAnalysedLines' ]= float(res.group(1))
elif line == "Raw metrics":
ScanState = TScanState.OTHER_REPORT_METRICS
else:
with suppress(PyLintMetricNotFound): RES_all['Statistics']['module'] = cls.TryExtractPYReportMetric(line,"module")
with suppress(PyLintMetricNotFound): RES_all['Statistics']['class'] = cls.TryExtractPYReportMetric(line,"class")
with suppress(PyLintMetricNotFound): RES_all['Statistics']['method'] = cls.TryExtractPYReportMetric(line,"method")
with suppress(PyLintMetricNotFound): RES_all['Statistics']['function'] = cls.TryExtractPYReportMetric(line,"function")
elif ScanState == TScanState.OTHER_REPORT_METRICS:
if line == "Duplication":
RES_all['RawMetricsPercent']['code'] = RES_all['RawMetrics']['code'] / RES_all['NbAnalysedLines' ]
RES_all['RawMetricsPercent']['docstring'] = RES_all['RawMetrics']['docstring'] / RES_all['NbAnalysedLines' ]
RES_all['RawMetricsPercent']['comment'] = RES_all['RawMetrics']['comment'] / RES_all['NbAnalysedLines' ]
RES_all['RawMetricsPercent']['empty'] = RES_all['RawMetrics']['empty'] / RES_all['NbAnalysedLines' ]
ScanState = TScanState.OTHER_REPORT_DUPLICATION
else:
with suppress(PyLintMetricNotFound): RES_all['RawMetrics']['code'] = cls.TryExtractPYReportMetric(line,"code")
with suppress(PyLintMetricNotFound): RES_all['RawMetrics']['docstring'] = cls.TryExtractPYReportMetric(line,"docstring")
with suppress(PyLintMetricNotFound): RES_all['RawMetrics']['comment'] = cls.TryExtractPYReportMetric(line,"comment")
with suppress(PyLintMetricNotFound): RES_all['RawMetrics']['empty'] = cls.TryExtractPYReportMetric(line,"empty")
elif ScanState == TScanState.OTHER_REPORT_DUPLICATION:
if line == "Messages by category":
ScanState = TScanState.OTHER_REPORT_MESSAGES_CAT
else:
with suppress(PyLintMetricNotFound): RES_all['Duplication']['NbDupLines'] = cls.TryExtractPYReportMetric(line,"nb duplicated lines")
with suppress(PyLintMetricNotFound): RES_all['Duplication']['PersentDuplicatedLines'] = cls.TryExtractPYReportMetric(line,"percent duplicated lines")
elif ScanState == TScanState.OTHER_REPORT_MESSAGES_CAT:
if line == "Messages":
ScanState = TScanState.OTHER_REPORT_MESSAGES
else:
with suppress(PyLintMetricNotFound): RES_all['MessagesCat']['Convention'] = cls.TryExtractPYReportMetric(line,"convention")
with suppress(PyLintMetricNotFound): RES_all['MessagesCat']['Refactor'] = cls.TryExtractPYReportMetric(line,"refactor")
with suppress(PyLintMetricNotFound): RES_all['MessagesCat']['Warning'] = cls.TryExtractPYReportMetric(line,"warning")
with suppress(PyLintMetricNotFound): RES_all['MessagesCat']['Error'] = cls.TryExtractPYReportMetric(line,"error")
elif ScanState == TScanState.OTHER_REPORT_MESSAGES:
# approx match because the number of '-' depend on screen width..
if line.startswith("--------"):
ScanState = TScanState.OTHER_REPORT_END
else:
for PylintMessage in cls.PylintMessageList.keys():
with suppress(PyLintMetricNotFound): RES_all['Messages'][PylintMessage] = cls.TryExtractPYReportMetric(line,PylintMessage)
elif ScanState == TScanState.OTHER_REPORT_END:
if res:=re.search(r"(?<=Your code has been rated at )(\d+(?:\.\d+)?)/10",line):
RES_all['GlobalScore']=float(res.group(1))
print(RES_all['GlobalScore'])
else:
raise RuntimeError("Invalid ScanState")
Outfile.write(JsonContent)
with open(cls.get_result_dir()/"metrics.json", 'w') as json_file:
json.dump(RES_all, json_file)
# exporting all Data in one csv, unused atm because jenkins seems not able to select columns from csv an keep displaying all...
# => to export a working full csv we need to a 'flat' dict (no more nested dict)
RES_all_trim = copy.deepcopy(RES_all)
del RES_all_trim["Messages"]
flat_RES_all=pandas.json_normalize(RES_all_trim, sep='_').to_dict(orient='records')[0]
with open(cls.get_result_dir()/"metrics.csv", 'w', newline='') as csv_file:
writer = csv.DictWriter(csv_file,fieldnames=flat_RES_all.keys())
writer.writeheader()
writer.writerow(flat_RES_all)
# splited csv exports for jenkins plots: RawMetricsPercent
RES_all_percent = RES_all["RawMetricsPercent"]
with open(cls.get_result_dir()/"metrics_rawpercent.csv", 'w', newline='') as csv_file:
writer = csv.DictWriter(csv_file,fieldnames=RES_all_percent.keys())
writer.writeheader()
writer.writerow(RES_all_percent)
# splited csv exports for jenkins plots: Statistics + Duplication + NbAnalysedStatments + NbAnalysedLines
RES_all_stats = copy.deepcopy(RES_all["Statistics"])
RES_all_stats["NbDupLines"] = RES_all['Duplication']['NbDupLines']
RES_all_stats["PersentDuplicatedLines"] = RES_all['Duplication']['PersentDuplicatedLines']
RES_all_stats["NbAnalysedStatments"] = RES_all['NbAnalysedStatments' ]
RES_all_stats["NbAnalysedLines"] = RES_all['NbAnalysedLines' ]
with open(cls.get_result_dir()/"metrics_Statistics.csv", 'w', newline='') as csv_file:
writer = csv.DictWriter(csv_file,fieldnames=RES_all_stats.keys())
writer.writeheader()
writer.writerow(RES_all_stats)
# splited csv exports for jenkins plots: Statistics + Duplication
RES_all_MessagesCat = RES_all['MessagesCat']
with open(cls.get_result_dir()/"metrics_MessagesCat.csv", 'w', newline='') as csv_file:
writer = csv.DictWriter(csv_file,fieldnames=RES_all_MessagesCat.keys())
writer.writeheader()
writer.writerow(RES_all_MessagesCat)
# splited csv exports for jenkins plots: GlobalScore
RES_GlobalScore = {'GlobalScore': RES_all['GlobalScore']}
with open(cls.get_result_dir()/"metrics_GlobalScore.csv", 'w', newline='') as csv_file:
writer = csv.DictWriter(csv_file,fieldnames=RES_GlobalScore.keys())
writer.writeheader()
writer.writerow(RES_GlobalScore)
# converting the report using pylint_json2html (/!\ internal API, but as their is no leading '_' ...)
with open(cls.get_result_dir()/"report.html","w+", encoding='utf-8') as Outfile:
raw_data = json.loads(JsonContent)
report=pylint_json2html.Report( raw_data)
Outfile.write(report.render())
print("Done")

View File

@@ -1,51 +0,0 @@
# pyChaChaDummyProject (c) by chacha
#
# pyChaChaDummyProject 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/>.
from __future__ import annotations
from typing import TYPE_CHECKING
from pathlib import Path
from mypy import api
from .helper_base import helper_withresults_base
class types_check(helper_withresults_base):
JUnitReportName = "junit.xml"
@classmethod
def do_job(cls):
print("checking code typing ...")
result = api.run([ # project path
"-m",
"src." + str(cls.pyproject['project']['name']),
# analysis configuration
"--ignore-missing-imports",
"--strict-equality",
# reports generation
"--cobertura-xml-report", str(cls.get_result_dir()),
"--html-report", str(cls.get_result_dir()),
"--linecount-report", str(cls.get_result_dir()),
"--linecoverage-report", str(cls.get_result_dir()),
"--lineprecision-report", str(cls.get_result_dir()),
"--txt-report", str(cls.get_result_dir()),
"--xml-report", str(cls.get_result_dir()),
"--junit-xml", str(cls.get_result_dir()) + "/" + cls.JUnitReportName
])
if result[0]:
print('\nType checking report:\n')
print(result[0]) # stdout
if result[1]:
print('\nError report:\n')
print(result[1]) # stderr
print('\nExit status:', result[2])
print("Done")

View File

@@ -1,79 +0,0 @@
# pyChaChaDummyProject (c) by chacha
#
# pyChaChaDummyProject 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/>.
from __future__ import annotations
from typing import TYPE_CHECKING
from pathlib import Path
import os
import datetime
import unittest
import xmlrunner
from junitparser import JUnitXml
from junit2htmlreport import parser as junit2html_parser
from .helper_base import helper_withresults_base
class unit_test(helper_withresults_base):
enable_coverage_check: bool = False
enable_xml_export: bool = True
enable_full_xml_export: bool = True
FullReportName: str = "full_report"
CoverageReportName: str = "test_coverage"
@classmethod
def do_job(cls):
if cls.enable_coverage_check==True:
import coverage
# preparing unittest framework
test_loader = unittest.TestLoader()
if cls.enable_coverage_check == True:
#we start coverage now because module files discovery is part of the coverage measurement
CoverageReportPath = Path(str(cls.get_result_dir())+"_coverage")
cls._reset_dir(CoverageReportPath)
cov = coverage.Coverage(cover_pylib=False,branch=True,source_pkgs=["src."+cls.pyproject['project']['name']])
cov.start()
package_tests = test_loader.discover(start_dir=str(cls.project_rootdir_path / "test"),top_level_dir=str(cls.project_rootdir_path / "test"))
if cls.enable_xml_export:
testRunner = xmlrunner.XMLTestRunner(output=str(str(cls.get_result_dir())))
else:
testRunner = unittest.TextTestRunner()
# running the test
testRunner.run(package_tests)
print("Test Finished")
if cls.enable_coverage_check == True:
cov.stop()
cov.save()
cov.html_report(directory=str(CoverageReportPath))
cov.xml_report(outfile=(CoverageReportPath/f"{cls.CoverageReportName}.xml"))
# computing results (Only if xml available)
if cls.enable_full_xml_export == True:
print("Full reports generation...")
FullReportPath = Path(str(cls.get_result_dir())+"_full")
cls._reset_dir(FullReportPath)
FullJUnitReport = JUnitXml()
for fname in [fname for fname in os.listdir(cls.get_result_dir()) if fname.endswith('.xml')]:
FullJUnitReport+=JUnitXml.fromfile(str(cls.get_result_dir() / fname))
current_datetime = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
full_report_base_name = f'{cls.pyproject["project"]["name"]}-{cls.FullReportName}-{current_datetime}'
FullJUnitReport.write(str(FullReportPath / f'{full_report_base_name}.xml'))
report = junit2html_parser.Junit(FullReportPath/ f'{full_report_base_name}.xml')
html = report.html()
with open(FullReportPath/ f'{full_report_base_name}.html', "wb") as outfile:
outfile.write(html.encode('utf-8'))
print("Done")

View File

@@ -0,0 +1,5 @@
from chacha_cicd_helper.__main__ import fct_main
import sys
if __name__ == "__main__":
fct_main(sys.argv[1:])

View File

@@ -1,19 +1,12 @@
# pyChaChaDummyProject (c) by chacha
#
# pyChaChaDummyProject 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/>.
docs_dir: docs
site_name: pychangelogfactory
site_url: https://chacha.ddns.net/mkdocs-web/chacha/pychangelogfactory/latest/
site_description: A simple changelog builder that you can feed with your repository change history
site_description: A simple changelog builder that you can feed with your repository
change history
site_author: chacha
repo_url: https://chacha.ddns.net/gitea/chacha/pychangelogfactory
use_directory_urls: false
copyright: CC BY-NC-SA 4.0
theme:
name: material
features:
@@ -21,8 +14,13 @@ theme:
- navigation.tracking
- navigation.tabs
- navigation.tabs.sticky
- toc.integrate
- navigation.footer
- navigation.path
- navigation.top
- navigation.section
- content.code.annotate
- navigation.expand
- toc.follow
palette:
- media: '(prefers-color-scheme: dark)'
scheme: slate
@@ -40,23 +38,85 @@ theme:
name: Switch to dark mode
plugins:
- search
- markdownextradata
- mermaid2
- localsearch
- autorefs
- mkdocstrings:
default_handler: python
handlers:
python:
selection:
path:
- src
options:
filters:
- '!^_(?!_init__)'
inherited_members: true
rendering:
show_root_heading: false
show_root_toc_entry: false
show_root_full_path: false
- '!^_[^_]'
inherited_members: false
show_if_no_docstring: true
show_signature_annotations: true
show_source: false
heading_level: 2
group_by_category: true
show_category_heading: true
group_by_category: true
show_root_full_path: false
merge_init_into_class: true
separate_signature: true
heading_level: 2
docstring_section_style: spacy
show_root_toc_entry: false
- with-pdf:
cover_subtitle: User Manual
cover_logo: C:\Users\chacha\git\pychangelogfactory\docs-static\Library.jpg
verbose: false
exclude_pages:
- LICENSE
output_path: C:\Users\chacha\git\pychangelogfactory\helpers-results\cl_doc_gen\site\pdf\manual.pdf
markdown_extensions:
- def_list
- tables
- attr_list
- abbr
- pymdownx.blocks.admonition:
types:
- new
- settings
- note
- abstract
- info
- tip
- success
- question
- warning
- failure
- danger
- bug
- example
- quote
- pymdownx.blocks.definition
- pymdownx.blocks.details
- pymdownx.blocks.tab
- pymdownx.blocks.html
- pymdownx.betterem:
smart_enable: all
- pymdownx.caret
- pymdownx.critic
- pymdownx.details
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.keys
- pymdownx.mark
- pymdownx.progressbar
- pymdownx.smartsymbols
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.tilde
- footnotes
- pymdownx.superfences
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji ''
emoji_generator: !!python/name:materialx.emoji.to_svg ''
extra:
branch: master
repository: pychangelogfactory

View File

@@ -12,11 +12,10 @@ build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
version_scheme= "post-release"
# tag_regex="^(?:v)?(?P<version>\\d+\\.\\d+\\.\\d+)([\\.\\-\\+])?(?:.*)?"
[project]
name = "pychangelogfactory"
description = "A simple changelog builder that you can feed with your repository change history"
description = "A simple changelog builder that you can feed with your repository change history."
readme = "README.md"
requires-python = ">=3.9"
keywords = ["chacha","chacha","template","pychangelogfactory"]
@@ -47,7 +46,21 @@ include-package-data = true
where = ["src"]
[tool.setuptools.package-data]
"pychangelogfactory.data" = ["*.*"]
"pysimpleini" = ["py.typed"]
# [[tool.mypy.overrides]]
# module = ""
# ignore_missing_imports = true
[tool.coverage.run]
cover_pylib = false
branch = true
data_file="helpers-results/cl_unit_test_raw_coverage/.coverage"
# debug = ["config","multiproc","process"]
parallel = true
concurrency = [
'thread'
]
[project.urls]
Homepage = "https://chacha.ddns.net/gitea/chacha/pychangelogfactory"
@@ -55,11 +68,12 @@ Documentation = "https://chacha.ddns.net/mkdocs-web/chacha/pychangelogfactory/
Tracker = "https://chacha.ddns.net/gitea/chacha/pychangelogfactory/issues"
[project.optional-dependencies]
test = ["junitparser>=2.8","junit2html>=30.1","xmlrunner>=1.7","mypy>=0.99" ]
coverage-check = ["coverage>=7.0"]
quality-check = ["pylint>=2.15","pylint-json2html>=0.4","pandas>=1.5"]
type-check = ["mypy[reports]>=0.99" ]
doc-gen = ["mkdocs>=1.4.0", "mkdocs-material>=8.5", "mkdocs-localsearch>=0.9.0", "mkdocstrings[python]>=0.19", "mkdocs-with-pdf>=0.9.3","pyyaml>=6.0"]
test = ["chacha_cicd_helper"]
coverage-check = ["chacha_cicd_helper"]
complexity-check = ["chacha_cicd_helper"]
quality-check = ["chacha_cicd_helper"]
type-check = ["chacha_cicd_helper"]
doc-gen = ["chacha_cicd_helper"]
#[project.scripts]
#my-script = "my_package.module:function"

View File

@@ -14,9 +14,10 @@ from importlib.metadata import version, PackageNotFoundError
try: # pragma: no cover
__version__ = version("pychangelogfactory")
except PackageNotFoundError: # pragma: no cover
except PackageNotFoundError: # pragma: no cover
import warnings
warnings.warn("can not read __version__, assuming local test context, setting it to ?.?.?")
__version__ = "?.?.?"
from pychangelogfactory.changelogfactory import ChangeLogFormater
from .changelogfactory import ChangelogFactory, ChangelogFormaterRecordType, ChangelogFormater

View File

@@ -9,136 +9,466 @@
# 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.
"""A simple changelog formater that consume merged message and produce nice pre-formated changelogs.
"""
from __future__ import annotations
import re
from re import Match, search, compile as _compile, match
from abc import ABC
from copy import deepcopy
def ChangeLogFormaterRecordType(Klass:type) -> type:
ChangeLogFormater.ar_Klass.append(Klass)
from typing import TYPE_CHECKING
from typing import Generic, TypeVar, cast
if TYPE_CHECKING:
from typing import Optional, ClassVar, Type, Dict, List
T_ChangelogFormater = TypeVar("T_ChangelogFormater", bound="ChangelogFormater")
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.RegisterBaseFormater(Klass)
return Klass
class ChangeLogFormater(ABC):
ar_Klass:list[ChangeLogFormater] = []
ar_LinesResult:list[ChangeLogFormater] = []
prefix:str = ''
title:str = 'Others :'
keywords:list[str] = []
priority:int = 0
def __init__(self,scope:str | None, ChangelogString:str):
self._scope = scope
self._ChangelogString = ChangelogString.strip()
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"""
cast(ChangelogFactory[ChangelogFormater], ChangelogFactory).ar_SavedFormaterKlass.add(Klass)
return ChangelogFormaterRecordType(Klass)
def RenderLine(self):
return self._ChangelogString.strip()
@classmethod
def RenderLines(cls) -> str:
changelog_category: str = ""
lines = cls.GetLines()
if len(lines)>0:
changelog_category = f"#### {cls.title}\n"
for line in lines:
changelog_category = changelog_category + f"> {line.RenderLine()}"
if (scope:=line.GetScope()) != "":
changelog_category = changelog_category + f"\t*[{scope}]*"
changelog_category = changelog_category + "\n"
class ChangelogFormater(ABC):
"""ChangelogFormater class
This class is the formater base class.
This class is for:
- classifying message: CheckLine() and CheckLine_keywords()
- storing lines: Clear() and PushLine()
- returning the formated output: Render() and RenderLines()
/// warning
this class does not aim to be instantiated by user.
///
"""
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) -> None:
"""ChangelogFormater class constructor"""
self._lines: List[None | str] = []
def Clear(self) -> None:
"""Clear the formater content"""
self._lines = []
def PushLine(self, ChangelogString: str) -> None:
"""Push a new line in the formater
Args:
ChangelogString: the new line to insert
"""
self._lines.append(ChangelogString.strip())
def Render(self) -> str:
"""Render all lines + title
Returns:
the rendered lines
"""
changelog_category = ""
if len(self._lines) > 0:
changelog_category = f"#### {self.title}\n"
changelog_category = changelog_category + self.RenderLines()
return changelog_category
def GetScope(self) -> str:
return self._scope if self._scope != None else ""
def RenderLines(self) -> str:
"""Render only lines
Returns:
the rendered lines
"""
full_lines = ""
for line in self._lines:
full_lines = full_lines + f"> {line}" + "\n"
return full_lines
@classmethod
def Clear(cls) -> None:
ChangeLogFormater.ar_LinesResult=[]
def CheckLine(cls, content: str) -> None | Match[str]:
"""Check if a line match the current formater (lazy identification)
/// warning
Only formal tags are parsed by this function
eg: `<change_type>(<change_target>): <change_message>`
///
Args:
content: line to parse
Returns:
match object
"""
regex = _compile(rf"^(?:-\s+)?(?:{cls.prefix})(?:\((.*)\))?(?::)(?:\s*)([^\s].+)")
_match = regex.match(content)
return _match
@classmethod
def _CheckLine(cls,content:str) -> bool:
regex=re.compile(f"^(?:-\s+)?(?:{cls.prefix})(?:\((.*)\))?(?::)(?:\s*)([^\s].+)")
return regex.match(content)
@classmethod
def _CheckLine_keywords(cls,content:str) -> bool:
keyword_list = (cls.keywords + list((cls.prefix,)))
for _keyword in keyword_list:
if ( _keyword != "") and re.search(_keyword,content):
return True
def CheckLine_keywords(cls, content: str) -> bool:
"""Check if a line match the current formater (deeper in-content identification)
Any word in the message can be used to categorize this message.
Args:
content: line to parse
Returns:
True if a keyword has matched, False otherwise
"""
keyword_list = cls.keywords
if keyword_list:
for _keyword in keyword_list:
if _keyword and search(_keyword, content):
return True
return False
@classmethod
def FactoryProcessLine(cls,RawChangelogLine:str) -> ChangeLogFormater:
for Klass in sorted(ChangeLogFormater.ar_Klass,key=lambda x: x.priority):
if content:=Klass._CheckLine(RawChangelogLine):
return Klass(content.group(1),content.group(2))
for Klass in sorted(ChangeLogFormater.ar_Klass,key=lambda x: x.priority,reverse=True):
if content:=Klass._CheckLine_keywords(RawChangelogLine):
return Klass(None,RawChangelogLine)
return ChangeLogFormater_others(None,RawChangelogLine)
class ChangelogFactory(Generic[T_ChangelogFormater]):
"""The main changelog class"""
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: Optional[str] = None) -> None:
"""Main ChangelogFormater class constructor
Args:
ChangelogString: optional input string to be processed
"""
self.ar_Formater: Dict[str, T_ChangelogFormater] = {}
self.ar_FormaterKlass = deepcopy(type(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) -> ChangelogFactory[T_ChangelogFormater]:
"""Reset the formater class list to original (Instance wise)
Returns:
self for convenience
"""
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 FactoryProcessFullChangelog(cls,RawChangelogMessage:str) -> list[ChangeLogFormater]:
LinesResult=[]
for line in RawChangelogMessage.split('\n'):
if(line.strip()!=""):
LinesResult.append(cls.FactoryProcessLine(line))
ChangeLogFormater.ar_LinesResult=LinesResult
return LinesResult
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 GetLinesOfType(cls,Klass:type) -> list[ChangeLogFormater]:
return [_ for _ in ChangeLogFormater.ar_LinesResult if type(_)==Klass]
def ResetBaseFormaterList(cls) -> None:
"""Reset the formater class list to original (BaseClass wise)"""
cls.ar_FormaterKlass = deepcopy(cls.ar_SavedFormaterKlass)
@classmethod
def GetLines(cls) -> list[ChangeLogFormater]:
return ChangeLogFormater.GetLinesOfType(cls)
def RegisterFormater(self, FormaterKlass: Type[T_ChangelogFormater]) -> ChangelogFactory[T_ChangelogFormater]:
"""Register a new formater in the current instance
@classmethod
def RenderFullChangelog(cls) -> str:
Args:
FormaterKlass: class of the formater to be added
Returns:
self for convenience
"""
self.ar_FormaterKlass.add(FormaterKlass)
self.ar_Formater[FormaterKlass.__name__] = FormaterKlass()
return self
def unRegisterFormater(self, FormaterKlass: Type[T_ChangelogFormater]) -> ChangelogFactory[T_ChangelogFormater]:
"""unRegister a new formater in the current instance
Args:
FormaterKlass: class of the formater to be dropped
Returns:
self for convenience
"""
self.ar_FormaterKlass.remove(FormaterKlass)
del self.ar_Formater[FormaterKlass.__name__]
return self
def Clear(self) -> ChangelogFactory[T_ChangelogFormater]:
"""Clear internal memory
Returns:
self for convenience
"""
for formater in self.ar_Formater.values():
formater.Clear()
return self
def _ProcessLineMain(self, RawChangelogLine: str) -> bool:
"""Process a line and look for identified ones
This function will try to apply every available formater for the 1st search round: formal search
If a matching formater is found, line is inserted.
Args:
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: 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
def _ProcessLineSecond(self, RawChangelogLine: str) -> bool:
"""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
If a matching formater is found, line is inserted.
Args:
RawChangelogLine: line to process
Returns:
True if successfully matched, False otherwise
"""
for formater in sorted(self.ar_Formater.values(), key=lambda x: x.priority, reverse=True):
if formater.CheckLine_keywords(RawChangelogLine):
formater.PushLine(RawChangelogLine)
return True
self.ar_Formater[ChangelogFormater_others.__name__].PushLine(RawChangelogLine)
return False
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 #`
A relevant commit line must contain:
- at least 2 words for formal
- at least 3 words for keywords
Args:
RawChangelogMessage: The full raw changelog to be processed
Returns:
self for convenience
"""
Lines2ndRound = []
for line in RawChangelogMessage.split("\n"):
lineWordsCount = len(line.split())
if (lineWordsCount > 1) and (not match(self.checkCommentPattern, line)):
if self._ProcessLineMain(line) is True:
continue
if lineWordsCount > 2:
Lines2ndRound.append(line)
for line in Lines2ndRound:
self._ProcessLineSecond(line)
return self
def RenderFullChangelog(self, 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 ChangeLogFormater.ar_Klass:
full_changelog = full_changelog + Klass.RenderLines()
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()
return full_changelog
# creating category classes: '<NAME>': (priority, ['<prefix1>',...], '<header>')
for RecordType, Config in { 'breaking': (20, ["break",], ':rotating_light: Breaking changes :rotating_light::'),
'feat': (20, ["feat","new","create"], 'Features :sparkles::'),
'fix': (10, ["issue","problem"], 'Fixes :wrench::'),
'security': (20, ["safe","leak"], 'Security :shield::'),
'chore': (20, ["task","refactor","build","better"], 'Chore :building_construction::'),
'perf': (0, ["fast",], 'Performance Enhancements :rocket::'),
'wip': (0, ["temp",], 'Work in progress changes :construction::'),
'docs': (0, ["doc",], 'Documentations :book::'),
'style': (30, ["beautify",], 'Style :art::'),
'refactor': (0, [], 'Refactorings :recycle::'),
'ci': (0, [""], 'Continuous Integration :cyclone::'),
'test': (15, ["unittest","check"], 'Testings :vertical_traffic_light::'),
'build': (0, ["compile","version"], 'Builds :package:')
}.items():
name=f"ChangeLogFormater_{RecordType}"
tmp = globals()[name] = type(name, (ChangeLogFormater,),{"prefix": RecordType,"title": Config[2],"keywords":Config[1],"priority":Config[0] })
ChangeLogFormater.ar_Klass.append(tmp)
@ChangeLogFormaterRecordType
class ChangeLogFormater_revert(ChangeLogFormater):
prefix:str = 'revert'
title:str = 'Reverts :back::'
keywords:list[str] = ["fallback"]
priority:int = 0
# to avoid writing class, they are initialized with the following structure:
# 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 {
# fmt: off
"break": ( 20, ["break"],
":rotating_light: Breaking changes :rotating_light: :",
),
"feat": ( 25, ["feat", "new", "create", "add"],
"Features :sparkles: :"
),
"fix": ( 0, ["fix","issue", "problem"],
"Fixes :wrench: :"
),
"security": ( 20, ["safe", "leak"],
"Security :shield: :"
),
"chore": ( 10, ["task", "refactor", "build", "better", "improve"],
"Chore :building_construction: :",
),
"perf": ( 15, ["fast","perf" ],
"Performance Enhancements :rocket: :",
),
"wip": ( 0, ["temp", ],
"Work in progress changes :construction: :",
),
"doc": ( 0, [ "doc", "manual"],
"Documentations :book: :",
),
"style": ( 5, ["beautify", ],
"Style :art: :",
),
"refactor": ( 0, [],
"Refactorings :recycle: :"
),
"ci": ( 0, ["jenkins", "git"],
"Continuous Integration :cyclone: :"
),
"test": ( -5, ["unittest", "check", "testing"],
"Testings :vertical_traffic_light: :"
),
"build": ( 0, ["compile", "version"],
"Builds :package: :"
),
# fmt: on
}.items():
# then we instantiate all of them
_name = f"ChangelogFormater_{RecordType}"
def RenderLine(self) -> str:
return "~~"+super().RenderLine()+"~~"
# can not change globals definition so mypy will keep complaining
_tmp = type(
_name,
(ChangelogFormater,),
{
"prefix": RecordType,
"title": Config[2],
"keywords": Config[1],
"priority": Config[0],
},
)
globals()[_name] = _tmp
cast(ChangelogFactory[ChangelogFormater], ChangelogFactory).RegisterBaseFormater(_tmp)
cast(ChangelogFactory[ChangelogFormater], ChangelogFactory).ar_SavedFormaterKlass.add(_tmp)
@ChangeLogFormaterRecordType
class ChangeLogFormater_others(ChangeLogFormater):
prefix:str = 'other'
title:str = 'Others :question::'
keywords:list[str] = [""]
priority:int = -20
@_ChangelogFormaterRecordType
class ChangelogFormater_revert(ChangelogFormater):
"""Revert scope formater"""
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
Returns:
the rendered lines
"""
full_lines = ""
for line in self._lines:
full_lines = full_lines + f"> ~~{line}~~" + "\n"
return full_lines
@_ChangelogFormaterRecordType
class ChangelogFormater_others(ChangelogFormater):
"""Others / unknown scope formater"""
prefix: ClassVar[Optional[str]] = "other"
title: ClassVar[Optional[str]] = "Others :question: :"
keywords: ClassVar[Optional[List[str]]] = [""]
priority: ClassVar[int] = -20

View File

@@ -0,0 +1 @@
# PlaceHolder

View File

@@ -7,62 +7,319 @@
# 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 pychangelogfactory
test_commitlog_1="""
breaking: test1
feat: test2
chore: test3
security: test4
style:test5
fix: test6
wip: test7
perf: test8
refactor: test9
ci: test10
docs: test11
test: test12
build: test13
- fix: test14
revert: test15
toto:un autre cas
fdsfdsfdsfsdfsdff
"""
from src.pychangelogfactory import ChangelogFormater, ChangelogFactory, ChangelogFormaterRecordType
class Testtest_module(unittest.TestCase):
def test_simplegeneration_full(self):
pychangelogfactory.ChangeLogFormater.FactoryProcessFullChangelog(test_commitlog_1)
changelog = pychangelogfactory.ChangeLogFormater.RenderFullChangelog()
print(changelog)
self.assertIn("test1",changelog)
self.assertIn("test2",changelog)
self.assertIn("test3",changelog)
self.assertIn("test4",changelog)
self.assertIn("test5",changelog)
self.assertIn("test6",changelog)
self.assertIn("test7",changelog)
self.assertIn("test8",changelog)
self.assertIn("test9",changelog)
self.assertIn("test10",changelog)
self.assertIn("test11",changelog)
self.assertIn("test12",changelog)
self.assertIn("test13",changelog)
self.assertIn("test14",changelog)
self.assertIn("test15",changelog)
self.assertIn("un autre cas",changelog)
self.assertIn("fdsfdsfdsfsdfsdff",changelog)
def setUp(self):
ChangelogFactory.ResetBaseFormaterList()
def simplegeneration(self, inputstr, teststrs: list[str], withunknown: bool = False):
hdlr = ChangelogFactory()
hdlr.ProcessFullChangelog(inputstr)
changelog = hdlr.RenderFullChangelog(include_unknown=withunknown)
for test in teststrs:
self.assertIn(test, changelog)
def test_simplegeneration_ignored2(self):
raw = "break: testbreak break" + "\n" + "#doc: testdoc doc" + "\n" + "#style: teststyle beautify" + "\n" + "//test: testtest check"
hdlr = ChangelogFactory(raw)
changelog = hdlr.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" + "#doc: testdoc" + "\n" + "#style: teststyle" + "\n" + "//test: testtest"
hdlr = ChangelogFactory(raw)
changelog = hdlr.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" + "doc: testdoc" + "\n" + "style: teststyle" + "\n" + "test: testtest"
hdlr = ChangelogFactory(raw)
changelog = hdlr.RenderFullChangelog().splitlines()
self.assertIn("testbreak", changelog[1])
self.assertIn("teststyle", changelog[3])
self.assertIn("testdoc", changelog[5])
self.assertIn("testtest", changelog[7])
def test_simplegeneration_toosmall(self):
self.simplegeneration("one", [], True)
self.simplegeneration("one two", [], True)
self.simplegeneration("one two three", ["one two three"], True)
def test_simplegeneration_unknown(self):
self.simplegeneration("one two three", ["one two three"], True)
self.simplegeneration("one two three", [""], False)
def test_simplegeneration_multiple(self):
raw = "break: testbreak" + "\n" + "doc: testdoc" + "\n" + "style: teststyle"
self.simplegeneration(raw, ["testbreak", "testdoc", "teststyle"])
def test_simplegeneration_breaking(self):
self.simplegeneration("break: teststring", ["teststring"])
self.simplegeneration("test break dummy1 dummy2", ["test break"])
def test_simplegeneration_features(self):
self.simplegeneration("feat: teststring", ["teststring"])
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 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 dummy1 dummy2", ["test safe"])
self.simplegeneration("test leak dummy1 dummy2", ["test leak"])
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 dummy1 dummy2", ["test fast"])
def test_simplegeneration_wip(self):
self.simplegeneration("wip: teststring", ["teststring"])
self.simplegeneration("test temp dummy1 dummy2", ["test temp"])
def test_simplegeneration_docs(self):
self.simplegeneration("doc: teststring", ["teststring"])
self.simplegeneration("test doc dummy1 dummy2", ["test doc"])
def test_simplegeneration_style(self):
self.simplegeneration("style: teststring", ["teststring"])
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 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 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 dummy1 dummy2", ["test compile"])
self.simplegeneration("test version dummy1 dummy2", ["test version"])
def test_simplegeneration_revert(self):
self.simplegeneration("revert: teststring", ["~~teststring~~"])
# fmt: off
raw_changelog = (
"feat: add a nice feature to the project\n"
"style: reindent the full Foo class\n"
"security: fix a security issue on the Foo2 component\n"
"security: fix another security problem on the Foo2 component\n"
"improve core performances by reducing complexity\n"
"some random changes in the text content\n"
)
expected_formated = (
"#### Features :sparkles: :\n"
"> add a nice feature to the project\n"
"#### Security :shield: :\n"
"> fix a security issue on the Foo2 component\n"
"> fix another security problem on the Foo2 component\n"
"#### Performance Enhancements :rocket: :\n"
"> improve core performances by reducing complexity\n"
"#### Style :art: :\n"
"> reindent the full Foo class\n"
"#### Others :question: :\n"
"> some random changes in the text content\n"
)
# fmt: on
def test_sample(self):
hdlr = ChangelogFactory(self.raw_changelog)
changelog = hdlr.RenderFullChangelog(include_unknown=True)
self.assertEqual(changelog, self.expected_formated)
def test_sample_aio(self):
changelog = ChangelogFactory(self.raw_changelog).RenderFullChangelog(include_unknown=True)
self.assertEqual(changelog, self.expected_formated)
def test_sample_exploded(self):
hdlr = ChangelogFactory()
hdlr.ProcessFullChangelog(self.raw_changelog)
changelog = hdlr.RenderFullChangelog(include_unknown=True)
self.assertEqual(changelog, self.expected_formated)
def test_sample_clear(self):
hdlr = ChangelogFactory()
hdlr.ProcessFullChangelog(self.raw_changelog)
changelog = hdlr.RenderFullChangelog(include_unknown=True)
self.assertEqual(changelog, self.expected_formated)
hdlr.Clear()
changelog = hdlr.RenderFullChangelog(include_unknown=True)
self.assertEqual(changelog, "")
class Testtest_module_othercontext(unittest.TestCase):
def setUp(self):
ChangelogFactory.ResetBaseFormaterList()
def test_custom(self):
"""
1st PART: register a global custom formater
"""
@ChangelogFormaterRecordType
class ChangelogFormater_TEST(ChangelogFormater):
"""My formater"""
prefix: str = "mytag"
title: str = "My Title :"
keywords: list[str] = ["foo", "42"]
priority: int = 10
# fmt: off
raw_changelog = ("mytag: add a nice feature to the project\n"
"foo modification in my file\n"
"need 42 coffee\n"
)
expected_formated_orig = (
"#### My Title :\n"
"> add a nice feature to the project\n"
"> foo modification in my file\n"
"> need 42 coffee\n"
)
# fmt: on
hdlr = ChangelogFactory(raw_changelog)
changelog = hdlr.RenderFullChangelog(include_unknown=True)
self.assertEqual(changelog, expected_formated_orig)
"""
2nd PART: cheking the custom formater is still here after new object creation
"""
hdlr = ChangelogFactory(raw_changelog)
changelog = hdlr.RenderFullChangelog(include_unknown=True)
self.assertEqual(changelog, expected_formated_orig)
"""
3rd PART: removing the custom formater at runtime
"""
hdlr = ChangelogFactory()
hdlr.unRegisterFormater(ChangelogFormater_TEST)
hdlr.ProcessFullChangelog(raw_changelog)
changelog = hdlr.RenderFullChangelog(include_unknown=True)
# fmt: off
expected_formated = (
"#### Features :sparkles: :\n"
"> mytag: add a nice feature to the project\n"
"#### Others :question: :\n"
"> foo modification in my file\n"
"> need 42 coffee\n"
)
# fmt: on
self.assertEqual(changelog, expected_formated)
"""
4th PART: checking it is back when create new obj
"""
hdlr = ChangelogFactory()
hdlr.ProcessFullChangelog(raw_changelog)
changelog = hdlr.RenderFullChangelog(include_unknown=True)
self.assertEqual(changelog, expected_formated_orig)
"""
3.1rd PART: removing the custom formater at runtime
"""
hdlr = ChangelogFactory()
hdlr.ResetFormaterList()
hdlr.ProcessFullChangelog(raw_changelog)
changelog = hdlr.RenderFullChangelog(include_unknown=True)
self.assertEqual(changelog, expected_formated)
"""
4.1th PART: checking it is back when create new obj
"""
hdlr = ChangelogFactory()
hdlr.ProcessFullChangelog(raw_changelog)
changelog = hdlr.RenderFullChangelog(include_unknown=True)
self.assertEqual(changelog, expected_formated_orig)
"""
5th PART: reseting class list globally
"""
ChangelogFactory.ResetBaseFormaterList()
hdlr = ChangelogFactory()
hdlr.ProcessFullChangelog(raw_changelog)
changelog = hdlr.RenderFullChangelog(include_unknown=True)
self.assertEqual(changelog, expected_formated)
"""
6th PART: checking it is still not here
"""
hdlr = ChangelogFactory()
hdlr.ProcessFullChangelog(raw_changelog)
changelog = hdlr.RenderFullChangelog(include_unknown=True)
self.assertEqual(changelog, expected_formated)
class Testtest_module_othercontext2(unittest.TestCase):
def setUp(self):
ChangelogFactory.ResetBaseFormaterList()
def test_custom2(self):
class ChangelogFormater_TEST2(ChangelogFormater):
"""My formater"""
prefix: str = "mytag"
title: str = "My Title 2:"
keywords: list[str] = ["foo", "42"]
priority: int = 10
# fmt: off
raw_changelog = ("mytag: add a nice feature to the project\n"
"foo modification in my file\n"
"need 42 coffee\n"
)
expected_formated = (
"#### My Title 2:\n"
"> add a nice feature to the project\n"
"> foo modification in my file\n"
"> need 42 coffee\n"
)
# fmt: on
hdlr = ChangelogFactory()
hdlr.RegisterFormater(ChangelogFormater_TEST2)
hdlr.ProcessFullChangelog(raw_changelog)
changelog = hdlr.RenderFullChangelog(include_unknown=True)
self.assertEqual(changelog, expected_formated)