From 8ee0a3d09bc696baf792af80ec03be6de37b2417 Mon Sep 17 00:00:00 2001 From: oniboni Date: Sun, 2 Jun 2024 13:32:47 +0000 Subject: [PATCH 01/15] ci: fix deploy job name --- .github/workflows/python-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 77779a2..ad2209d 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -13,7 +13,7 @@ env: FORCE_COLOR: "1" jobs: - deploy-test: + deploy: runs-on: ubuntu-latest From 0d1dffdaf8b8275cb05f6160cbf3717b946dd466 Mon Sep 17 00:00:00 2001 From: oniboni Date: Sun, 2 Jun 2024 13:39:52 +0000 Subject: [PATCH 02/15] build: add mypy type checker --- pyproject.toml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 73fb328..d97d1ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,7 @@ include = [ [tool.hatch.envs.test] template = "hatch-test" [tool.hatch.envs.test.env-vars] +# start local test kroki: KROKI_SERVER_URL = "http://localhost:8080" [tool.hatch.envs.hatch-test] @@ -78,7 +79,7 @@ extra-dependencies = [ "click", ] [[tool.hatch.envs.hatch-test.matrix]] -python = ["3.10", "3.11", "3.12"] +python = ["3.9", "3.10", "3.11", "3.12", "3.13"] [tool.hatch.envs.hatch-test.scripts] run = "pytest {env:HATCH_TEST_ARGS:} {args:tests}" run-cov = "coverage run -m pytest {env:HATCH_TEST_ARGS:} {args:tests} --junitxml=junit/test-results.xml" @@ -87,3 +88,11 @@ cov-report = [ "coverage xml", "coverage report --omit='tests/*'", ] + +[tool.hatch.envs.types] +template = "hatch-test" +extra-dependencies = [ + "mypy", +] +[tool.hatch.envs.types.scripts] +check = "mypy --install-types --non-interactive ." From ee59e4a93078634619332d32098ac8dd89a279c5 Mon Sep 17 00:00:00 2001 From: oniboni Date: Sun, 2 Jun 2024 13:53:00 +0000 Subject: [PATCH 03/15] fix: check diagram types for file type overrides --- kroki/diagram_types.py | 58 +++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/kroki/diagram_types.py b/kroki/diagram_types.py index a9b04f2..eb77cc6 100644 --- a/kroki/diagram_types.py +++ b/kroki/diagram_types.py @@ -1,6 +1,8 @@ from collections import ChainMap from typing import ClassVar +from mkdocs.exceptions import PluginError + from kroki.logging import log @@ -66,7 +68,7 @@ def __init__( ): self._fence_prefix = fence_prefix - kroki_types = ChainMap( + diagram_type_file_ext_map = ChainMap( self._kroki_base, self._kroki_blockdiag if blockdiag_enabled else {}, self._kroki_bpmn if bpmn_enabled else {}, @@ -75,30 +77,52 @@ def __init__( self._kroki_diagramsnet if diagramsnet_enabled else {}, ) - self._kroki_types_supporting_file = {} + self._kroki_type_file_ext_mapping: dict[str, str] = self._get_kroki_type_file_ext_mapping( + diagram_type_file_ext_map, file_types, file_type_overrides + ) - for kroki_type, kroki_file_types in kroki_types.items(): - kroki_file_type = next(filter(lambda file: file in kroki_file_types, file_types), None) - if kroki_file_type is not None: - self._kroki_types_supporting_file[kroki_type] = next( - filter(lambda file: file in kroki_file_types, file_types), None - ) + log.debug("File and Diagram types configured: %s", self._kroki_type_file_ext_mapping) - for kroki_type, kroki_file_type in file_type_overrides.items(): - self._kroki_types_supporting_file[kroki_type] = kroki_file_type + def _get_kroki_type_file_ext_mapping( + self, + diagram_type_file_ext_map: ChainMap[str, list[str]], + file_types: list[str], + file_type_overrides: dict[str, str], + ) -> dict[str, str]: + def get_file_type(kroki_type: str) -> str: + supported_file_types = diagram_type_file_ext_map[kroki_type] + file_type_override = file_type_overrides.get(kroki_type) + if file_type_override is not None: + if file_type_override not in supported_file_types: + err_msg = ( + f"{kroki_type}: {file_type_override} not in supported file types: " f"{supported_file_types}" + ) + raise PluginError(err_msg) + return file_type_override + + try: + target_file_type = next(t for t in file_types if t in supported_file_types) + except StopIteration as e: + err_msg = ( + f"{kroki_type}: Not able to satisfy any of {file_types}, " + f"supported file types: {supported_file_types}" + ) + raise PluginError(err_msg) from e + else: + return target_file_type - log.debug("File and Diagram types configured: %s", self._kroki_types_supporting_file) + return {kroki_type: get_file_type(kroki_type) for kroki_type in diagram_type_file_ext_map} def get_file_ext(self, kroki_type: str) -> str: - return self._kroki_types_supporting_file[kroki_type] + return self._kroki_type_file_ext_mapping[kroki_type] - def get_kroki_type(self, block_type: None | str) -> None | str: + def get_kroki_type(self, block_type: None | str) -> str | None: if block_type is None: - return + return None if not block_type.startswith(self._fence_prefix): - return + return None kroki_type = block_type.removeprefix(self._fence_prefix).lower() - if kroki_type not in self._kroki_types_supporting_file: - return + if kroki_type not in self._kroki_type_file_ext_mapping: + return None return kroki_type From ffb65e34f3160db8487bee0e9f3fa74be06e52c4 Mon Sep 17 00:00:00 2001 From: oniboni Date: Sun, 2 Jun 2024 14:08:49 +0000 Subject: [PATCH 04/15] fix: python types --- kroki/client.py | 4 +- kroki/config.py | 5 +- kroki/render.py | 3 +- tests/test_errors.py | 40 ++++++++++------ tests/test_errors_fail_fast.py | 20 ++++---- tests/test_fences.py | 62 ++++++++----------------- tests/test_nested.py | 4 +- tests/utils.py | 83 ++++++++++++++++++---------------- 8 files changed, 111 insertions(+), 110 deletions(-) diff --git a/kroki/client.py b/kroki/client.py index d0cc823..ee3893e 100644 --- a/kroki/client.py +++ b/kroki/client.py @@ -20,13 +20,13 @@ class DownloadedContent: def _ugly_temp_excalidraw_fix(self) -> None: """TODO: remove me, when excalidraw container works again.. ref: https://github.com/excalidraw/excalidraw/issues/7366""" - self.file_content = self.file_content.replace( + self.file_content: bytes = self.file_content.replace( b"https://unpkg.com/@excalidraw/excalidraw@undefined/dist", b"https://unpkg.com/@excalidraw/excalidraw@0.17.1/dist", ) def __init__(self, file_content: bytes, file_extension: str, additional_metadata: None | dict) -> None: - file_uuid = uuid3(NAMESPACE_OID, f"{additional_metadata}{file_content}") + file_uuid = uuid3(NAMESPACE_OID, f"{additional_metadata}{file_content!r}") self.file_name = f"{FILE_PREFIX}{file_uuid}.{file_extension}" self.file_content = file_content diff --git a/kroki/config.py b/kroki/config.py index 1077eb0..58278e8 100644 --- a/kroki/config.py +++ b/kroki/config.py @@ -7,8 +7,11 @@ class DeprecatedDownloadImagesCompat(config_options.Deprecated): - def pre_validation(self, config: "KrokiPluginConfig", key_name: str) -> None: + def pre_validation(self, config: MkDocsBaseConfig, key_name: str) -> None: """Set `HttpMethod: 'POST'`, if enabled""" + if not isinstance(config, KrokiPluginConfig): + return + if config.get(key_name) is None: return diff --git a/kroki/render.py b/kroki/render.py index 05b97f8..fb06278 100644 --- a/kroki/render.py +++ b/kroki/render.py @@ -22,7 +22,8 @@ def _get_media_type(self, file_ext: str) -> str: case "pdf": return "application/pdf" case _: - raise NotImplementedError(file_ext) + err_msg = f"Not implemented: '{file_ext}" + raise PluginError(err_msg) def _image_response(self, image_src: ImageSrc) -> str: media_type = self._get_media_type(image_src.file_ext) diff --git a/tests/test_errors.py b/tests/test_errors.py index 52266a5..d64b04c 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -4,56 +4,66 @@ from tests.utils import MkDocsHelper, get_expected_log_line +def _assert_error_block(err_msg:str, index_html: str): + index_soup = bs4.BeautifulSoup(index_html) + details_tag = index_soup.find("details") + assert isinstance(details_tag, bs4.Tag), "Error message container not in resulting HTML" + summary_tag = details_tag.summary + assert isinstance(summary_tag, bs4.Tag), "Error message container has no summary element" + assert err_msg in summary_tag.text + + @pytest.mark.usefixtures("kroki_timeout") def test_request_timeout() -> None: + # Arrange with MkDocsHelper("happy_path") as mkdocs_helper: mkdocs_helper.set_http_method("POST") - + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 0 assert get_expected_log_line("Request error") in result.output with open(mkdocs_helper.test_dir / "site/index.html") as index_html_file: - index_soup = bs4.BeautifulSoup(index_html_file.read()) - assert "Request error" in index_soup.find("details").summary.text + _assert_error_block("Request error", index_html_file.read()) @pytest.mark.usefixtures("kroki_bad_request") def test_request_bad_request() -> None: + # Arrange with MkDocsHelper("happy_path") as mkdocs_helper: mkdocs_helper.set_http_method("POST") - + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 0 assert get_expected_log_line("Diagram error!") in result.output with open(mkdocs_helper.test_dir / "site/index.html") as index_html_file: - index_soup = bs4.BeautifulSoup(index_html_file.read()) - assert "Diagram error!" in index_soup.find("details").summary.text + _assert_error_block("Diagram error!", index_html_file.read()) @pytest.mark.usefixtures("kroki_is_a_teapot") def test_request_other_error() -> None: + # Arrange with MkDocsHelper("happy_path") as mkdocs_helper: mkdocs_helper.set_http_method("POST") - + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 0 assert get_expected_log_line("Could not retrieve image data") in result.output with open(mkdocs_helper.test_dir / "site/index.html") as index_html_file: - index_soup = bs4.BeautifulSoup(index_html_file.read()) - assert "Could not retrieve image data" in index_soup.find("details").summary.text + _assert_error_block("Could not retrieve image data", index_html_file.read()) @pytest.mark.usefixtures("kroki_dummy") def test_missing_file_from() -> None: + # Arrange with MkDocsHelper("missing_from_file") as mkdocs_helper: mkdocs_helper.set_http_method("POST") + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 0 assert get_expected_log_line("Can't read file:") in result.output with open(mkdocs_helper.test_dir / "site/index.html") as index_html_file: - index_soup = bs4.BeautifulSoup(index_html_file.read()) - assert "Can't read file: " in index_soup.find("details").summary.text + _assert_error_block("Can't read file: ", index_html_file.read()) diff --git a/tests/test_errors_fail_fast.py b/tests/test_errors_fail_fast.py index 493facb..e8008b2 100644 --- a/tests/test_errors_fail_fast.py +++ b/tests/test_errors_fail_fast.py @@ -5,12 +5,13 @@ @pytest.mark.usefixtures("kroki_timeout") def test_request_timeout() -> None: + # Arrange with MkDocsHelper("happy_path") as mkdocs_helper: mkdocs_helper.set_http_method("POST") mkdocs_helper.enable_fail_fast() - + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 1 assert get_expected_log_line("Request error") in result.output assert "Aborted with a BuildError!" in result.output @@ -18,12 +19,13 @@ def test_request_timeout() -> None: @pytest.mark.usefixtures("kroki_bad_request") def test_request_bad_request() -> None: + # Arrange with MkDocsHelper("happy_path") as mkdocs_helper: mkdocs_helper.set_http_method("POST") mkdocs_helper.enable_fail_fast() - + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 1 assert get_expected_log_line("Diagram error!") in result.output assert "Aborted with a BuildError!" in result.output @@ -31,12 +33,13 @@ def test_request_bad_request() -> None: @pytest.mark.usefixtures("kroki_is_a_teapot") def test_request_other_error() -> None: + # Arrange with MkDocsHelper("happy_path") as mkdocs_helper: mkdocs_helper.set_http_method("POST") mkdocs_helper.enable_fail_fast() - + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 1 assert get_expected_log_line("Could not retrieve image data") in result.output assert "Aborted with a BuildError!" in result.output @@ -44,11 +47,12 @@ def test_request_other_error() -> None: @pytest.mark.usefixtures("kroki_dummy") def test_missing_file_from() -> None: + # Arrange with MkDocsHelper("missing_from_file") as mkdocs_helper: mkdocs_helper.enable_fail_fast() - + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 1 assert get_expected_log_line("Can't read file:") in result.output assert "Aborted with a BuildError!" in result.output diff --git a/tests/test_fences.py b/tests/test_fences.py index dceade2..5359d83 100644 --- a/tests/test_fences.py +++ b/tests/test_fences.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field import pytest from pytest_mock import MockerFixture @@ -11,9 +11,9 @@ @dataclass class StubInput: page_data: str - expected_code_block_data: str | None = None - epxected_options: dict | None = None - expected_kroki_type: str | None = None + expected_code_block_data: str = "" + epxected_options: dict = field(default_factory=dict) + expected_kroki_type: str = "" TEST_CASES = { @@ -26,7 +26,6 @@ class StubInput: """, expected_code_block_data="stuff containing ```\n\n", expected_kroki_type="plantuml", - epxected_options={}, ), "https://pandoc.org/MANUAL.html#fenced-code-blocks": StubInput( page_data="""~~~~~~~~~~~~~~~~ mermaid @@ -36,7 +35,6 @@ class StubInput: ~~~~~~~~~~~~~~~~""", expected_code_block_data="~~~~~~~~~~\ncode including tildes\n~~~~~~~~~~\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-119": StubInput( page_data=""" @@ -47,7 +45,6 @@ class StubInput: """, expected_code_block_data="<\n >\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-120": StubInput( page_data=""" @@ -58,7 +55,6 @@ class StubInput: """, expected_code_block_data="<\n >\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-122": StubInput( page_data=""" @@ -69,7 +65,6 @@ class StubInput: """, expected_code_block_data="aaa\n~~~\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-123": StubInput( page_data=""" @@ -80,7 +75,6 @@ class StubInput: """, expected_code_block_data="aaa\n```\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-125": StubInput( page_data=""" @@ -91,7 +85,6 @@ class StubInput: """, expected_code_block_data="aaa\n~~~\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-129": StubInput( page_data=""" @@ -102,7 +95,6 @@ class StubInput: """, expected_code_block_data="\n\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-130": StubInput( page_data=""" @@ -111,7 +103,6 @@ class StubInput: """, expected_code_block_data="", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-147": StubInput( page_data=""" @@ -121,7 +112,6 @@ class StubInput: """, expected_code_block_data="``` aaa\n", expected_kroki_type="mermaid", - epxected_options={}, ), } @@ -136,7 +126,6 @@ class StubInput: """, expected_code_block_data="aaa\n aaa\naaa\n", # "aaa\naaa\naaa\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-133": StubInput( page_data=""" @@ -148,7 +137,15 @@ class StubInput: """, expected_code_block_data=" aaa\n aaa\naaa\n", # "aaa\n aaa\naaa\n", expected_kroki_type="mermaid", - epxected_options={}, + ), + "https://spec.commonmark.org/0.31.2/#example-134": StubInput( + page_data=""" + ``` mermaid + aaa + ``` +""", + expected_code_block_data="aaa\n", # should not be replaced.. + expected_kroki_type="mermaid", ), } @@ -161,7 +158,6 @@ class StubInput: """, expected_code_block_data="foo\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-124": StubInput( page_data=""" @@ -172,7 +168,6 @@ class StubInput: """, expected_code_block_data="aaa\n```\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-126": StubInput( page_data=""" @@ -180,7 +175,6 @@ class StubInput: """, expected_code_block_data="\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-127": StubInput( page_data=""" @@ -191,7 +185,6 @@ class StubInput: """, expected_code_block_data="\n```\naaa\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-128": StubInput( page_data=""" @@ -202,7 +195,6 @@ class StubInput: """, expected_code_block_data="aaa\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-131": StubInput( page_data=""" @@ -213,17 +205,6 @@ class StubInput: """, expected_code_block_data="aaa\naaa\n", expected_kroki_type="mermaid", - epxected_options={}, - ), - "https://spec.commonmark.org/0.31.2/#example-134": StubInput( - page_data=""" - ``` mermaid - aaa - ```, - expected_code_block_data="aaa\n", # should not be replaced.. - expected_kroki_type="mermaid", - epxected_options={}, -""" ), "https://spec.commonmark.org/0.31.2/#example-135": StubInput( page_data=""" @@ -233,7 +214,6 @@ class StubInput: """, expected_code_block_data="aaa\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-136": StubInput( page_data=""" @@ -243,7 +223,6 @@ class StubInput: """, expected_code_block_data="aaa\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-140": StubInput( page_data=""" @@ -255,7 +234,6 @@ class StubInput: """, expected_code_block_data="bar\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-141": StubInput( page_data=""" @@ -268,7 +246,6 @@ class StubInput: """, expected_code_block_data="bar\n", expected_kroki_type="mermaid", - epxected_options={}, ), "https://spec.commonmark.org/0.31.2/#example-146": StubInput( page_data=""" @@ -278,7 +255,6 @@ class StubInput: """, expected_code_block_data="foo\n", expected_kroki_type="mermaid", - epxected_options={}, ), } @@ -289,13 +265,13 @@ class StubInput: + [pytest.param(v, id=k) for k, v in TEST_CASES_NOT_COMPLYING.items()], ) def test_fences(test_data: StubInput, mock_kroki_diagram_types: KrokiDiagramTypes, mocker: MockerFixture) -> None: + # Arrange parser = MarkdownParser("", mock_kroki_diagram_types) - callback_stub = mocker.stub() context_stub = mocker.stub() - + # Act parser.replace_kroki_blocks(test_data.page_data, callback_stub, context_stub) - + # Assert callback_stub.assert_called_once_with( KrokiImageContext( kroki_type=test_data.expected_kroki_type, @@ -310,11 +286,11 @@ def test_fences(test_data: StubInput, mock_kroki_diagram_types: KrokiDiagramType def test_fences_not_supported( test_data: StubInput, mock_kroki_diagram_types: KrokiDiagramTypes, mocker: MockerFixture ) -> None: + # Arrange parser = MarkdownParser("", mock_kroki_diagram_types) - callback_stub = mocker.stub() context_stub = mocker.stub() - + # Act parser.replace_kroki_blocks(test_data.page_data, callback_stub, context_stub) - + # Assert callback_stub.assert_not_called() diff --git a/tests/test_nested.py b/tests/test_nested.py index 9012d99..640e4a8 100644 --- a/tests/test_nested.py +++ b/tests/test_nested.py @@ -6,6 +6,7 @@ @pytest.mark.usefixtures("kroki_dummy") def test_block_inside_html() -> None: + # Arrange with MkDocsTemplateHelper("""
Show Sequence diagram... @@ -21,8 +22,9 @@ def test_block_inside_html() -> None: ``` """) as mkdocs_helper: mkdocs_helper.set_http_method("POST") + # Act result = mkdocs_helper.invoke_build() - + # Assert assert result.exit_code == 0, f"exit code {result.exit_code}, expected 0" with open(mkdocs_helper.test_dir / "site/index.html") as index_html: index_soup = bs4.BeautifulSoup(index_html.read()) diff --git a/tests/utils.py b/tests/utils.py index 664bf6e..65511a5 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -5,7 +5,7 @@ from contextlib import AbstractContextManager from pathlib import Path from string import Template -from typing import Literal +from typing import Final, Literal import yaml from click.testing import CliRunner, Result @@ -25,62 +25,67 @@ def __init__(self) -> None: class MkDocsHelper(AbstractContextManager): + class Context: + def __init__(self, test_dir: Path) -> None: + self.test_dir: Final[Path] = test_dir + self.config_file_path: Final[Path] = test_dir / "mkdocs.yml" + with open(self.test_dir / "mkdocs.yml") as file: + self.config_file = yaml.safe_load(file) + + def _dump_config(self) -> None: + with open(self.config_file_path, mode="w") as file: + yaml.safe_dump(self.config_file, file) + + def _get_plugin_config_entry(self) -> dict: + for plugin_entry in self.config_file["plugins"]: + if "kroki" in plugin_entry: + return plugin_entry["kroki"] + raise NoPluginEntryError + + def enable_fail_fast(self) -> None: + self._get_plugin_config_entry()["FailFast"] = True + + def set_http_method(self, method: Literal["GET", "POST"]) -> None: + self._get_plugin_config_entry()["HttpMethod"] = method + + def set_fence_prefix(self, fence_prefix: str) -> None: + self._get_plugin_config_entry()["FencePrefix"] = fence_prefix + + def invoke_build(self) -> Result: + self._dump_config() + runner = CliRunner() + with chdir(self.test_dir): + return runner.invoke(build_command) + def __init__(self, test_case: str) -> None: - self.config_file = None self.test_case = test_case self.test_dir = Path(tempfile.mkdtemp()) - self._copy_test_case() def _copy_test_case(self) -> None: # equals to `../data`, using current source file as a pin data_dir = pathlib.Path(os.path.realpath(__file__)).parent / "data" shutil.copytree(data_dir / self.test_case, self.test_dir, dirs_exist_ok=True) - def _load_config(self) -> None: - with open(self.test_dir / "mkdocs.yml") as file: - self.config_file = yaml.safe_load(file) - - def _dump_config(self) -> None: - with open(self.test_dir / "mkdocs.yml", mode="w") as file: - yaml.safe_dump(self.config_file, file) - - def __enter__(self) -> "MkDocsHelper": - self._load_config() - - return self + def __enter__(self) -> Context: + self._copy_test_case() + return MkDocsHelper.Context(self.test_dir) def __exit__(self, *_args) -> None: shutil.rmtree(self.test_dir) - def _get_plugin_config_entry(self) -> dict: - for plugin_entry in self.config_file["plugins"]: - if "kroki" in plugin_entry: - return plugin_entry["kroki"] - raise NoPluginEntryError - - def enable_fail_fast(self) -> None: - self._get_plugin_config_entry()["FailFast"] = True - - def set_http_method(self, method: Literal["GET", "POST"]) -> None: - self._get_plugin_config_entry()["HttpMethod"] = method - - def set_fence_prefix(self, fence_prefix: str) -> None: - self._get_plugin_config_entry()["FencePrefix"] = fence_prefix - - def invoke_build(self) -> Result: - self._dump_config() - runner = CliRunner() - with chdir(self.test_dir): - return runner.invoke(build_command) - class MkDocsTemplateHelper(MkDocsHelper): - def _substitute_code_block(self, code_block: str): + def _substitute_code_block(self): with open(self.test_dir / "docs/index.md") as in_file: file_content = Template(in_file.read()) with open(self.test_dir / "docs/index.md", "w") as out_file: - out_file.write(file_content.substitute(code_block=code_block)) + out_file.write(file_content.substitute(code_block=self.code_block)) def __init__(self, code_block: str) -> None: super().__init__("template") - self._substitute_code_block(code_block) + self.code_block = code_block + + def __enter__(self) -> MkDocsHelper.Context: + context = super().__enter__() + self._substitute_code_block() + return context From c3e94cd25449314a3280e3614a03b607ea7aa652 Mon Sep 17 00:00:00 2001 From: oniboni Date: Sun, 2 Jun 2024 18:32:39 +0000 Subject: [PATCH 05/15] build: add type checker to hooks & CI --- .github/workflows/lint.yml | 2 ++ .pre-commit-config.yaml | 6 ++++++ pyproject.toml | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index dd4c590..063b817 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,3 +31,5 @@ jobs: pip install --upgrade hatch - name: Lint run: hatch fmt --check + - name: Check types + run: hatch run types:check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14bc790..03fcc3f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,6 +17,12 @@ repos: language: system pass_filenames: false types: [ python ] + - id: check-types + name: run mypy + entry: hatch run types:check + language: system + pass_filenames: false + types: [ python ] - id: format name: run the formatter entry: hatch fmt -f diff --git a/pyproject.toml b/pyproject.toml index d97d1ff..9d52660 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -69,6 +68,7 @@ include = [ template = "hatch-test" [tool.hatch.envs.test.env-vars] # start local test kroki: +# docker-compose up KROKI_SERVER_URL = "http://localhost:8080" [tool.hatch.envs.hatch-test] @@ -79,7 +79,7 @@ extra-dependencies = [ "click", ] [[tool.hatch.envs.hatch-test.matrix]] -python = ["3.9", "3.10", "3.11", "3.12", "3.13"] +python = ["3.10", "3.11", "3.12"] [tool.hatch.envs.hatch-test.scripts] run = "pytest {env:HATCH_TEST_ARGS:} {args:tests}" run-cov = "coverage run -m pytest {env:HATCH_TEST_ARGS:} {args:tests} --junitxml=junit/test-results.xml" From ba84667747c8712807925d2a54f6d333b2b89b5e Mon Sep 17 00:00:00 2001 From: oniboni Date: Sun, 2 Jun 2024 18:38:45 +0000 Subject: [PATCH 06/15] style: format --- tests/test_errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_errors.py b/tests/test_errors.py index d64b04c..1e036c0 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -4,7 +4,7 @@ from tests.utils import MkDocsHelper, get_expected_log_line -def _assert_error_block(err_msg:str, index_html: str): +def _assert_error_block(err_msg: str, index_html: str): index_soup = bs4.BeautifulSoup(index_html) details_tag = index_soup.find("details") assert isinstance(details_tag, bs4.Tag), "Error message container not in resulting HTML" From 68e00dfe241720a9c01ca9b6161e8a20a600bc27 Mon Sep 17 00:00:00 2001 From: oniboni Date: Sun, 2 Jun 2024 20:27:19 +0000 Subject: [PATCH 07/15] build: add mypy types dependencies --- pyproject.toml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9d52660..d18f52c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,9 +33,9 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] dependencies = [ - "mkdocs>=1.5.0", - "requests>=2.27.0", - "result", + "mkdocs~=1.5.0", + "requests~=2.27.0", + "result~=0.17.0", ] [project.entry-points."mkdocs.plugins"] @@ -93,6 +93,10 @@ cov-report = [ template = "hatch-test" extra-dependencies = [ "mypy", + "types-PyYAML", + "types-beautifulsoup4", + "types-requests", + "types-babel", ] [tool.hatch.envs.types.scripts] -check = "mypy --install-types --non-interactive ." +check = "mypy --install-types --non-interactive {args:kroki tests}" From d62039e83ad84879a63af1af77ba1caf877e5eed Mon Sep 17 00:00:00 2001 From: oniboni Date: Sun, 14 Jul 2024 17:45:37 +0000 Subject: [PATCH 08/15] style: add type hint --- tests/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils.py b/tests/utils.py index 65511a5..a40f430 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -15,7 +15,7 @@ from tests.compat import chdir -def get_expected_log_line(log_msg) -> str: +def get_expected_log_line(log_msg: str) -> str: return f"{log.prefix}: {log_msg}" From eb57c5ad8d8ad4e1dc33fdf20c8ea740c355c2a6 Mon Sep 17 00:00:00 2001 From: oniboni Date: Sun, 14 Jul 2024 17:56:01 +0000 Subject: [PATCH 09/15] build: ease version pinning --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d18f52c..6a457a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,9 +33,9 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] dependencies = [ - "mkdocs~=1.5.0", - "requests~=2.27.0", - "result~=0.17.0", + "mkdocs>=1.5.0", + "requests>=2.27.0", + "result>=0.17.0", ] [project.entry-points."mkdocs.plugins"] From 0a74d6948fec8cee27b83b5c61921117e8fcf824 Mon Sep 17 00:00:00 2001 From: oniboni Date: Fri, 26 Jul 2024 15:48:19 +0000 Subject: [PATCH 10/15] refactor: unify naming --- kroki/diagram_types.py | 61 +++++++++++++++++----------------- tests/test_errors.py | 2 +- tests/test_errors_fail_fast.py | 2 +- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/kroki/diagram_types.py b/kroki/diagram_types.py index eb77cc6..8e41cbc 100644 --- a/kroki/diagram_types.py +++ b/kroki/diagram_types.py @@ -7,7 +7,7 @@ class KrokiDiagramTypes: - _kroki_base: ClassVar[dict[str, list[str]]] = { + _base_diagrams: ClassVar[dict[str, list[str]]] = { "bytefield": ["svg"], "ditaa": ["png", "svg"], "erd": ["png", "svg", "jpeg", "pdf"], @@ -29,7 +29,7 @@ class KrokiDiagramTypes: "wireviz": ["png", "svg"], } - _kroki_blockdiag: ClassVar[dict[str, list[str]]] = { + _blockdiag: ClassVar[dict[str, list[str]]] = { "blockdiag": ["png", "svg", "pdf"], "seqdiag": ["png", "svg", "pdf"], "actdiag": ["png", "svg", "pdf"], @@ -38,19 +38,19 @@ class KrokiDiagramTypes: "rackdiag": ["png", "svg", "pdf"], } - _kroki_bpmn: ClassVar[dict[str, list[str]]] = { + _bpmn: ClassVar[dict[str, list[str]]] = { "bpmn": ["svg"], } - _kroki_excalidraw: ClassVar[dict[str, list[str]]] = { + _excalidraw: ClassVar[dict[str, list[str]]] = { "excalidraw": ["svg"], } - _kroki_mermaid: ClassVar[dict[str, list[str]]] = { + _mermaid: ClassVar[dict[str, list[str]]] = { "mermaid": ["png", "svg"], } - _kroki_diagramsnet: ClassVar[dict[str, list[str]]] = { + _diagramsnet: ClassVar[dict[str, list[str]]] = { "diagramsnet": ["svg"], } @@ -69,60 +69,59 @@ def __init__( self._fence_prefix = fence_prefix diagram_type_file_ext_map = ChainMap( - self._kroki_base, - self._kroki_blockdiag if blockdiag_enabled else {}, - self._kroki_bpmn if bpmn_enabled else {}, - self._kroki_excalidraw if excalidraw_enabled else {}, - self._kroki_mermaid if mermaid_enabled else {}, - self._kroki_diagramsnet if diagramsnet_enabled else {}, + self._base_diagrams, + self._blockdiag if blockdiag_enabled else {}, + self._bpmn if bpmn_enabled else {}, + self._excalidraw if excalidraw_enabled else {}, + self._mermaid if mermaid_enabled else {}, + self._diagramsnet if diagramsnet_enabled else {}, ) - self._kroki_type_file_ext_mapping: dict[str, str] = self._get_kroki_type_file_ext_mapping( + self._file_ext_mapping: dict[str, str] = self._get_file_ext_mapping( diagram_type_file_ext_map, file_types, file_type_overrides ) - log.debug("File and Diagram types configured: %s", self._kroki_type_file_ext_mapping) + log.debug("File and Diagram types configured: %s", self._file_ext_mapping) - def _get_kroki_type_file_ext_mapping( + def _get_file_ext_mapping( self, diagram_type_file_ext_map: ChainMap[str, list[str]], file_types: list[str], file_type_overrides: dict[str, str], ) -> dict[str, str]: - def get_file_type(kroki_type: str) -> str: - supported_file_types = diagram_type_file_ext_map[kroki_type] - file_type_override = file_type_overrides.get(kroki_type) + def get_file_type(diagram_type: str) -> str: + supported_file_types = diagram_type_file_ext_map[diagram_type] + file_type_override = file_type_overrides.get(diagram_type) if file_type_override is not None: if file_type_override not in supported_file_types: err_msg = ( - f"{kroki_type}: {file_type_override} not in supported file types: " f"{supported_file_types}" + f"{diagram_type}: {file_type_override} not in supported file types: " f"{supported_file_types}" ) raise PluginError(err_msg) return file_type_override - try: - target_file_type = next(t for t in file_types if t in supported_file_types) - except StopIteration as e: + target_file_type = next((t for t in file_types if t in supported_file_types), None) + if target_file_type is None: err_msg = ( - f"{kroki_type}: Not able to satisfy any of {file_types}, " + f"{diagram_type}: Not able to satisfy any of {file_types}, " f"supported file types: {supported_file_types}" ) - raise PluginError(err_msg) from e - else: - return target_file_type + raise PluginError(err_msg) - return {kroki_type: get_file_type(kroki_type) for kroki_type in diagram_type_file_ext_map} + return target_file_type + + return {diagram_type: get_file_type(diagram_type) for diagram_type in diagram_type_file_ext_map} def get_file_ext(self, kroki_type: str) -> str: - return self._kroki_type_file_ext_mapping[kroki_type] + return self._file_ext_mapping[kroki_type] def get_kroki_type(self, block_type: None | str) -> str | None: if block_type is None: return None if not block_type.startswith(self._fence_prefix): return None - kroki_type = block_type.removeprefix(self._fence_prefix).lower() - if kroki_type not in self._kroki_type_file_ext_mapping: + diagram_type = block_type.removeprefix(self._fence_prefix).lower() + if diagram_type not in self._file_ext_mapping: return None - return kroki_type + return diagram_type diff --git a/tests/test_errors.py b/tests/test_errors.py index 1e036c0..3c26584 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -56,7 +56,7 @@ def test_request_other_error() -> None: @pytest.mark.usefixtures("kroki_dummy") -def test_missing_file_from() -> None: +def test_missing_from_file() -> None: # Arrange with MkDocsHelper("missing_from_file") as mkdocs_helper: mkdocs_helper.set_http_method("POST") diff --git a/tests/test_errors_fail_fast.py b/tests/test_errors_fail_fast.py index e8008b2..e3a8d01 100644 --- a/tests/test_errors_fail_fast.py +++ b/tests/test_errors_fail_fast.py @@ -46,7 +46,7 @@ def test_request_other_error() -> None: @pytest.mark.usefixtures("kroki_dummy") -def test_missing_file_from() -> None: +def test_missing_from_file() -> None: # Arrange with MkDocsHelper("missing_from_file") as mkdocs_helper: mkdocs_helper.enable_fail_fast() From 34e42df2f349da9759e76b64fd9cff72303e8d4f Mon Sep 17 00:00:00 2001 From: oniboni Date: Fri, 26 Jul 2024 15:54:27 +0000 Subject: [PATCH 11/15] test: add happy path test --- .../happy_path/docs/assets/diagram.plantuml | 3 +++ tests/data/happy_path/docs/index.md | 8 +++++++ tests/test_happy_path.py | 22 +++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 tests/data/happy_path/docs/assets/diagram.plantuml create mode 100644 tests/test_happy_path.py diff --git a/tests/data/happy_path/docs/assets/diagram.plantuml b/tests/data/happy_path/docs/assets/diagram.plantuml new file mode 100644 index 0000000..1da6ac5 --- /dev/null +++ b/tests/data/happy_path/docs/assets/diagram.plantuml @@ -0,0 +1,3 @@ +@startuml +Bob -> Alice : hello +@enduml \ No newline at end of file diff --git a/tests/data/happy_path/docs/index.md b/tests/data/happy_path/docs/index.md index 400bcd1..892cebe 100644 --- a/tests/data/happy_path/docs/index.md +++ b/tests/data/happy_path/docs/index.md @@ -1,5 +1,7 @@ # Test +## inline + ```c4plantuml !include !include @@ -29,3 +31,9 @@ Rel(site_host, db, "read") Rel(site_host, reader, "serve") ``` + +## from file + +```plantuml +@from_file:assets/diagram.plantuml +``` \ No newline at end of file diff --git a/tests/test_happy_path.py b/tests/test_happy_path.py new file mode 100644 index 0000000..8149520 --- /dev/null +++ b/tests/test_happy_path.py @@ -0,0 +1,22 @@ +import bs4 +import pytest + +from tests.utils import MkDocsHelper + + +@pytest.mark.usefixtures("kroki_dummy") +def test_missing_from_file() -> None: + # Arrange + with MkDocsHelper("happy_path") as mkdocs_helper: + mkdocs_helper.set_http_method("POST") + # Act + result = mkdocs_helper.invoke_build() + # Assert + assert result.exit_code == 0 + with open(mkdocs_helper.test_dir / "site/index.html") as index_html_file: + index_html = index_html_file.read() + + index_soup = bs4.BeautifulSoup(index_html) + img_tags = index_soup.find_all(attrs={"name":"Kroki"}) + + assert len(img_tags) == 2 From 9246e4dee0e08c0a853b43320bc9763c0bd1c100 Mon Sep 17 00:00:00 2001 From: oniboni Date: Fri, 26 Jul 2024 15:55:02 +0000 Subject: [PATCH 12/15] test: add docker-compose file for local kroki service --- docker-compose.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d2c603e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +services: + kroki: + image: docker.io/yuzutech/kroki + depends_on: + - mermaid + - bpmn + - excalidraw + environment: + - KROKI_MERMAID_HOST=mermaid + - KROKI_BPMN_HOST=bpmn + - KROKI_EXCALIDRAW_HOST=excalidraw + ports: + - "127.0.0.1:8080:8000" + mermaid: + image: docker.io/yuzutech/kroki-mermaid + bpmn: + image: docker.io/yuzutech/kroki-bpmn + excalidraw: + image: docker.io/yuzutech/kroki-excalidraw \ No newline at end of file From a737013994f2ccdac96803737fd3c457fc6b6276 Mon Sep 17 00:00:00 2001 From: oniboni Date: Fri, 26 Jul 2024 15:55:34 +0000 Subject: [PATCH 13/15] style: format --- docker-compose.yml | 2 +- tests/data/happy_path/docs/assets/diagram.plantuml | 2 +- tests/data/happy_path/docs/index.md | 2 +- tests/test_happy_path.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d2c603e..80778e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,4 +16,4 @@ services: bpmn: image: docker.io/yuzutech/kroki-bpmn excalidraw: - image: docker.io/yuzutech/kroki-excalidraw \ No newline at end of file + image: docker.io/yuzutech/kroki-excalidraw diff --git a/tests/data/happy_path/docs/assets/diagram.plantuml b/tests/data/happy_path/docs/assets/diagram.plantuml index 1da6ac5..a7b4074 100644 --- a/tests/data/happy_path/docs/assets/diagram.plantuml +++ b/tests/data/happy_path/docs/assets/diagram.plantuml @@ -1,3 +1,3 @@ @startuml Bob -> Alice : hello -@enduml \ No newline at end of file +@enduml diff --git a/tests/data/happy_path/docs/index.md b/tests/data/happy_path/docs/index.md index 892cebe..1c33850 100644 --- a/tests/data/happy_path/docs/index.md +++ b/tests/data/happy_path/docs/index.md @@ -36,4 +36,4 @@ Rel(site_host, reader, "serve") ```plantuml @from_file:assets/diagram.plantuml -``` \ No newline at end of file +``` diff --git a/tests/test_happy_path.py b/tests/test_happy_path.py index 8149520..d17adcf 100644 --- a/tests/test_happy_path.py +++ b/tests/test_happy_path.py @@ -17,6 +17,6 @@ def test_missing_from_file() -> None: index_html = index_html_file.read() index_soup = bs4.BeautifulSoup(index_html) - img_tags = index_soup.find_all(attrs={"name":"Kroki"}) + img_tags = index_soup.find_all(attrs={"name": "Kroki"}) assert len(img_tags) == 2 From fe02abdb5ed57f6fc260c58d9a157a66cb77bacd Mon Sep 17 00:00:00 2001 From: oniboni Date: Fri, 26 Jul 2024 16:00:24 +0000 Subject: [PATCH 14/15] test: rename happy path test --- tests/test_happy_path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_happy_path.py b/tests/test_happy_path.py index d17adcf..50bb6a6 100644 --- a/tests/test_happy_path.py +++ b/tests/test_happy_path.py @@ -5,7 +5,7 @@ @pytest.mark.usefixtures("kroki_dummy") -def test_missing_from_file() -> None: +def test_happy_path() -> None: # Arrange with MkDocsHelper("happy_path") as mkdocs_helper: mkdocs_helper.set_http_method("POST") From 7299dfecaf0c7de48ede5730f79e5deac32dbf2f Mon Sep 17 00:00:00 2001 From: oniboni Date: Fri, 26 Jul 2024 16:00:24 +0000 Subject: [PATCH 15/15] ci: add project version check before publish --- .github/workflows/python-publish.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index ad2209d..c8cb779 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -27,6 +27,16 @@ jobs: run: | python -m pip install --upgrade pip pip install --upgrade hatch + - name: Check project version + run: | + PROJECT_VERSION=$(TERM=dumb hatch version) + + if [ "$PROJECT_VERSION" != "${TAG_NAME#v}" ]; then + echo "Wrong project version, cannot release: $PROJECT_VERSION, ${TAG_NAME#v} expected" + exit 1 + fi + env: + TAG_NAME: ${{ github.ref }} - name: Build package run: hatch build - name: Publish package