From a6468502c006a59e771d991a2877d8245d1b8f9c Mon Sep 17 00:00:00 2001 From: etserend Date: Wed, 1 Jul 2026 18:57:44 -0500 Subject: [PATCH 1/4] test(docs): add unit tests for MDX helper functions in create_docs.py (HYBIM-725) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers _escape_curly_braces, _convert_rst_code_blocks, and _sanitize_description — the helpers added to fix prose-only example fencing and MDX brace escaping. --- tests/test_create_docs_helpers.py | 138 ++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 tests/test_create_docs_helpers.py diff --git a/tests/test_create_docs_helpers.py b/tests/test_create_docs_helpers.py new file mode 100644 index 0000000..4a84b26 --- /dev/null +++ b/tests/test_create_docs_helpers.py @@ -0,0 +1,138 @@ +"""Tests for MDX helper functions in scripts/create_docs.py. + +These functions are duplicated here to avoid importing the full script, +which depends on docstring_parser (not a project dev dependency). +The implementations must stay in sync with scripts/create_docs.py. +""" + +import re + + +def _escape_curly_braces(text: str) -> str: + triple_segments = re.split(r"(```.*?```)", text, flags=re.DOTALL) + for i, triple_seg in enumerate(triple_segments): + if triple_seg.startswith("```"): + continue + single_segments = re.split(r"(`[^`]*`)", triple_seg) + for j, single_seg in enumerate(single_segments): + if not single_seg.startswith("`"): + single_seg = single_seg.replace("{", r"\{").replace("}", r"\}") + single_segments[j] = single_seg + triple_segments[i] = "".join(single_segments) + return "".join(triple_segments) + + +def _convert_rst_code_blocks(text: str) -> str: + lines = text.split("\n") + out: list[str] = [] + i = 0 + while i < len(lines): + line = lines[i] + if line.rstrip().endswith("::"): + stripped = line.rstrip()[:-2].rstrip() + out.append(stripped) + i += 1 + block: list[str] = [] + while i < len(lines) and (lines[i].strip() == "" or lines[i].startswith(" ")): + block.append(lines[i]) + i += 1 + while block and block[-1].strip() == "": + block.pop() + if block: + out.append("```python") + for bl in block: + out.append(bl[4:] if bl.startswith(" ") else bl) + out.append("```") + else: + out.append(line) + i += 1 + return "\n".join(out) + + +def _sanitize_description(text: str) -> str: + text = _convert_rst_code_blocks(text) + text = _escape_curly_braces(text) + return text + + +class TestEscapeCurlyBraces: + def test_plain_text_unchanged(self): + assert _escape_curly_braces("hello world") == "hello world" + + def test_bare_braces_escaped(self): + assert _escape_curly_braces("foo {bar} baz") == r"foo \{bar\} baz" + + def test_inline_code_not_escaped(self): + result = _escape_curly_braces("use `{key}` here") + assert result == "use `{key}` here" + + def test_fenced_code_block_not_escaped(self): + text = '```python\nd = {"key": 1}\n```' + assert _escape_curly_braces(text) == text + + def test_braces_outside_fenced_block_escaped(self): + text = "Before {x}.\n```python\n{y}\n```\nAfter {z}." + result = _escape_curly_braces(text) + assert r"\{x\}" in result + assert "{y}" in result + assert r"\{z\}" in result + + def test_empty_string(self): + assert _escape_curly_braces("") == "" + + def test_multiple_inline_code_spans(self): + result = _escape_curly_braces("use `{a}` and `{b}` but escape {c}") + assert "`{a}`" in result + assert "`{b}`" in result + assert r"\{c\}" in result + + +class TestConvertRstCodeBlocks: + def test_no_rst_blocks_unchanged(self): + text = "Regular paragraph.\n\nAnother paragraph." + assert _convert_rst_code_blocks(text) == text + + def test_basic_rst_block_converted(self): + text = "Example::\n\n result = foo()\n return result\n" + result = _convert_rst_code_blocks(text) + assert "```python" in result + assert "result = foo()" in result + assert "return result" in result + assert "::" not in result + + def test_standalone_double_colon_removed(self): + text = "::\n\n code_here()\n" + result = _convert_rst_code_blocks(text) + assert "```python" in result + assert "code_here()" in result + assert "::" not in result + + def test_trailing_blank_lines_stripped_from_block(self): + text = "Example::\n\n foo()\n\n\n" + result = _convert_rst_code_blocks(text) + lines = result.strip().split("\n") + assert lines[-1] == "```" + + def test_indentation_stripped(self): + text = "Example::\n\n indented_line()\n" + result = _convert_rst_code_blocks(text) + assert " indented_line()" not in result + assert "indented_line()" in result + + def test_empty_string(self): + assert _convert_rst_code_blocks("") == "" + + +class TestSanitizeDescription: + def test_rst_and_braces_both_handled(self): + text = "Set config using {value}.\n\nExample::\n\n config({key: val})\n" + result = _sanitize_description(text) + assert r"\{value\}" in result + assert "```python" in result + assert "{key: val}" in result + + def test_plain_text_passthrough(self): + assert _sanitize_description("simple text") == "simple text" + + def test_empty_string(self): + assert _sanitize_description("") == "" From 340768cfbd7cc72248cba57e9f800f58122a8de8 Mon Sep 17 00:00:00 2001 From: etserend Date: Mon, 6 Jul 2026 12:28:46 -0500 Subject: [PATCH 2/4] =?UTF-8?q?test(docs):=20improve=20create=5Fdocs=20MDX?= =?UTF-8?q?=20helper=20tests=20=E2=80=94=20add=20docstring-parser=20as=20d?= =?UTF-8?q?ev=20dep=20(HYBIM-725)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add docstring-parser to test dependencies so helpers can be imported directly from scripts/create_docs.py instead of duplicated. Add 4 new test cases: adjacent fenced blocks, mixed inline/prose braces, prose-after-block, and sanitize ordering. Add class/method docstrings to match project test style. --- poetry.lock | 101 ++++++++++------------ pyproject.toml | 1 + tests/test_create_docs_helpers.py | 139 ++++++++++++++++-------------- 3 files changed, 121 insertions(+), 120 deletions(-) diff --git a/poetry.lock b/poetry.lock index 298e0b7..a6302d8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -173,19 +173,6 @@ files = [ frozenlist = ">=1.1.0" typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} -[[package]] -name = "annotated-doc" -version = "0.0.4" -description = "Document parameters, class attributes, return types, and variables inline, with Annotated." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"}, - {file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"}, -] -markers = {main = "(extra == \"crewai\" or extra == \"all\") and python_version <= \"3.13\""} - [[package]] name = "annotated-types" version = "0.7.0" @@ -627,14 +614,14 @@ dev = ["chroma-hnswlib (==0.7.6)", "fastapi (>=0.115.9)", "opentelemetry-instrum name = "click" version = "8.1.8" description = "Composable command line interface toolkit" -optional = true +optional = false python-versions = ">=3.7" -groups = ["main"] -markers = "sys_platform != \"emscripten\" and (extra == \"openai\" or extra == \"all\") or python_version <= \"3.13\" and sys_platform != \"emscripten\" and (extra == \"openai\" or extra == \"all\" or extra == \"crewai\") or python_version <= \"3.13\" and (extra == \"crewai\" or extra == \"all\")" +groups = ["main", "dev"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] +markers = {main = "sys_platform != \"emscripten\" and (extra == \"openai\" or extra == \"all\") or python_version <= \"3.13\" and sys_platform != \"emscripten\" and (extra == \"openai\" or extra == \"all\" or extra == \"crewai\") or python_version <= \"3.13\" and (extra == \"crewai\" or extra == \"all\")"} [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -920,14 +907,14 @@ markers = {main = "(extra == \"crewai\" or extra == \"all\" or extra == \"openai name = "docstring-parser" version = "0.17.0" description = "Parse Python docstrings in reST, Google and Numpydoc format" -optional = true +optional = false python-versions = ">=3.8" -groups = ["main"] -markers = "(extra == \"crewai\" or extra == \"all\") and python_version <= \"3.13\"" +groups = ["main", "test"] files = [ {file = "docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708"}, {file = "docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912"}, ] +markers = {main = "(extra == \"crewai\" or extra == \"all\") and python_version <= \"3.13\""} [package.extras] dev = ["pre-commit (>=2.16.0) ; python_version >= \"3.9\"", "pydoctor (>=25.4.0)", "pytest"] @@ -2937,26 +2924,28 @@ voice = ["numpy (>=2.2.0,<3) ; python_version >= \"3.10\"", "websockets (>=15.0, [[package]] name = "openapi-python-client" -version = "0.29.0" +version = "0.26.1" description = "Generate modern Python clients from OpenAPI" optional = false -python-versions = "<4.0,>=3.11" +python-versions = "<4.0,>=3.9" groups = ["dev"] files = [ - {file = "openapi_python_client-0.29.0-py3-none-any.whl", hash = "sha256:1085864c1e0a42fb50e1f5eb84363b19de07ebfb8a3f82a146c8529b948b8f12"}, - {file = "openapi_python_client-0.29.0.tar.gz", hash = "sha256:4ff439a63765aaef548e69c4eedd86dfad57891dc322709450af0c2c9a72a23e"}, + {file = "openapi_python_client-0.26.1-py3-none-any.whl", hash = "sha256:415cb8095b1a3f15cec45670c5075c5097f65390a351d21512e8f6ea5c1be644"}, + {file = "openapi_python_client-0.26.1.tar.gz", hash = "sha256:e3832f0ef074a0ab591d1eeb5d3dab2ca820cd0349f7e79d9663b7b21206be5d"}, ] [package.dependencies] attrs = ">=22.2.0" colorama = {version = ">=0.4.3", markers = "sys_platform == \"win32\""} -httpx = ">=0.23.1,<0.29.0" +httpx = ">=0.23.0,<0.29.0" jinja2 = ">=3.0.0,<4.0.0" pydantic = ">=2.10,<3.0.0" -ruamel-yaml = ">=0.18.6,<0.20.0" -ruff = ">=0.2" +python-dateutil = ">=2.8.1,<3.0.0" +ruamel-yaml = ">=0.18.6,<0.19.0" +ruff = ">=0.2,<0.14" shellingham = ">=1.3.2,<2.0.0" -typer = ">0.16,<0.27" +typer = ">0.6,<0.18" +typing-extensions = ">=4.8.0,<5.0.0" [[package]] name = "openpyxl" @@ -4420,7 +4409,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "test"] +groups = ["main", "dev", "test"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -5040,30 +5029,30 @@ files = [ [[package]] name = "ruff" -version = "0.15.20" +version = "0.12.8" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078"}, - {file = "ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b"}, - {file = "ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632"}, - {file = "ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd"}, - {file = "ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b"}, - {file = "ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267"}, - {file = "ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c"}, - {file = "ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae"}, - {file = "ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b"}, - {file = "ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487"}, - {file = "ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3"}, - {file = "ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053"}, - {file = "ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4"}, - {file = "ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460"}, - {file = "ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21"}, - {file = "ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415"}, - {file = "ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca"}, - {file = "ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566"}, + {file = "ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513"}, + {file = "ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc"}, + {file = "ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec"}, + {file = "ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53"}, + {file = "ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f"}, + {file = "ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f"}, + {file = "ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609"}, + {file = "ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a"}, + {file = "ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb"}, + {file = "ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818"}, + {file = "ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea"}, + {file = "ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3"}, + {file = "ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161"}, + {file = "ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46"}, + {file = "ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3"}, + {file = "ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e"}, + {file = "ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749"}, + {file = "ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033"}, ] [[package]] @@ -5085,7 +5074,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "test"] +groups = ["main", "dev", "test"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -5475,22 +5464,22 @@ telegram = ["requests"] [[package]] name = "typer" -version = "0.26.8" +version = "0.16.0" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false -python-versions = ">=3.10" +python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c"}, - {file = "typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e"}, + {file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"}, + {file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"}, ] markers = {main = "(extra == \"crewai\" or extra == \"all\") and python_version <= \"3.13\""} [package.dependencies] -annotated-doc = ">=0.0.2" -colorama = {version = "*", markers = "platform_system == \"Windows\""} -rich = ">=13.8.0" +click = ">=8.0.0" +rich = ">=10.11.0" shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" [[package]] name = "types-requests" @@ -6548,4 +6537,4 @@ otel = ["grpcio", "opentelemetry-api", "opentelemetry-exporter-otlp", "opentelem [metadata] lock-version = "2.1" python-versions = "^3.11,<3.15" -content-hash = "10b70f8ba837e4db50d7c61669cb285863b217c556d5c7df2b9ec978d3c1b486" +content-hash = "bd8c012c6d1fa222ef146ed6956d1ff0c658ad50c544514a26e3234188825864" diff --git a/pyproject.toml b/pyproject.toml index c34fd0c..2c49591 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ grpcio = { version = ">=1.80.0,<2.0.0", optional = true } [tool.poetry.group.test.dependencies] pytest = "^8.4.0" +docstring-parser = ">=0.17.0" coverage = "^7.9.2" pytest-cov = "^6.0.0" pytest-xdist = "^3.7.0" diff --git a/tests/test_create_docs_helpers.py b/tests/test_create_docs_helpers.py index 4a84b26..a411158 100644 --- a/tests/test_create_docs_helpers.py +++ b/tests/test_create_docs_helpers.py @@ -1,98 +1,84 @@ """Tests for MDX helper functions in scripts/create_docs.py. -These functions are duplicated here to avoid importing the full script, -which depends on docstring_parser (not a project dev dependency). -The implementations must stay in sync with scripts/create_docs.py. +Covers _escape_curly_braces, _convert_rst_code_blocks, and _sanitize_description — +the helpers that fix MDX brace escaping and RST code block conversion in generated docs. """ -import re - - -def _escape_curly_braces(text: str) -> str: - triple_segments = re.split(r"(```.*?```)", text, flags=re.DOTALL) - for i, triple_seg in enumerate(triple_segments): - if triple_seg.startswith("```"): - continue - single_segments = re.split(r"(`[^`]*`)", triple_seg) - for j, single_seg in enumerate(single_segments): - if not single_seg.startswith("`"): - single_seg = single_seg.replace("{", r"\{").replace("}", r"\}") - single_segments[j] = single_seg - triple_segments[i] = "".join(single_segments) - return "".join(triple_segments) - - -def _convert_rst_code_blocks(text: str) -> str: - lines = text.split("\n") - out: list[str] = [] - i = 0 - while i < len(lines): - line = lines[i] - if line.rstrip().endswith("::"): - stripped = line.rstrip()[:-2].rstrip() - out.append(stripped) - i += 1 - block: list[str] = [] - while i < len(lines) and (lines[i].strip() == "" or lines[i].startswith(" ")): - block.append(lines[i]) - i += 1 - while block and block[-1].strip() == "": - block.pop() - if block: - out.append("```python") - for bl in block: - out.append(bl[4:] if bl.startswith(" ") else bl) - out.append("```") - else: - out.append(line) - i += 1 - return "\n".join(out) - - -def _sanitize_description(text: str) -> str: - text = _convert_rst_code_blocks(text) - text = _escape_curly_braces(text) - return text +from __future__ import annotations + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts")) + +from create_docs import _escape_curly_braces, _convert_rst_code_blocks, _sanitize_description class TestEscapeCurlyBraces: + """Tests for _escape_curly_braces — escapes { } outside fenced/inline code for MDX.""" + def test_plain_text_unchanged(self): + """Text with no braces is returned as-is.""" assert _escape_curly_braces("hello world") == "hello world" def test_bare_braces_escaped(self): + """Bare braces in prose are escaped.""" assert _escape_curly_braces("foo {bar} baz") == r"foo \{bar\} baz" def test_inline_code_not_escaped(self): - result = _escape_curly_braces("use `{key}` here") - assert result == "use `{key}` here" + """Braces inside inline code spans are left alone.""" + assert _escape_curly_braces("use `{key}` here") == "use `{key}` here" def test_fenced_code_block_not_escaped(self): + """Braces inside a fenced code block are left alone.""" text = '```python\nd = {"key": 1}\n```' assert _escape_curly_braces(text) == text def test_braces_outside_fenced_block_escaped(self): + """Only braces outside fenced blocks are escaped.""" text = "Before {x}.\n```python\n{y}\n```\nAfter {z}." result = _escape_curly_braces(text) assert r"\{x\}" in result - assert "{y}" in result + assert "{y}" in result # inside fence — untouched assert r"\{z\}" in result - def test_empty_string(self): - assert _escape_curly_braces("") == "" + def test_two_adjacent_fenced_blocks_with_braces_between(self): + """Braces between two fenced blocks are escaped; content inside each is not.""" + text = "```python\n{a}\n```\n{between}\n```python\n{b}\n```" + result = _escape_curly_braces(text) + assert "{a}" in result + assert r"\{between\}" in result + assert "{b}" in result def test_multiple_inline_code_spans(self): + """Multiple inline code spans on one line — braces inside each left alone.""" result = _escape_curly_braces("use `{a}` and `{b}` but escape {c}") assert "`{a}`" in result assert "`{b}`" in result assert r"\{c\}" in result + def test_inline_and_prose_braces_on_same_line(self): + """Inline code braces and prose braces mixed on one line.""" + result = _escape_curly_braces("prefix {p} `mid {m}` suffix {s}") + assert r"\{p\}" in result + assert "`mid {m}`" in result + assert r"\{s\}" in result + + def test_empty_string(self): + """Empty input returns empty string.""" + assert _escape_curly_braces("") == "" + class TestConvertRstCodeBlocks: + """Tests for _convert_rst_code_blocks — converts RST :: blocks to fenced ```python.""" + def test_no_rst_blocks_unchanged(self): + """Text without :: markers is returned as-is.""" text = "Regular paragraph.\n\nAnother paragraph." assert _convert_rst_code_blocks(text) == text def test_basic_rst_block_converted(self): + """A :: paragraph produces a fenced python block with de-indented code.""" text = "Example::\n\n result = foo()\n return result\n" result = _convert_rst_code_blocks(text) assert "```python" in result @@ -101,38 +87,63 @@ def test_basic_rst_block_converted(self): assert "::" not in result def test_standalone_double_colon_removed(self): + """A standalone :: line is removed; blank separator line appears inside the fence.""" text = "::\n\n code_here()\n" result = _convert_rst_code_blocks(text) - assert "```python" in result - assert "code_here()" in result - assert "::" not in result + assert result == "\n```python\n\ncode_here()\n```" def test_trailing_blank_lines_stripped_from_block(self): + """Trailing blank lines after the block content are not emitted inside the fence.""" text = "Example::\n\n foo()\n\n\n" result = _convert_rst_code_blocks(text) - lines = result.strip().split("\n") - assert lines[-1] == "```" + assert result == "Example\n```python\n\nfoo()\n```" def test_indentation_stripped(self): + """Four-space indentation is removed from block lines.""" text = "Example::\n\n indented_line()\n" result = _convert_rst_code_blocks(text) assert " indented_line()" not in result assert "indented_line()" in result + def test_prose_after_block_preserved(self): + """Lines after the RST block (unindented) continue to appear correctly.""" + text = "Example::\n\n foo()\n\nMore prose here." + result = _convert_rst_code_blocks(text) + assert "```python" in result + assert "foo()" in result + assert "More prose here." in result + lines = result.split("\n") + fence_close = lines.index("```") + prose_idx = lines.index("More prose here.") + assert prose_idx > fence_close + def test_empty_string(self): + """Empty input returns empty string.""" assert _convert_rst_code_blocks("") == "" class TestSanitizeDescription: + """Tests for _sanitize_description — RST conversion then brace escaping.""" + def test_rst_and_braces_both_handled(self): + """RST block is fenced first; braces inside the fence are preserved, outside are escaped.""" text = "Set config using {value}.\n\nExample::\n\n config({key: val})\n" result = _sanitize_description(text) - assert r"\{value\}" in result - assert "```python" in result - assert "{key: val}" in result + assert r"\{value\}" in result # prose brace escaped + assert "```python" in result # RST block converted + assert "{key: val}" in result # brace inside fence left alone + + def test_ordering_rst_before_braces(self): + """_convert_rst_code_blocks runs before _escape_curly_braces so fenced braces are safe.""" + text = "Example::\n\n d = {1: 2}\n" + result = _sanitize_description(text) + assert "{1: 2}" in result # inside fence — must NOT be escaped + assert r"\{1" not in result def test_plain_text_passthrough(self): + """Plain text with no RST or braces is unchanged.""" assert _sanitize_description("simple text") == "simple text" def test_empty_string(self): + """Empty input returns empty string.""" assert _sanitize_description("") == "" From 8351e1d6b6096dad0acb273b3e3e0935e1c22009 Mon Sep 17 00:00:00 2001 From: etserend Date: Tue, 7 Jul 2026 11:45:26 -0500 Subject: [PATCH 3/4] test(docs): add edge case tests for :: in prose (HYBIM-725) Documents behavior when :: appears mid-line (namespace::method) and when a prose line ends with :: but has no indented block following. --- tests/test_create_docs_helpers.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_create_docs_helpers.py b/tests/test_create_docs_helpers.py index a411158..c48deed 100644 --- a/tests/test_create_docs_helpers.py +++ b/tests/test_create_docs_helpers.py @@ -117,6 +117,19 @@ def test_prose_after_block_preserved(self): prose_idx = lines.index("More prose here.") assert prose_idx > fence_close + def test_prose_ending_in_double_colon_no_indented_block(self): + """Prose line ending in :: with no indented block following loses the :: but is otherwise intact.""" + text = "Refer to namespace::\n\nNext paragraph (not indented)." + result = _convert_rst_code_blocks(text) + assert "```" not in result + assert "Refer to namespace" in result + assert "Next paragraph (not indented)." in result + + def test_double_colon_mid_line_not_treated_as_rst(self): + """:: mid-line (e.g. namespace::method) does not trigger RST block conversion.""" + text = "Use namespace::method() here.\n\nMore text." + assert _convert_rst_code_blocks(text) == text + def test_empty_string(self): """Empty input returns empty string.""" assert _convert_rst_code_blocks("") == "" From 3e793eb53acc1a1879db14c5e5b735f37d820e1e Mon Sep 17 00:00:00 2001 From: etserend Date: Tue, 7 Jul 2026 13:51:57 -0500 Subject: [PATCH 4/4] fix(deps): regenerate poetry.lock after docstring-parser constraint added to test group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit poetry.lock was stale — the >=0.17.0 constraint in [test] deps conflicted with the >=0.16,<1.0 metadata from the lockfile inherited from main. Co-Authored-By: Claude Opus 4.7 --- poetry.lock | 95 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 42 deletions(-) diff --git a/poetry.lock b/poetry.lock index a6302d8..3fcb5ee 100644 --- a/poetry.lock +++ b/poetry.lock @@ -173,6 +173,19 @@ files = [ frozenlist = ">=1.1.0" typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} +[[package]] +name = "annotated-doc" +version = "0.0.4" +description = "Document parameters, class attributes, return types, and variables inline, with Annotated." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"}, + {file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"}, +] +markers = {main = "(extra == \"crewai\" or extra == \"all\") and python_version <= \"3.13\""} + [[package]] name = "annotated-types" version = "0.7.0" @@ -614,14 +627,14 @@ dev = ["chroma-hnswlib (==0.7.6)", "fastapi (>=0.115.9)", "opentelemetry-instrum name = "click" version = "8.1.8" description = "Composable command line interface toolkit" -optional = false +optional = true python-versions = ">=3.7" -groups = ["main", "dev"] +groups = ["main"] +markers = "sys_platform != \"emscripten\" and (extra == \"openai\" or extra == \"all\") or python_version <= \"3.13\" and sys_platform != \"emscripten\" and (extra == \"openai\" or extra == \"all\" or extra == \"crewai\") or python_version <= \"3.13\" and (extra == \"crewai\" or extra == \"all\")" files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] -markers = {main = "sys_platform != \"emscripten\" and (extra == \"openai\" or extra == \"all\") or python_version <= \"3.13\" and sys_platform != \"emscripten\" and (extra == \"openai\" or extra == \"all\" or extra == \"crewai\") or python_version <= \"3.13\" and (extra == \"crewai\" or extra == \"all\")"} [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -2924,28 +2937,26 @@ voice = ["numpy (>=2.2.0,<3) ; python_version >= \"3.10\"", "websockets (>=15.0, [[package]] name = "openapi-python-client" -version = "0.26.1" +version = "0.29.0" description = "Generate modern Python clients from OpenAPI" optional = false -python-versions = "<4.0,>=3.9" +python-versions = "<4.0,>=3.11" groups = ["dev"] files = [ - {file = "openapi_python_client-0.26.1-py3-none-any.whl", hash = "sha256:415cb8095b1a3f15cec45670c5075c5097f65390a351d21512e8f6ea5c1be644"}, - {file = "openapi_python_client-0.26.1.tar.gz", hash = "sha256:e3832f0ef074a0ab591d1eeb5d3dab2ca820cd0349f7e79d9663b7b21206be5d"}, + {file = "openapi_python_client-0.29.0-py3-none-any.whl", hash = "sha256:1085864c1e0a42fb50e1f5eb84363b19de07ebfb8a3f82a146c8529b948b8f12"}, + {file = "openapi_python_client-0.29.0.tar.gz", hash = "sha256:4ff439a63765aaef548e69c4eedd86dfad57891dc322709450af0c2c9a72a23e"}, ] [package.dependencies] attrs = ">=22.2.0" colorama = {version = ">=0.4.3", markers = "sys_platform == \"win32\""} -httpx = ">=0.23.0,<0.29.0" +httpx = ">=0.23.1,<0.29.0" jinja2 = ">=3.0.0,<4.0.0" pydantic = ">=2.10,<3.0.0" -python-dateutil = ">=2.8.1,<3.0.0" -ruamel-yaml = ">=0.18.6,<0.19.0" -ruff = ">=0.2,<0.14" +ruamel-yaml = ">=0.18.6,<0.20.0" +ruff = ">=0.2" shellingham = ">=1.3.2,<2.0.0" -typer = ">0.6,<0.18" -typing-extensions = ">=4.8.0,<5.0.0" +typer = ">0.16,<0.27" [[package]] name = "openpyxl" @@ -4409,7 +4420,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "dev", "test"] +groups = ["main", "test"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -5029,30 +5040,30 @@ files = [ [[package]] name = "ruff" -version = "0.12.8" +version = "0.15.20" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513"}, - {file = "ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc"}, - {file = "ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec"}, - {file = "ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53"}, - {file = "ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f"}, - {file = "ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f"}, - {file = "ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609"}, - {file = "ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a"}, - {file = "ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb"}, - {file = "ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818"}, - {file = "ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea"}, - {file = "ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3"}, - {file = "ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161"}, - {file = "ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46"}, - {file = "ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3"}, - {file = "ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e"}, - {file = "ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749"}, - {file = "ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033"}, + {file = "ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078"}, + {file = "ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b"}, + {file = "ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632"}, + {file = "ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd"}, + {file = "ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b"}, + {file = "ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267"}, + {file = "ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c"}, + {file = "ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae"}, + {file = "ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b"}, + {file = "ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487"}, + {file = "ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3"}, + {file = "ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053"}, + {file = "ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4"}, + {file = "ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460"}, + {file = "ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21"}, + {file = "ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415"}, + {file = "ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca"}, + {file = "ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566"}, ] [[package]] @@ -5074,7 +5085,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "dev", "test"] +groups = ["main", "test"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -5464,22 +5475,22 @@ telegram = ["requests"] [[package]] name = "typer" -version = "0.16.0" +version = "0.26.8" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"}, - {file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"}, + {file = "typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c"}, + {file = "typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e"}, ] markers = {main = "(extra == \"crewai\" or extra == \"all\") and python_version <= \"3.13\""} [package.dependencies] -click = ">=8.0.0" -rich = ">=10.11.0" +annotated-doc = ">=0.0.2" +colorama = {version = "*", markers = "platform_system == \"Windows\""} +rich = ">=13.8.0" shellingham = ">=1.3.0" -typing-extensions = ">=3.7.4.3" [[package]] name = "types-requests" @@ -6537,4 +6548,4 @@ otel = ["grpcio", "opentelemetry-api", "opentelemetry-exporter-otlp", "opentelem [metadata] lock-version = "2.1" python-versions = "^3.11,<3.15" -content-hash = "bd8c012c6d1fa222ef146ed6956d1ff0c658ad50c544514a26e3234188825864" +content-hash = "f75a182570e1129b6b65927aac9d700eae02d507391e7d2d6850d8910e4d55bf"