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
This commit was merged in pull request #2.
This commit is contained in:
2023-03-25 00:00:05 +01:00
17 changed files with 914 additions and 557 deletions

View File

@@ -1,13 +1,17 @@
<?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>

116
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
@@ -21,10 +27,17 @@ def _bDraft = false
// release content / changelog management
def _bAutoChangelog = true //Not supported yet
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)
@@ -131,11 +184,11 @@ pipeline {
sh(". ~/BUILD_ENV/bin/activate && pip install --upgrade setuptools build pip copier jinja2-slug toml")
sh(". ~/TOOLS_ENV/bin/activate && pip install simple_rest_client requests")
sh(". ~/TOOLS_ENV/bin/activate && pip install simple_rest_client requests twine")
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
{
@@ -145,6 +198,7 @@ pipeline {
}
sh("git config --global user.email $_MaintainerEmail")
sh("git config --global user.name $_MaintainerName")
sh("git config --global init.defaultBranch master")
}
}
@@ -321,7 +375,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]")
}
}
}
@@ -331,6 +385,14 @@ pipeline {
steps {
dir("gitrepo") {
sh(". ~/TEST_ENV/bin/activate && python -m helpers --type-check --quality-check")
script {
def jsonObj = readJSON file: "helpers-results/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 helpers --complexity-check")
}
}
post {
@@ -385,6 +447,14 @@ pipeline {
style: 'stackedArea',
keepRecords: true,
numBuilds: ''])
plot([ csvFileName: 'plot-4ceb9ee2-ca78-11ed-afa1-0242ac120002.csv',
csvSeries: [[ file: 'gitrepo/helpers-results/complexity_check/MI.csv', inclusionFlag: 'INCLUDE_BY_STRING',exclusionValues: 'MeanMaintainability', url: '']],
group: 'metrics',
title: 'maintainability',
style: 'stackedArea',
keepRecords: true,
numBuilds: ''])
}
}
@@ -397,6 +467,36 @@ pipeline {
println unit_test_full_name__html
unit_test_full_name__xml=findFiles(glob: "helpers-results/unit_test_full/*.xml")[0].getName()
println unit_test_full_name__xml
coverage_report_path = "helpers-results/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))
//complexity = new BigDecimal( 10*GetCoverageValue_complexity(coverage_report_path))
//sz_complexity = complexity.setScale(2, RoundingMode.HALF_EVEN).toString()
//badge_complexity.setStatus(sz_complexity)
//badge_quality.setColor(getColorScale_reversed(complexity))
//badge_maintainability
records = readCSV file: 'helpers-results/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")}
}
}
}
@@ -554,6 +654,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,17 @@
![](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
# pyChangeLogHelper
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 merged commit message and produce nice pre-formated changelogs
It is also collectings data to feed Jenkins build.
Checkout [Latest Documentation](https://chacha.ddns.net/mkdocs-web/chacha/pychachadummyproject/{{branch}}/latest/).
Checkout [Latest Documentation](https://chacha.ddns.net/mkdocs-web/chacha/pychangelogfactory/master/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

View File

@@ -1,16 +1,24 @@
# 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.
## Installation
## 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.
From pypi repository (prefered):
## 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.
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
## Import in your project
Add this line on the top of your python script:
from pychangelogfactory import ChangeLogFormater

View File

@@ -16,89 +16,101 @@ import os
import logging
import sys
if __package__=="helpers":
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:
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
from .complexity_check import complexity_check
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
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
from helpers.complexity_check import complexity_check
logging.getLogger().setLevel(logging.INFO)
if __name__ == "__main__":
project_rootdir_path=Path(__file__).parent.parent.absolute()
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')
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")
parser.add_argument("-cpc", "--complexity-check", dest="complexitycheck", action="store_true", help="enable complexity check")
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
# args.typecheck = True
# args.qualitycheck = True
# args.unittest = True
# args.coveragecheck = True
# args.docgen = True
# args.docgenpdf = True
# args.changeloggen = True
# args.complexitycheck = True
helpers = []
if args.typecheck == True:
if args.typecheck == True:
helpers.append(types_check)
if args.unittest == True:
if args.unittest == True:
helpers.append(unit_test)
if args.coveragecheck == True:
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:
if args.qualitycheck == True:
helpers.append(quality_check)
if args.docgen == True:
if args.docgen == True:
helpers.append(doc_gen)
if args.docgenpdf==True:
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:
if args.changeloggen == True:
helpers.append(changelog_gen)
if args.complexitycheck == True:
helpers.append(complexity_check)
for helper in helpers:
helper.set_context(project_rootdir_path,pyproject)
helper.set_context(project_rootdir_path, pyproject)
helper.reset_result_dir()
helper.do_job()

View File

@@ -9,16 +9,15 @@
from __future__ import annotations
from typing import TYPE_CHECKING
#from pathlib import Path
#import os
#import datetime
# 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
class changelog_gen(helper_base):
@classmethod
def do_job(cls):
pass

View File

@@ -0,0 +1,70 @@
# 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 statistics
import csv
from json import loads as JSON_LOADS
from radon.complexity import cc_rank, SCORE
from radon.cli import Config
from radon.cli.harvest import CCHarvester, HCHarvester, MIHarvester
from .helper_base import helper_withresults_base
from pprint import pprint
class complexity_check(helper_withresults_base):
@classmethod
def do_job(cls):
config = Config(
exclude="__init__\.py",
ignore=None,
order=SCORE,
show_closures=False,
no_assert=True,
min="A",
max="F",
multi=False,
)
h = MIHarvester([str(_) for _ in sorted((cls.project_rootdir_path / "src").rglob("*.py"))], config).as_json()
res = JSON_LOADS(h)
with open(cls.get_result_dir() / "MI.json", "w", newline="") as oFile:
oFile.write(h)
mean = statistics.mean(_["mi"] for _ in res.values())
if mean >= 65:
rank = "A+"
elif mean >= 20:
rank = "A"
elif mean >= 10:
rank = "B"
else:
rank = "C"
RES_MI = {"MeanMaintainability": mean, "MaintainabilityIndex": rank}
with open(cls.get_result_dir() / "MI.csv", "w", newline="") as oFile:
writer = csv.DictWriter(oFile, fieldnames=RES_MI.keys())
writer.writeheader()
writer.writerow(RES_MI)
config = Config(exclude=None, ignore=None, order=SCORE, show_closures=False, no_assert=True, min="A", max="F", multi=False)
h = CCHarvester([str(_) for _ in sorted((cls.project_rootdir_path / "src").rglob("*.py"))], config).as_json()
with open(cls.get_result_dir() / "CC.json", "w", newline="") as oFile:
oFile.write(h)
config = Config(exclude=None, ignore=None, order=SCORE, show_closures=False, no_assert=True, min="A", max="F", by_function=None)
h = HCHarvester([str(_) for _ in sorted((cls.project_rootdir_path / "src").rglob("*.py"))], config).as_json()
with open(cls.get_result_dir() / "HC.json", "w", newline="") as oFile:
oFile.write(h)

View File

@@ -17,6 +17,7 @@ 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:
@@ -24,35 +25,34 @@ except ImportError:
from .helper_base import helper_withresults_base
class doc_gen(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()
@classmethod
def do_job(cls):
# create doc root dir
doc_path = cls.project_rootdir_path/"docs"
doc_path = cls.project_rootdir_path / "docs"
cls._reset_dir(doc_path)
site_path = cls.get_result_dir()/"site"
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"))
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))
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")
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)
@@ -61,43 +61,44 @@ class doc_gen(helper_withresults_base):
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)
identifier = "src." + ".".join(parts)
print("::: " + identifier, file=fd)
cmdopts = [f"{sys.executable}","-m","mkdocs","-v","build","--site-dir",str(site_path),"--clean"]
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
# => 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 = 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')
}})
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)
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')
print(" !! done")

View File

@@ -9,64 +9,66 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from abc import ABC,abstractmethod
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
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
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):
@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
[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
@classmethod
@abstractmethod
def do_job(cls):
def do_job(cls):
raise NotImplementedError()
@classmethod
@classmethod
def run_cmd_(cls, cmdarray):
process = subprocess.run(cmdarray, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, check=True)
return process.stdout
@classmethod
@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
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
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

@@ -25,197 +25,222 @@ import pylint_json2html
from .helper_base import helper_withresults_base
class PyLintMetricNotFound(Warning):
pass
class quality_check(helper_withresults_base):
PylintMessageList=dict()
@classmethod
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)
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):
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):
@classmethod
def do_job(cls):
print("checking code quality ...")
cls.GetPylintMessageList()
RES_all=dict()
RES_all = dict()
with StringIO() as StdOutput:
JsonContent=""
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:
pylint_Run(
[
"--load-plugins=pylint.extensions.mccabe",
"--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
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:
for line in StdOutput.getvalue().split("\n"):
print(line)
if ScanState == TScanState.TEXT_REPORT:
# ignoring this part, we need json
if line=='[':
JsonContent+=line
if line == "[":
JsonContent += line
ScanState = TScanState.JSON_REPORT
elif line=='[]':
JsonContent+=line
elif line == "[]":
JsonContent += line
ScanState = TScanState.OTHER_REPORT_START
elif ScanState == TScanState.JSON_REPORT:
JsonContent+=line
if line==']':
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))
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 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:
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' ]
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:
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:
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:
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'])
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:
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...
# 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())
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())
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())
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())
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())
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:
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)
report = pylint_json2html.Report(raw_data)
Outfile.write(report.render())
print("Done")

View File

@@ -16,36 +16,46 @@ from mypy import api
from .helper_base import helper_withresults_base
class types_check(helper_withresults_base):
class types_check(helper_withresults_base):
JUnitReportName = "junit.xml"
@classmethod
def do_job(cls):
@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
])
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("\nType checking report:\n")
print(result[0]) # stdout
if result[1]:
print('\nError report:\n')
print("\nError report:\n")
print(result[1]) # stderr
print('\nExit status:', result[2])
print("Done")
print("\nExit status:", result[2])
print("Done")

View File

@@ -21,34 +21,36 @@ 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
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:
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")
# 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 = 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"))
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)
@@ -57,23 +59,23 @@ class unit_test(helper_withresults_base):
cov.stop()
cov.save()
cov.html_report(directory=str(CoverageReportPath))
cov.xml_report(outfile=(CoverageReportPath/f"{cls.CoverageReportName}.xml"))
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")
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))
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')
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")
with open(FullReportPath / f"{full_report_base_name}.html", "wb") as outfile:
outfile.write(html.encode("utf-8"))
print("Done")

View File

@@ -6,7 +6,6 @@
# 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/
@@ -14,49 +13,80 @@ site_description: A simple changelog builder that you can feed with your reposit
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:
- navigation.instant
- navigation.tracking
- navigation.tabs
- navigation.tabs.sticky
- toc.integrate
- navigation.top
- navigation.instant
- navigation.tracking
- navigation.tabs
- navigation.tabs.sticky
- toc.integrate
- navigation.top
palette:
- media: '(prefers-color-scheme: dark)'
scheme: slate
toggle:
icon: material/brightness-4
name: Switch to system preference
- media: (prefers-color-scheme)
toggle:
icon: material/brightness-auto
name: Switch to light mode
- media: '(prefers-color-scheme: light)'
scheme: default
toggle:
icon: material/brightness-7
name: Switch to dark mode
- media: '(prefers-color-scheme: dark)'
scheme: slate
toggle:
icon: material/brightness-4
name: Switch to system preference
- media: (prefers-color-scheme)
toggle:
icon: material/brightness-auto
name: Switch to light mode
- media: '(prefers-color-scheme: light)'
scheme: default
toggle:
icon: material/brightness-7
name: Switch to dark mode
plugins:
- search
- localsearch
- autorefs
- mkdocstrings:
default_handler: python
handlers:
python:
selection:
filters:
- '!^_(?!_init__)'
inherited_members: true
rendering:
show_root_heading: false
show_root_toc_entry: false
show_root_full_path: false
show_if_no_docstring: true
show_signature_annotations: true
show_source: false
heading_level: 2
group_by_category: true
show_category_heading: true
- search
- markdownextradata
- mermaid2
- localsearch
- autorefs
- mkdocstrings:
default_handler: python
handlers:
python:
selection:
filters:
- '!^_(?!_init__)'
inherited_members: true
rendering:
show_root_heading: false
show_root_toc_entry: false
show_root_full_path: false
show_if_no_docstring: true
show_signature_annotations: true
show_source: false
heading_level: 2
group_by_category: true
show_category_heading: true
markdown_extensions:
- def_list
- tables
- attr_list
- abbr
- 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
extra:
branch: master
repository: pygitversionhelper

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"]
@@ -57,9 +56,10 @@ Tracker = "https://chacha.ddns.net/gitea/chacha/pychangelogfactory/issue
[project.optional-dependencies]
test = ["junitparser>=2.8","junit2html>=30.1","xmlrunner>=1.7","mypy>=0.99" ]
coverage-check = ["coverage>=7.0"]
complexity-check = ["radon>=5.1"]
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"]
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","pymdown-extensions>=9","mkdocs-markdownextradata-plugin","mkdocs-mermaid2-plugin"]
#[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 ChangeLogFormater

View File

@@ -9,7 +9,7 @@
# You should have received a copy of the license along with this
# work. If not, see <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
"""Phasellus tellus lectus, volutpat eu dapibus ut, suscipit vel augue.
""" A simple changelog formater that consume merged commit message and produce nice pre-formated changelogs
"""
@@ -18,127 +18,212 @@ from __future__ import annotations
import re
from abc import ABC
def ChangeLogFormaterRecordType(Klass:type) -> type:
def ChangeLogFormaterRecordType(Klass: type) -> type:
"""decorator helper function to register interface implementation"""
ChangeLogFormater.ar_Klass.append(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):
class ChangeLogFormater(ABC):
"""the main changelog class that define nearly everythings.
This was supposed to be a very shorty script this is why it is all-in-one...
Factory and base-object are mixed.
"""
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 RenderLine(self):
"""return a rendered line"""
return self._ChangelogString.strip()
@classmethod
@classmethod
def RenderLines(cls) -> str:
"""render all lines"""
changelog_category: str = ""
lines = cls.GetLines()
if len(lines)>0:
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()) != "":
if (scope := line.GetScope()) != "":
changelog_category = changelog_category + f"\t*[{scope}]*"
changelog_category = changelog_category + "\n"
return changelog_category
def GetScope(self) -> str:
return self._scope if self._scope != None else ""
"""return the current scope (category)"""
return self._scope if self._scope is not None else ""
@classmethod
def Clear(cls) -> None:
ChangeLogFormater.ar_LinesResult=[]
"""clear internal memory"""
ChangeLogFormater.ar_LinesResult = []
@classmethod
def _CheckLine(cls,content:str) -> bool:
regex=re.compile(f"^(?:-\s+)?(?:{cls.prefix})(?:\((.*)\))?(?::)(?:\s*)([^\s].+)")
return regex.match(content)
def CheckLine(cls, content: str) -> bool:
"""check if a line is in the current scope (lazy identification)"""
regex = re.compile(r"^(?:-\s+)?(?:{0})(?:\((.*)\))?(?::)(?:\s*)([^\s].+)".format(cls.prefix))
_match = regex.match(content)
return _match
@classmethod
def _CheckLine_keywords(cls,content:str) -> bool:
keyword_list = (cls.keywords + list((cls.prefix,)))
def CheckLine_keywords(cls, content: str) -> bool:
"""check if a line is in the current scope (deeper in-word identification)"""
keyword_list = cls.keywords
for _keyword in keyword_list:
if ( _keyword != "") and re.search(_keyword,content):
if (_keyword != "") and re.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)
@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 FactoryProcessLineMain(cls, RawChangelogLine: str) -> ChangeLogFormater:
"""Process a line and look for identified ones"""
for Klass in sorted(ChangeLogFormater.ar_Klass, key=lambda x: x.priority):
content = Klass.CheckLine(RawChangelogLine)
if content is not None:
return Klass(content.group(1), content.group(2))
return ChangeLogFormater_others(None, RawChangelogLine)
@classmethod
def GetLinesOfType(cls,Klass:type) -> list[ChangeLogFormater]:
return [_ for _ in ChangeLogFormater.ar_LinesResult if type(_)==Klass]
def FactoryProcessLineSecond(cls, RawChangelogLine: str) -> ChangeLogFormater:
"""Process a line and look for non-identified ones"""
for Klass in sorted(ChangeLogFormater.ar_Klass, key=lambda x: x.priority, reverse=True):
if Klass.CheckLine_keywords(RawChangelogLine):
return Klass(None, RawChangelogLine)
return ChangeLogFormater_others(None, RawChangelogLine)
@classmethod
def FactoryProcessFullChangelog(cls, RawChangelogMessage: str) -> list[ChangeLogFormater]:
"""Process all input lines"""
LinesResult = []
Lines2ndRound = []
for line in RawChangelogMessage.split("\n"):
if line.strip() != "":
res = cls.FactoryProcessLineMain(line)
if res is not ChangeLogFormater_others:
LinesResult.append(res)
else:
Lines2ndRound.append(line)
for line in Lines2ndRound:
LinesResult.append(cls.FactoryProcessLineSecond(line))
ChangeLogFormater.ar_LinesResult = LinesResult
return ChangeLogFormater.ar_LinesResult
@classmethod
def GetLinesOfType(cls, Klass: type) -> list[ChangeLogFormater]:
"""retrieve all lines of specified type"""
return [_ for _ in ChangeLogFormater.ar_LinesResult if isinstance(_, Klass)]
@classmethod
def GetLines(cls) -> list[ChangeLogFormater]:
"""retrieve all lines for the current formater"""
return ChangeLogFormater.GetLinesOfType(cls)
@classmethod
def RenderFullChangelog(cls) -> str:
"""render the main changelog"""
full_changelog = ""
for Klass in ChangeLogFormater.ar_Klass:
for Klass in sorted(ChangeLogFormater.ar_Klass, key=lambda x: x.priority, reverse=True):
full_changelog = full_changelog + Klass.RenderLines()
return full_changelog
# to avoid writing class, they are initialized with the following structure:
# creating category classes: '<NAME>': (priority, ['<prefix1>',...], '<header>')
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] })
for RecordType, Config in {
"break": (
20,
[],
":rotating_light: Breaking changes :rotating_light::",
),
"feat": (20, ["feat", "new", "create", "add"], "Features :sparkles::"),
"fix": (10, ["issue", "problem"], "Fixes :wrench::"),
"security": (20, ["safe", "leak"], "Security :shield::"),
"chore": (
20,
["task", "refactor", "build", "better", "improve"],
"Chore :building_construction::",
),
"perf": (
0,
[
"fast",
],
"Performance Enhancements :rocket::",
),
"wip": (
0,
[
"temp",
],
"Work in progress changes :construction::",
),
"docs": (
0,
[
"doc",
],
"Documentations :book::",
),
"style": (
5,
[
"beautify",
],
"Style :art::",
),
"refactor": (0, [], "Refactorings :recycle::"),
"ci": (0, ["jenkins", "git"], "Continuous Integration :cyclone::"),
"test": (15, ["unittest", "check", r"^(?:\s)*test(?:\s)*$"], "Testings :vertical_traffic_light::"),
"build": (0, ["compile", "version"], "Builds :package:"),
}.items():
# then we instantiate all of them
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
"""revert scope formater"""
prefix: str = "revert"
title: str = "Reverts :back::"
keywords: list[str] = ["fallback"]
priority: int = 0
def RenderLine(self) -> str:
return "~~"+super().RenderLine()+"~~"
return "~~" + super().RenderLine() + "~~"
@ChangeLogFormaterRecordType
class ChangeLogFormater_others(ChangeLogFormater):
prefix:str = 'other'
title:str = 'Others :question::'
keywords:list[str] = [""]
priority:int = -20
"""others / unknown scope formater"""
prefix: str = "other"
title: str = "Others :question::"
keywords: list[str] = [""]
priority: int = -20

View File

@@ -7,62 +7,93 @@
# 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
"""
class Testtest_module(unittest.TestCase):
def test_simplegeneration_full(self):
pychangelogfactory.ChangeLogFormater.FactoryProcessFullChangelog(test_commitlog_1)
def simplegeneration(self, inputstr, teststrs: list[str]):
pychangelogfactory.ChangeLogFormater.FactoryProcessFullChangelog(inputstr)
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)
for test in teststrs:
self.assertIn(test, changelog)
def test_simplegeneration_order(self):
raw = "break: testbreak" + "\n" + "docs: testdoc" + "\n" + "style: teststyle" + "\n" + "test: testtest"
pychangelogfactory.ChangeLogFormater.FactoryProcessFullChangelog(raw)
changelog = pychangelogfactory.ChangeLogFormater.RenderFullChangelog().splitlines()
self.assertIn("testbreak", changelog[1])
self.assertIn("testtest", changelog[3])
self.assertIn("teststyle", changelog[5])
self.assertIn("testdoc", changelog[7])
def test_simplegeneration_multiple(self):
raw = "break: testbreak" + "\n" + "docs: testdoc" + "\n" + "style: teststyle"
self.simplegeneration(raw, ["testbreak", "testdoc", "teststyle"])
def test_simplegeneration_breaking(self):
self.simplegeneration("break: teststring", ["teststring"])
self.simplegeneration("test break", ["test break"])
def test_simplegeneration_features(self):
self.simplegeneration("feat: teststring", ["teststring"])
self.simplegeneration("test feat", ["test feat"])
self.simplegeneration("test new", ["test new"])
self.simplegeneration("test create", ["test create"])
self.simplegeneration("test add", ["test add"])
def test_simplegeneration_fix(self):
self.simplegeneration("fix: teststring", ["teststring"])
self.simplegeneration("test fix", ["test fix"])
self.simplegeneration("test issue", ["test issue"])
self.simplegeneration("test problem", ["test problem"])
def test_simplegeneration_security(self):
self.simplegeneration("security: teststring", ["teststring"])
self.simplegeneration("test safe", ["test safe"])
self.simplegeneration("test leak", ["test leak"])
def test_simplegeneration_task(self):
self.simplegeneration("task: teststring", ["teststring"])
self.simplegeneration("test refactor", ["test refactor"])
self.simplegeneration("test build", ["test build"])
self.simplegeneration("test better", ["test better"])
self.simplegeneration("test improve", ["test improve"])
def test_simplegeneration_perf(self):
self.simplegeneration("perf: teststring", ["teststring"])
self.simplegeneration("test fast", ["test fast"])
def test_simplegeneration_wip(self):
self.simplegeneration("wip: teststring", ["teststring"])
self.simplegeneration("test temp", ["test temp"])
def test_simplegeneration_docs(self):
self.simplegeneration("docs: teststring", ["teststring"])
self.simplegeneration("test doc", ["test doc"])
def test_simplegeneration_style(self):
self.simplegeneration("style: teststring", ["teststring"])
self.simplegeneration("test beautify", ["test beautify"])
def test_simplegeneration_refactor(self):
self.simplegeneration("refactor: teststring", ["teststring"])
def test_simplegeneration_ci(self):
self.simplegeneration("ci: teststring", ["teststring"])
self.simplegeneration("test jenkins", ["test jenkins"])
self.simplegeneration("test git", ["test git"])
def test_simplegeneration_test(self):
self.simplegeneration("test: teststring", ["teststring"])
self.simplegeneration("test unittest", ["test unittest"])
self.simplegeneration("test check", ["test check"])
def test_simplegeneration_build(self):
self.simplegeneration("build: teststring", ["teststring"])
self.simplegeneration("test compile", ["test compile"])
self.simplegeneration("test version", ["test version"])
def test_simplegeneration_revert(self):
self.simplegeneration("revert: teststring", ["~~teststring~~"])