diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..eb1ca111 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,21 @@ +--- +name: Test and quality + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4.2.2 + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + version: "0.4.22" + - name: Install dependencies, run unit tests and check quality + run: | + uv venv + uv pip install -r requirements.txt + ci/unittest.sh + ci/quality.sh diff --git a/README.md b/README.md index 10bd7554..a923dcca 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ The Kwaliteitsaanpak consists of a main document containing the Kwaliteitsaanpak - Clone this repository - Run `docker compose up` - Run `open html/index.html` to view the latest release and the work in progress (wip) +- Run `uvx md-dead-link-check Content` to check for dead links ## Releasing a new version of the documentation diff --git a/ci/quality.sh b/ci/quality.sh index 0c41c906..44b12770 100755 --- a/ci/quality.sh +++ b/ci/quality.sh @@ -2,8 +2,9 @@ set -e -black --quiet --line-length 118 src tests -mypy --no-error-summary src tests -pylint src tests +uvx ruff check +uvx ruff format --check +uvx mypy --python-executable=.venv/bin/python src tests NAMES_TO_IGNORE='' -vulture --min-confidence 0 --ignore-names $NAMES_TO_IGNORE src tests .vulture_white_list.py +uvx vulture --min-confidence 0 --ignore-names $NAMES_TO_IGNORE src tests .vulture_white_list.py +uvx md-dead-link-check . diff --git a/ci/unittest.sh b/ci/unittest.sh index fa8a331c..81d5db62 100755 --- a/ci/unittest.sh +++ b/ci/unittest.sh @@ -1,7 +1,7 @@ #!/bin/sh export PYTHONPATH=src:$PYTHONPATH -coverage run --omit=*venv/* --branch -m unittest --quiet -coverage xml -o build/unittest-coverage.xml -coverage html --directory build/unittest-coverage -coverage report --fail-under=100 --skip-covered +.venv/bin/coverage run --omit=*venv/* --branch -m unittest --quiet +.venv/bin/coverage xml -o build/unittest-coverage.xml +.venv/bin/coverage html --directory build/unittest-coverage +.venv/bin/coverage report --fail-under=100 --skip-covered diff --git a/pyproject.toml b/pyproject.toml index 6b2260d9..9cfb39bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,29 +3,18 @@ name = "ictu-kwaliteitsaanpak" version = "4.0.0" requires-python = ">=3.12" classifiers = [ - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] dependencies = [ - "python-docx==1.1.2", - "python-pptx==1.0.2", - "xlsxwriter==3.2.0", + "coverage==7.6.4", + "lxml-stubs==0.5.1", + "python-docx==1.1.2", + "python-pptx==1.0.2", + "vulture==2.13", + "xlsxwriter==3.2", ] -optional-dependencies.dev = [ - "black==24.10.0", - "coverage==7.6.4", - "lxml-stubs==0.5.1", - "mypy==1.13.0", - "pip-tools==7.4.1", - "pylint==3.3.1", - "vulture==2.13", -] - -[tool.black] -line-length = 120 - -[tool.pylint] -max-line-length=120 [tool.mypy] ignore_missing_imports = false @@ -37,8 +26,43 @@ warn_unused_ignores = true [[tool.mypy.overrides]] module = [ - "pptx", - "pptx.util", - "xlsxwriter", + "pptx", + "pptx.util", + "xlsxwriter", ] ignore_missing_imports = true + +[tool.md_dead_link_check] +exclude_links = [ + "#m01", + "#m02", + "#m04", + "#m05", + "#m07", + "#m08", + "#m10", + "#m14", + "#m16", + "#m18", + "#m19", + "#m26", + "#m27", + "#m28", + "#m29", + "#m30", + "#m31", + "#m32", + "#m33", + "#m34", + "#relatie-met-nen-npr-5326", + "#terminologie-en-afkortingen", + "relaties-tussen-producten.png", + "relaties-testproducten-agile.png", +] + +[tool.ruff] +target-version = "py312" +line-length = 120 +src = [ + "src", +] diff --git a/requirements.txt b/requirements.txt index 58f7d654..f749e00e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,69 @@ # This file was autogenerated by uv via the following command: # uv pip compile pyproject.toml --generate-hashes +coverage==7.6.4 \ + --hash=sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376 \ + --hash=sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9 \ + --hash=sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111 \ + --hash=sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172 \ + --hash=sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491 \ + --hash=sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546 \ + --hash=sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2 \ + --hash=sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11 \ + --hash=sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08 \ + --hash=sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c \ + --hash=sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2 \ + --hash=sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963 \ + --hash=sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613 \ + --hash=sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0 \ + --hash=sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db \ + --hash=sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf \ + --hash=sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73 \ + --hash=sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117 \ + --hash=sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1 \ + --hash=sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e \ + --hash=sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522 \ + --hash=sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25 \ + --hash=sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc \ + --hash=sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea \ + --hash=sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52 \ + --hash=sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a \ + --hash=sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07 \ + --hash=sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06 \ + --hash=sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa \ + --hash=sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901 \ + --hash=sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b \ + --hash=sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17 \ + --hash=sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0 \ + --hash=sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21 \ + --hash=sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19 \ + --hash=sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5 \ + --hash=sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51 \ + --hash=sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3 \ + --hash=sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3 \ + --hash=sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f \ + --hash=sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076 \ + --hash=sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a \ + --hash=sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718 \ + --hash=sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba \ + --hash=sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e \ + --hash=sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27 \ + --hash=sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e \ + --hash=sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09 \ + --hash=sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e \ + --hash=sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70 \ + --hash=sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f \ + --hash=sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72 \ + --hash=sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a \ + --hash=sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef \ + --hash=sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b \ + --hash=sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b \ + --hash=sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f \ + --hash=sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806 \ + --hash=sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b \ + --hash=sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1 \ + --hash=sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c \ + --hash=sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858 + # via ictu-kwaliteitsaanpak (pyproject.toml) lxml==5.3.0 \ --hash=sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e \ --hash=sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229 \ @@ -142,6 +206,10 @@ lxml==5.3.0 \ # via # python-docx # python-pptx +lxml-stubs==0.5.1 \ + --hash=sha256:1f689e5dbc4b9247cb09ae820c7d34daeb1fdbd1db06123814b856dae7787272 \ + --hash=sha256:e0ec2aa1ce92d91278b719091ce4515c12adc1d564359dfaf81efa7d4feab79d + # via ictu-kwaliteitsaanpak (pyproject.toml) pillow==11.0.0 \ --hash=sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7 \ --hash=sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5 \ @@ -233,6 +301,10 @@ typing-extensions==4.12.2 \ # via # python-docx # python-pptx +vulture==2.13 \ + --hash=sha256:34793ba60488e7cccbecdef3a7fe151656372ef94fdac9fe004c52a4000a6d44 \ + --hash=sha256:78248bf58f5eaffcc2ade306141ead73f437339950f80045dce7f8b078e5a1aa + # via ictu-kwaliteitsaanpak (pyproject.toml) xlsxwriter==3.2.0 \ --hash=sha256:9977d0c661a72866a61f9f7a809e25ebbb0fb7036baa3b9fe74afcfca6b3cb8c \ --hash=sha256:ecfd5405b3e0e228219bcaf24c2ca0915e012ca9464a14048021d21a995d490e diff --git a/src/builder/__init__.py b/src/builder/__init__.py index 671c7c16..e8f06745 100644 --- a/src/builder/__init__.py +++ b/src/builder/__init__.py @@ -1,7 +1 @@ """Builder package.""" - -from .builder import Builder -from .docx_builder import DocxBuilder -from .html_builder import HTMLBuilder -from .pptx_builder import PptxBuilder -from .xlsx_builder import XlsxBuilder diff --git a/src/builder/builder.py b/src/builder/builder.py index 33f45477..253cfa72 100644 --- a/src/builder/builder.py +++ b/src/builder/builder.py @@ -8,8 +8,6 @@ class Builder: """Abstract builder.""" - # pylint: disable=unused-argument - def __init__(self, filename: pathlib.Path) -> None: self.filename = filename self._stack: list[tuple[str, TreeBuilderAttributes]] = [] diff --git a/src/builder/docx_builder.py b/src/builder/docx_builder.py index 879efe76..17f549ff 100644 --- a/src/builder/docx_builder.py +++ b/src/builder/docx_builder.py @@ -18,7 +18,7 @@ from .table_of_contents import add_table_of_contents -class DocxBuilder(Builder): # pylint: disable=too-many-instance-attributes +class DocxBuilder(Builder): """Docx builder.""" SCHEMA = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}" @@ -37,7 +37,10 @@ class DocxBuilder(Builder): # pylint: disable=too-many-instance-attributes ) def __init__( - self, filename: pathlib.Path, docx_reference_filename: pathlib.Path, images_folder: pathlib.Path + self, + filename: pathlib.Path, + docx_reference_filename: pathlib.Path, + images_folder: pathlib.Path, ) -> None: super().__init__(filename) filename.unlink(missing_ok=True) @@ -51,8 +54,7 @@ def __init__( self.row: Row | None = None self.column_index = 0 - def start_element(self, tag: str, attributes: TreeBuilderAttributes) -> None: # pylint:disable=too-many-branches - # pylint: disable=protected-access + def start_element(self, tag: str, attributes: TreeBuilderAttributes) -> None: super().start_element(tag, attributes) if tag == xmltags.PARAGRAPH: self.paragraph = self.doc.add_paragraph(style="Maatregel" if self.in_element(xmltags.MEASURE) else None) @@ -87,18 +89,20 @@ def start_element(self, tag: str, attributes: TreeBuilderAttributes) -> None: # self._add_table_cell(attributes) elif tag == xmltags.HEADER: self.paragraph = cast(Paragraph, self.doc.sections[0].header.paragraphs[0]) - self.paragraph.paragraph_format.alignment = WD_PARAGRAPH_ALIGNMENT.RIGHT # pylint: disable=no-member + self.paragraph.paragraph_format.alignment = WD_PARAGRAPH_ALIGNMENT.RIGHT elif tag == xmltags.TITLE: self.paragraph = self.doc.add_paragraph(style="Title") elif tag == xmltags.TABLE_OF_CONTENTS: self.doc.add_paragraph(attributes[xmltags.TABLE_OF_CONTENTS_HEADING], style="TOC Heading") add_table_of_contents(self.doc.add_paragraph()) elif tag == xmltags.IMAGE: - self.doc.add_picture(str(self.images_folder / str(attributes["src"])), width=Cm(int(attributes["width"]))) + self.doc.add_picture( + str(self.images_folder / str(attributes["src"])), + width=Cm(int(attributes["width"])), + ) def _add_list_item(self) -> None: """Add a list item.""" - # pylint: disable=protected-access self.paragraph = self.doc.add_paragraph(style=self.current_list_style[-1]) level = len(self.current_list_style) - 1 self.paragraph_number(self.paragraph).get_or_add_ilvl().val = level @@ -119,18 +123,15 @@ def _add_list_item(self) -> None: @staticmethod def paragraph_number(paragraph: Paragraph): """Return the current paragraph number .""" - # pylint: disable=protected-access return paragraph._p.get_or_add_pPr().get_or_add_numPr() # type: ignore[attr-defined] def _add_table_cell(self, attributes: TreeBuilderAttributes) -> None: """Add a table cell.""" - # pylint: disable=protected-access assert self.row cell = self.row.cells[self.column_index] cell._tc.tcPr.tcW.type = "auto" # type: ignore[union-attr] self.paragraph = cast(Paragraph, cell.paragraphs[0]) if alignment_attr := attributes.get(xmltags.TABLE_CELL_ALIGNMENT): - # pylint: disable=no-member alignment = { "left": WD_PARAGRAPH_ALIGNMENT.LEFT, "right": WD_PARAGRAPH_ALIGNMENT.RIGHT, @@ -146,7 +147,7 @@ def text(self, tag: str, text: str, attributes: TreeBuilderAttributes) -> None: if self.in_element(xmltags.BOLD): run.font.bold = True if self.in_element(xmltags.INSTRUCTION): - run.font.highlight_color = WD_COLOR_INDEX.YELLOW # pylint: disable=no-member + run.font.highlight_color = WD_COLOR_INDEX.YELLOW if self.in_element(xmltags.ITALIC): run.font.italic = True if self.in_element(xmltags.STRIKETHROUGH): diff --git a/src/builder/html_builder.py b/src/builder/html_builder.py index bba7c1f5..4421c8e7 100644 --- a/src/builder/html_builder.py +++ b/src/builder/html_builder.py @@ -36,7 +36,7 @@ def __init__(self, filename: pathlib.Path, stylesheet_path: pathlib.Path) -> Non self.in_keep_together_div = False self.in_measure = False - def start_element(self, tag: str, attributes: TreeBuilderAttributes) -> None: # pylint:disable=too-many-branches + def start_element(self, tag: str, attributes: TreeBuilderAttributes) -> None: match tag: case xmltags.DOCUMENT: self.builder.start(html_tags.HTML, {html_tags.LANGUAGE: "nl"}) @@ -48,7 +48,10 @@ def start_element(self, tag: str, attributes: TreeBuilderAttributes) -> None: # self.builder.end(html_tags.TITLE) self.builder.start( html_tags.LINK, - {html_tags.LINK_REL: "stylesheet", html_tags.LINK_HREF: self.STYLESHEET}, + { + html_tags.LINK_REL: "stylesheet", + html_tags.LINK_HREF: self.STYLESHEET, + }, ) self.builder.end(html_tags.LINK) self.builder.end(html_tags.HEAD) @@ -74,7 +77,10 @@ def start_element(self, tag: str, attributes: TreeBuilderAttributes) -> None: # self.builder.start(html_tags.TABLE_ROW, {}) case xmltags.TABLE_CELL: alignment = str(attributes[xmltags.TABLE_CELL_ALIGNMENT]) - self.builder.start(self.table_cell_html_tag, {html_tags.STYLE: f"text-align:{alignment}"}) + self.builder.start( + self.table_cell_html_tag, + {html_tags.STYLE: f"text-align:{alignment}"}, + ) case xmltags.ANCHOR: self.builder.start( html_tags.ANCHOR, diff --git a/src/builder/hyperlink.py b/src/builder/hyperlink.py index b77a30ad..c821eb4d 100644 --- a/src/builder/hyperlink.py +++ b/src/builder/hyperlink.py @@ -37,6 +37,6 @@ def add_hyperlink(paragraph, url, text, style="Hyperlink"): new_run.style = style hyperlink.append(new_run) - paragraph._p.append(hyperlink) # pylint: disable=protected-access + paragraph._p.append(hyperlink) return hyperlink diff --git a/src/builder/pptx_builder.py b/src/builder/pptx_builder.py index aa1b92a7..cd1cf46f 100644 --- a/src/builder/pptx_builder.py +++ b/src/builder/pptx_builder.py @@ -25,7 +25,7 @@ def __init__(self, filename: pathlib.Path, pptx_reference_filename: pathlib.Path super().__init__(filename) filename.unlink(missing_ok=True) shutil.copy(pptx_reference_filename, filename) - self.presentation = Presentation(filename) + self.presentation = Presentation(str(filename)) # Make the title placeholder on the measure slide wider measure_master_slide = self.presentation.slide_master.slide_layouts[self.CONTENT_SLIDE] measure_master_slide.placeholders[0].width = Inches(10) @@ -50,7 +50,9 @@ def text(self, tag: str, text: str, attributes: TreeBuilderAttributes) -> None: self.chapter_heading = "" if tag == xmltags.BOLD: self.add_slide(self.CONTENT_SLIDE, text) - self.current_slide.shapes.title.text_frame.paragraphs[0].font.size = Pt(24) # type: ignore + self.current_slide.shapes.title.text_frame.paragraphs[ # type: ignore + 0 + ].font.size = Pt(24) else: if len(self.current_slide.shapes) == 1: # type: ignore self.add_text_box() @@ -87,10 +89,10 @@ def add_text_box(self): def remove_bullet(self, paragraph_index: int): """Remove bullets from the paragraph.""" - # pylint: disable=c-extension-no-member no_bullet = etree.Element("{http://schemas.openxmlformats.org/drawingml/2006/main}buNone") - # pylint: disable=protected-access - self.current_slide.shapes[1].text_frame.paragraphs[paragraph_index]._pPr.insert(0, no_bullet) # type: ignore + self.current_slide.shapes[1].text_frame.paragraphs[paragraph_index]._pPr.insert( # type: ignore + 0, no_bullet + ) def in_appendix(self) -> bool: """Return whether the current section is an appendix.""" @@ -98,4 +100,4 @@ def in_appendix(self) -> bool: def end_document(self) -> None: """Override to save the presentation.""" - self.presentation.save(self.filename) + self.presentation.save(str(self.filename)) diff --git a/src/builder/table_of_contents.py b/src/builder/table_of_contents.py index 4464cdf2..8fee6899 100644 --- a/src/builder/table_of_contents.py +++ b/src/builder/table_of_contents.py @@ -23,7 +23,7 @@ def add_table_of_contents(paragraph: Paragraph) -> None: fld_char4 = OxmlElement("w:fldChar") fld_char4.set(qn("w:fldCharType"), "end") - r_element = run._r # pylint: disable=protected-access + r_element = run._r r_element.append(fld_char) r_element.append(instr_text) r_element.append(fld_char2) diff --git a/src/builder/xlsx_builder.py b/src/builder/xlsx_builder.py index 14d2aad5..433a23f6 100644 --- a/src/builder/xlsx_builder.py +++ b/src/builder/xlsx_builder.py @@ -12,7 +12,7 @@ from .builder import Builder -class XlsxBuilder(Builder): # pylint: disable=too-many-instance-attributes +class XlsxBuilder(Builder): """Self-assessment builder.""" MEASURE_ID_COLUMN, MEASURE_COLUMN, STATUS_COLUMN, EXPLANATION_COLUMN = range(4) @@ -31,12 +31,25 @@ def __init__(self, filename: pathlib.Path) -> None: self.measure_text: List[str] = [] @staticmethod - def __create_formats(workbook: xlsxwriter.Workbook) -> Dict[str, xlsxwriter.format.Format]: + def __create_formats( + workbook: xlsxwriter.Workbook, + ) -> Dict[str, xlsxwriter.format.Format]: """Create the formats.""" - measure_format_options = {"bg_color": "#BCD2EE", "text_wrap": True, "valign": "top"} + measure_format_options = { + "bg_color": "#BCD2EE", + "text_wrap": True, + "valign": "top", + } status_format_options = {"bg_color": "#FED32D", "text_wrap": True} return { - "header": workbook.add_format({"text_wrap": True, "font_size": 14, "bold": True, "bg_color": "#B3D6C9"}), + "header": workbook.add_format( + { + "text_wrap": True, + "font_size": 14, + "bold": True, + "bg_color": "#B3D6C9", + } + ), "instructions": workbook.add_format({"text_wrap": True, "font_size": 13, "bg_color": "#B3D6C9"}), "measure": workbook.add_format(measure_format_options), "submeasure": workbook.add_format({"align": "vjustify", "indent": 1, **measure_format_options}), @@ -65,9 +78,7 @@ def start_element(self, tag: str, attributes: TreeBuilderAttributes) -> None: elif self.measure_text: if tag == xmltags.LIST_ITEM: prefix = ( - f"{str(attributes[xmltags.LIST_ITEM_NUMBER])}. " - if xmltags.LIST_ITEM_NUMBER in attributes - else "- " + f"{str(attributes[xmltags.LIST_ITEM_NUMBER])}. " if xmltags.LIST_ITEM_NUMBER in attributes else "- " ) self.measure_text.append(prefix) elif tag == xmltags.TABLE_CELL: @@ -82,7 +93,11 @@ def text(self, tag: str, text: str, attributes: TreeBuilderAttributes) -> None: self.measure_row = self.row self.measure_id, measure_title = text.split(":") has_submeasures = self.in_element(xmltags.MEASURE, {"composite": "true"}) - self.__write_measure(self.measure_id, measure_title.strip(), has_submeasures=has_submeasures) + self.__write_measure( + self.measure_id, + measure_title.strip(), + has_submeasures=has_submeasures, + ) self.measure_text.append(text) elif self.measure_text: if ( @@ -91,11 +106,18 @@ def text(self, tag: str, text: str, attributes: TreeBuilderAttributes) -> None: and self.measure_id in ("M01", "M02", "M05", "M07", "M16", "M31", "M32", "M34") ): self.row += 1 - self.__write_measure("", f"{str(attributes[xmltags.LIST_ITEM_NUMBER])}. {text}", submeasure=True) + self.__write_measure( + "", + f"{str(attributes[xmltags.LIST_ITEM_NUMBER])}. {text}", + submeasure=True, + ) if tag == xmltags.TABLE_CELL and self.measure_id == "M01": # The unicode check symbol is wider than other characters, messing up the table layout: text = text.replace("✔", "x") - column, row = int(attributes[xmltags.TABLE_CELL_COLUMN]), int(attributes[xmltags.TABLE_CELL_ROW]) + column, row = ( + int(attributes[xmltags.TABLE_CELL_COLUMN]), + int(attributes[xmltags.TABLE_CELL_ROW]), + ) if row > 0 and column == 0: # Table cell with a document self.row += 1 # Write the document as (sub)measure @@ -104,13 +126,27 @@ def text(self, tag: str, text: str, attributes: TreeBuilderAttributes) -> None: self.measure_text.append(text) def __write_measure( - self, measure_id, measure_text, submeasure: bool = False, has_submeasures: bool = False + self, + measure_id, + measure_text, + submeasure: bool = False, + has_submeasures: bool = False, ) -> None: """Write a measure row.""" measure_format_key = "submeasure" if submeasure else "measure" status_format_key = "substatus" if submeasure else "status" - self.checklist.write(self.row, self.MEASURE_ID_COLUMN, measure_id, self.formats[measure_format_key]) - self.checklist.write(self.row, self.MEASURE_COLUMN, measure_text, self.formats[measure_format_key]) + self.checklist.write( + self.row, + self.MEASURE_ID_COLUMN, + measure_id, + self.formats[measure_format_key], + ) + self.checklist.write( + self.row, + self.MEASURE_COLUMN, + measure_text, + self.formats[measure_format_key], + ) self.__write_assessment_choices(self.row, self.STATUS_COLUMN) if has_submeasures: self.checklist.write_comment( @@ -189,7 +225,12 @@ def __create_checklist(self, version: str) -> None: self.checklist.set_row(3, 40) self.checklist.set_row(self.HEADER_ROW, 30) for column, (header, width) in enumerate( - [("Maatregel", 12), ("Omschrijving", 70), ("Status", 20), ("Toelichting", 70)] + [ + ("Maatregel", 12), + ("Omschrijving", 70), + ("Status", 20), + ("Toelichting", 70), + ] ): self.checklist.write(self.HEADER_ROW, column, header, self.formats["header"]) self.checklist.set_column(f"{'ABCD'[column]}:{'ABCD'[column]}", width) @@ -200,14 +241,24 @@ def __finish_checklist(self) -> None: def __write_assessment_choices(self, row: int, column: int) -> None: """Write the assessment choices, colors and data validation in the status column.""" - assessment_choices = ["voldoet", "voldoet deels", "voldoet niet", "niet van toepassing"] + assessment_choices = [ + "voldoet", + "voldoet deels", + "voldoet niet", + "niet van toepassing", + ] for choice in assessment_choices: self.checklist.conditional_format( row, column, row, column, - {"type": "cell", "criteria": "==", "value": f'"{choice}"', "format": self.formats[choice]}, + { + "type": "cell", + "criteria": "==", + "value": f'"{choice}"', + "format": self.formats[choice], + }, ) self.checklist.data_validation(row, column, row, column, {"validate": "list", "source": assessment_choices}) diff --git a/src/cli.py b/src/cli.py index daa6639a..5758f63a 100644 --- a/src/cli.py +++ b/src/cli.py @@ -7,7 +7,9 @@ def parse_cli_arguments() -> argparse.Namespace: """Parse the command-line arguments.""" parser = argparse.ArgumentParser() parser.add_argument( - "settings", nargs="+", help="One or more JSON settings files that specify the conversion options" + "settings", + nargs="+", + help="One or more JSON settings files that specify the conversion options", ) parser.add_argument("--version", help="Document version", default="wip") parser.add_argument( diff --git a/src/convert.py b/src/convert.py index d996e283..edd209d5 100644 --- a/src/convert.py +++ b/src/convert.py @@ -15,7 +15,10 @@ from cli import parse_cli_arguments from converter import Converter -from builder import DocxBuilder, HTMLBuilder, PptxBuilder, XlsxBuilder +from builder.docx_builder import DocxBuilder +from builder.html_builder import HTMLBuilder +from builder.pptx_builder import PptxBuilder +from builder.xlsx_builder import XlsxBuilder from markdown_converter import MarkdownConverter from markdown_syntax import VARIABLE_USE_PATTERN from custom_types import JSON, Settings, Variables @@ -23,7 +26,6 @@ def convert(settings_filename: str, version: str) -> None: """Convert the input document to the specified output formats.""" - # pylint: disable=unsubscriptable-object,unsupported-assignment-operation variables = read_variables(settings_filename, version) settings = read_settings(settings_filename, variables) logging.info("Converting with settings:\n%s", pprint.pformat(settings)) @@ -46,7 +48,7 @@ def read_variables(settings_filename: str, version: str) -> Variables: """Read the variables.""" settings = cast(Settings, read_json(settings_filename)) variables = cast(Variables, {}) - for variable_file in settings["VariablesFiles"]: # pylint: disable=unsubscriptable-object + for variable_file in settings["VariablesFiles"]: variables.update(cast(Variables, read_json(variable_file))) variables["VERSIE"] = version if version == "wip" else f"v{version}" variables["VERSIE_ZONDER_V"] = version @@ -57,7 +59,6 @@ def read_variables(settings_filename: str, version: str) -> Variables: def read_settings(settings_filename: str, variables: Variables) -> Settings: """Read the settings.""" settings = read_json(settings_filename, variables) - # pylint: disable=unsupported-assignment-operation settings["Version"] = variables["VERSIE"] settings["Date"] = variables["DATUM"] return cast(Settings, settings) @@ -117,7 +118,10 @@ def convert_pptx(converter, settings: Settings) -> None: """Convert the XML to pptx.""" build_path = get_build_path(settings) pptx_build_filename = build_path / settings["OutputFormats"]["pptx"]["OutputFile"] - pptx_builder = PptxBuilder(pptx_build_filename, pathlib.Path(settings["OutputFormats"]["pptx"]["ReferenceFile"])) + pptx_builder = PptxBuilder( + pptx_build_filename, + pathlib.Path(settings["OutputFormats"]["pptx"]["ReferenceFile"]), + ) converter.convert(pptx_builder) copy_output(pptx_build_filename, settings, "pptx") diff --git a/src/converter.py b/src/converter.py index 8224137b..14326c7f 100644 --- a/src/converter.py +++ b/src/converter.py @@ -3,7 +3,7 @@ from xml.etree.ElementTree import Element, ElementTree from typing import cast -from builder import Builder +from builder.builder import Builder from custom_types import TreeBuilderAttributes @@ -31,5 +31,5 @@ def convert_element(self, element: Element, builder: Builder, parent: Element | self.convert_element(child_element, builder, element) builder.end_element(element.tag, attributes) if element.tail: - assert parent + assert parent is not None builder.tail(element.tag, element.tail, parent.tag, attributes) diff --git a/src/markdown_converter.py b/src/markdown_converter.py index 9fd08b19..ec00d0fb 100644 --- a/src/markdown_converter.py +++ b/src/markdown_converter.py @@ -41,7 +41,8 @@ def _convert_markdown_file(self, markdown_filename: pathlib.Path, settings: Sett if line.startswith("#include"): filename = line.split(" ", maxsplit=1)[1].strip().strip('"') filename = filename.replace( - "{{DOCUMENT-FOLDER}}", settings.get("DocumentFolder", "DocumentFolder missing in settings") + "{{DOCUMENT-FOLDER}}", + settings.get("DocumentFolder", "DocumentFolder missing in settings"), ) self._convert_markdown_file(pathlib.Path(filename), settings) else: @@ -50,7 +51,10 @@ def _convert_markdown_file(self, markdown_filename: pathlib.Path, settings: Sett def _start_document(self, settings: Settings) -> None: """Start the document.""" document_attributes: TreeBuilderAttributes = {} - for setting, tag in (("Title", xmltags.DOCUMENT_TITLE), ("Version", xmltags.DOCUMENT_VERSION)): + for setting, tag in ( + ("Title", xmltags.DOCUMENT_TITLE), + ("Version", xmltags.DOCUMENT_VERSION), + ): document_attributes[tag] = str(settings[setting]) self.builder.start(xmltags.DOCUMENT, document_attributes) self._create_frontpage(settings) @@ -100,7 +104,10 @@ def _create_frontpage(self, settings: Settings) -> None: if settings["FrontPage"] == "ICTU": self._add_element( xmltags.IMAGE, - attributes={xmltags.IMAGE_SRC: "word-cloud.png", xmltags.IMAGE_WIDTH: "15"}, + attributes={ + xmltags.IMAGE_SRC: "word-cloud.png", + xmltags.IMAGE_WIDTH: "15", + }, ) self._add_element(xmltags.PAGEBREAK) @@ -121,12 +128,15 @@ def _create_footer(self) -> None: def _create_table_of_contents(self) -> None: """Create the table of contents placeholder. Actually creating a table of contents is the responsibility of the target format (e.g. docx).""" - self._add_element(xmltags.TABLE_OF_CONTENTS, attributes={xmltags.TABLE_OF_CONTENTS_HEADING: "Inhoudsopgave"}) + self._add_element( + xmltags.TABLE_OF_CONTENTS, + attributes={xmltags.TABLE_OF_CONTENTS_HEADING: "Inhoudsopgave"}, + ) self._add_element(xmltags.PAGEBREAK) def _process_line(self, line: str) -> None: """Process a line of Markdown.""" - if not (stripped_line := line.strip()): # pylint: disable=superfluous-parens + if not (stripped_line := line.strip()): self._end_lists() self._end_table() return # Empty line, nothing further to do @@ -155,7 +165,11 @@ def _process_line(self, line: str) -> None: def _process_variables(self, line: str) -> str: """Replace the variables with their values.""" - return re.sub(markdown_syntax.VARIABLE_USE_PATTERN, lambda variable: self.variables[variable.group(1)], line) + return re.sub( + markdown_syntax.VARIABLE_USE_PATTERN, + lambda variable: self.variables[variable.group(1)], + line, + ) def _process_heading(self, heading: str, level: int) -> None: """Process a heading.""" @@ -177,7 +191,10 @@ def _process_heading(self, heading: str, level: int) -> None: } self.builder.start(xmltags.SECTION, attributes) self.current_section_level = level - self.builder.start(xmltags.SECTION, {**is_appendix, xmltags.SECTION_LEVEL: str(self.current_section_level)}) + self.builder.start( + xmltags.SECTION, + {**is_appendix, xmltags.SECTION_LEVEL: str(self.current_section_level)}, + ) with self.element(xmltags.HEADING): self._process_formatted_text(heading) @@ -253,11 +270,27 @@ def _process_formatted_text(self, line: str) -> None: seen = "" formats = [ (markdown_syntax.BOLD_START, markdown_syntax.BOLD_END, xmltags.BOLD), - (markdown_syntax.BOLD_ALTERNATIVE_START, markdown_syntax.BOLD_ALTERNATIVE_END, xmltags.BOLD), - (markdown_syntax.INSTRUCTION_START, markdown_syntax.INSTRUCTION_END, xmltags.INSTRUCTION), + ( + markdown_syntax.BOLD_ALTERNATIVE_START, + markdown_syntax.BOLD_ALTERNATIVE_END, + xmltags.BOLD, + ), + ( + markdown_syntax.INSTRUCTION_START, + markdown_syntax.INSTRUCTION_END, + xmltags.INSTRUCTION, + ), (markdown_syntax.ITALIC_START, markdown_syntax.ITALIC_END, xmltags.ITALIC), - (markdown_syntax.ITALIC_ALTERNATIVE_START, markdown_syntax.ITALIC_ALTERNATIVE_END, xmltags.ITALIC), - (markdown_syntax.STRIKETROUGH_START, markdown_syntax.STRIKETROUGH_END, xmltags.STRIKETHROUGH), + ( + markdown_syntax.ITALIC_ALTERNATIVE_START, + markdown_syntax.ITALIC_ALTERNATIVE_END, + xmltags.ITALIC, + ), + ( + markdown_syntax.STRIKETROUGH_START, + markdown_syntax.STRIKETROUGH_END, + xmltags.STRIKETHROUGH, + ), ] while line: format_found = False diff --git a/tests/test_converter.py b/tests/test_converter.py index 0d3d7c80..ebcd61f1 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -5,7 +5,7 @@ from unittest.mock import Mock from converter import Converter -from builder import Builder +from builder.builder import Builder class ConverterTestCase(unittest.TestCase): diff --git a/tests/test_markdown_converter.py b/tests/test_markdown_converter.py index a5d2f93d..cca67f0d 100644 --- a/tests/test_markdown_converter.py +++ b/tests/test_markdown_converter.py @@ -8,9 +8,6 @@ from markdown_converter import MarkdownConverter -# pylint: disable=unsupported-assignment-operation,unsubscriptable-object - - class MarkdownConverterTestCase(unittest.TestCase): """Base class for Markdown converter tests.""" @@ -61,7 +58,10 @@ def test_frontpage_unknown_document_type(self): def test_frontpage_title(self): """Test the front page title.""" self.settings["FrontPage"] = "ICTU" - self.assertEqual(self.settings["Title"], self.xml().find(xmltags.FRONTPAGE).find(xmltags.TITLE).text) + self.assertEqual( + self.settings["Title"], + self.xml().find(xmltags.FRONTPAGE).find(xmltags.TITLE).text, + ) def test_toc(self): """Test the table of contents.""" @@ -108,7 +108,8 @@ def test_close_implicit_sections(self): self.assertEqual("Heading 1", section2.find(xmltags.HEADING).text) @patch( - "markdown_converter.open", mock_open(read_data="**Bold** _Italic_ ~~Strike~~ {Instruction} [Anchor](link)") + "markdown_converter.open", + mock_open(read_data="**Bold** _Italic_ ~~Strike~~ {Instruction} [Anchor](link)"), ) def test_formatting(self): """Test formatting.""" @@ -140,11 +141,20 @@ def test_numbered_list(self): def test_table(self): """Test table.""" table = self.xml().find(xmltags.TABLE) - self.assertEqual(("1", "3"), (table.attrib[xmltags.TABLE_ROWS], table.attrib[xmltags.TABLE_COLUMNS])) + self.assertEqual( + ("1", "3"), + (table.attrib[xmltags.TABLE_ROWS], table.attrib[xmltags.TABLE_COLUMNS]), + ) self.assertEqual("col1", table.find(xmltags.TABLE_HEADER_ROW).find(xmltags.TABLE_CELL).text) cell = table.find(xmltags.TABLE_ROW).find(xmltags.TABLE_CELL) self.assertEqual("cell1", cell.text) - self.assertEqual(("1", "0"), (cell.attrib[xmltags.TABLE_CELL_ROW], cell.attrib[xmltags.TABLE_CELL_COLUMN])) + self.assertEqual( + ("1", "0"), + ( + cell.attrib[xmltags.TABLE_CELL_ROW], + cell.attrib[xmltags.TABLE_CELL_COLUMN], + ), + ) self.assertEqual("center", cell.attrib[xmltags.TABLE_CELL_ALIGNMENT]) self.assertEqual(str(len("cell1")), cell.attrib[xmltags.TABLE_CELL_WIDTH]) @@ -152,7 +162,10 @@ def test_table(self): def test_table_marker_without_columns(self): """Test an incomplete table.""" table = self.xml().find(xmltags.TABLE) - self.assertEqual(("0", "1"), (table.attrib[xmltags.TABLE_ROWS], table.attrib[xmltags.TABLE_COLUMNS])) + self.assertEqual( + ("0", "1"), + (table.attrib[xmltags.TABLE_ROWS], table.attrib[xmltags.TABLE_COLUMNS]), + ) self.assertEqual("text", table.find(xmltags.TABLE_HEADER_ROW).find(xmltags.TABLE_CELL).text) @patch("markdown_converter.open", mock_open(read_data="Replace $var$.")) @@ -160,14 +173,18 @@ def test_variable(self): """Test that a variable is replaced with its value.""" self.assertEqual("Replace variable.", self.xml().find(xmltags.PARAGRAPH).text) - @patch("markdown_converter.open", mock_open(read_data="Replace [anchor](https://$var$/).\n")) + @patch( + "markdown_converter.open", + mock_open(read_data="Replace [anchor](https://$var$/).\n"), + ) def test_variable_in_url(self): """Test that a variable in a URL is replaced with its value.""" anchor_link = self.xml().find(xmltags.PARAGRAPH).find(xmltags.ANCHOR).attrib[xmltags.ANCHOR_LINK] self.assertEqual("https://variable/", anchor_link) @patch( - "markdown_converter.open", mock_open(read_data="\nMeasure\n\n") + "markdown_converter.open", + mock_open(read_data="\nMeasure\n\n"), ) def test_measure(self): """Test measure.""" @@ -176,5 +193,8 @@ def test_measure(self): @patch("markdown_converter.open", new_callable=mock_open, read_data="#include 'file'") def test_include(self, mocked_open): """Test that Markdown files can be included.""" - mocked_open.side_effect = (mocked_open.return_value, mock_open(read_data="included\n").return_value) + mocked_open.side_effect = ( + mocked_open.return_value, + mock_open(read_data="included\n").return_value, + ) self.assertEqual("included", self.xml().find(xmltags.PARAGRAPH).text) diff --git a/thirdparty/WCAG/wcag.py b/thirdparty/WCAG/wcag.py index 5ad44935..2d0ff6c6 100644 --- a/thirdparty/WCAG/wcag.py +++ b/thirdparty/WCAG/wcag.py @@ -1,10 +1,8 @@ """Script to convert the WCAG success critera to a Markdown table, and include which Axe-core rules check which WCAG criteria.""" -import ast import json import pathlib -import re import subprocess @@ -45,7 +43,12 @@ def node(script): # Generate the Markdown table lines = [] -lines.extend([f"| Item | Omschrijving | Niveau | Axe-core {axe_core_version} regels |", "| :--- | :--- | :--- | :--- |"]) +lines.extend( + [ + f"| Item | Omschrijving | Niveau | Axe-core {axe_core_version} regels |", + "| :--- | :--- | :--- | :--- |", + ] +) for principle in wcag["principles"]: lines.append(f"| Principe {principle['num']} | {item_url(principle)} | | |") for guideline in principle["guidelines"]: