From 3be37cb6c62b5a6e5ad68b6ce855bab407a49e1f Mon Sep 17 00:00:00 2001 From: heumsi Date: Tue, 31 Mar 2026 14:46:40 +0900 Subject: [PATCH 01/13] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Scaffold=20mkdocs-?= =?UTF-8?q?shadcn=20documentation=20site?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 + docs/cookbook/clean-architecture.md | 1 + docs/cookbook/gradual-adoption.md | 1 + docs/cookbook/hexagonal-architecture.md | 1 + docs/cookbook/layered-architecture.md | 1 + docs/cookbook/monorepo.md | 1 + docs/cookbook/named-capture-recipes.md | 1 + docs/getting-started/installation.md | 1 + docs/getting-started/pre-commit.md | 1 + docs/getting-started/quick-start.md | 1 + docs/guide/cli.md | 1 + docs/guide/configuration.md | 1 + docs/guide/inline-ignore.md | 1 + docs/guide/patterns.md | 1 + docs/guide/rule-merging.md | 1 + docs/guide/rules.md | 1 + docs/index.md | 3 + docs/reference/config-schema.md | 1 + mkdocs.yml | 33 +++ pyproject.toml | 4 + uv.lock | 317 +++++++++++++++++++++++- 21 files changed, 373 insertions(+), 1 deletion(-) create mode 100644 docs/cookbook/clean-architecture.md create mode 100644 docs/cookbook/gradual-adoption.md create mode 100644 docs/cookbook/hexagonal-architecture.md create mode 100644 docs/cookbook/layered-architecture.md create mode 100644 docs/cookbook/monorepo.md create mode 100644 docs/cookbook/named-capture-recipes.md create mode 100644 docs/getting-started/installation.md create mode 100644 docs/getting-started/pre-commit.md create mode 100644 docs/getting-started/quick-start.md create mode 100644 docs/guide/cli.md create mode 100644 docs/guide/configuration.md create mode 100644 docs/guide/inline-ignore.md create mode 100644 docs/guide/patterns.md create mode 100644 docs/guide/rule-merging.md create mode 100644 docs/guide/rules.md create mode 100644 docs/index.md create mode 100644 docs/reference/config-schema.md create mode 100644 mkdocs.yml diff --git a/.gitignore b/.gitignore index d6add4d..2888ee8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build/ .venv/ .pytest_cache/ .ruff_cache/ +site/ diff --git a/docs/cookbook/clean-architecture.md b/docs/cookbook/clean-architecture.md new file mode 100644 index 0000000..7a1828a --- /dev/null +++ b/docs/cookbook/clean-architecture.md @@ -0,0 +1 @@ +# Clean Architecture diff --git a/docs/cookbook/gradual-adoption.md b/docs/cookbook/gradual-adoption.md new file mode 100644 index 0000000..1ce765d --- /dev/null +++ b/docs/cookbook/gradual-adoption.md @@ -0,0 +1 @@ +# Gradual Adoption diff --git a/docs/cookbook/hexagonal-architecture.md b/docs/cookbook/hexagonal-architecture.md new file mode 100644 index 0000000..4160036 --- /dev/null +++ b/docs/cookbook/hexagonal-architecture.md @@ -0,0 +1 @@ +# Hexagonal Architecture diff --git a/docs/cookbook/layered-architecture.md b/docs/cookbook/layered-architecture.md new file mode 100644 index 0000000..691f2a1 --- /dev/null +++ b/docs/cookbook/layered-architecture.md @@ -0,0 +1 @@ +# Layered Architecture diff --git a/docs/cookbook/monorepo.md b/docs/cookbook/monorepo.md new file mode 100644 index 0000000..1163655 --- /dev/null +++ b/docs/cookbook/monorepo.md @@ -0,0 +1 @@ +# Monorepo diff --git a/docs/cookbook/named-capture-recipes.md b/docs/cookbook/named-capture-recipes.md new file mode 100644 index 0000000..01b1855 --- /dev/null +++ b/docs/cookbook/named-capture-recipes.md @@ -0,0 +1 @@ +# Named Capture Recipes diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 0000000..25267fe --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1 @@ +# Installation diff --git a/docs/getting-started/pre-commit.md b/docs/getting-started/pre-commit.md new file mode 100644 index 0000000..3c2db93 --- /dev/null +++ b/docs/getting-started/pre-commit.md @@ -0,0 +1 @@ +# Pre-commit diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md new file mode 100644 index 0000000..05cf8c1 --- /dev/null +++ b/docs/getting-started/quick-start.md @@ -0,0 +1 @@ +# Quick Start diff --git a/docs/guide/cli.md b/docs/guide/cli.md new file mode 100644 index 0000000..3f213d4 --- /dev/null +++ b/docs/guide/cli.md @@ -0,0 +1 @@ +# CLI diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md new file mode 100644 index 0000000..a025a48 --- /dev/null +++ b/docs/guide/configuration.md @@ -0,0 +1 @@ +# Configuration diff --git a/docs/guide/inline-ignore.md b/docs/guide/inline-ignore.md new file mode 100644 index 0000000..efb37ab --- /dev/null +++ b/docs/guide/inline-ignore.md @@ -0,0 +1 @@ +# Inline Ignore diff --git a/docs/guide/patterns.md b/docs/guide/patterns.md new file mode 100644 index 0000000..3c9daa3 --- /dev/null +++ b/docs/guide/patterns.md @@ -0,0 +1 @@ +# Patterns diff --git a/docs/guide/rule-merging.md b/docs/guide/rule-merging.md new file mode 100644 index 0000000..4638497 --- /dev/null +++ b/docs/guide/rule-merging.md @@ -0,0 +1 @@ +# Rule Merging diff --git a/docs/guide/rules.md b/docs/guide/rules.md new file mode 100644 index 0000000..779ea24 --- /dev/null +++ b/docs/guide/rules.md @@ -0,0 +1 @@ +# Rules diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..26e59d4 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,3 @@ +# python-dependency-linter + +A dependency linter for Python projects. diff --git a/docs/reference/config-schema.md b/docs/reference/config-schema.md new file mode 100644 index 0000000..c54431e --- /dev/null +++ b/docs/reference/config-schema.md @@ -0,0 +1 @@ +# Config Schema diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..c66981e --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,33 @@ +site_name: python-dependency-linter +site_url: https://heumsi.github.io/python-dependency-linter/ +repo_url: https://github.com/heumsi/python-dependency-linter +repo_name: heumsi/python-dependency-linter + +exclude_docs: | + superpowers/ + +theme: + name: shadcn + +nav: + - Home: index.md + - Getting Started: + - Installation: getting-started/installation.md + - Quick Start: getting-started/quick-start.md + - Pre-commit: getting-started/pre-commit.md + - Guide: + - Configuration: guide/configuration.md + - Rules: guide/rules.md + - Patterns: guide/patterns.md + - Rule Merging: guide/rule-merging.md + - Inline Ignore: guide/inline-ignore.md + - CLI: guide/cli.md + - Cookbook: + - Layered Architecture: cookbook/layered-architecture.md + - Hexagonal Architecture: cookbook/hexagonal-architecture.md + - Clean Architecture: cookbook/clean-architecture.md + - Named Capture Recipes: cookbook/named-capture-recipes.md + - Monorepo: cookbook/monorepo.md + - Gradual Adoption: cookbook/gradual-adoption.md + - Reference: + - Config Schema: reference/config-schema.md diff --git a/pyproject.toml b/pyproject.toml index 815d46e..71bfe11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,10 @@ dev = [ "ty>=0.0.26", "pre-commit>=3.0", ] +docs = [ + "mkdocs>=1.6", + "mkdocs-shadcn", +] [tool.hatch.version] source = "vcs" diff --git a/uv.lock b/uv.lock index 7d3af24..aa103ec 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.10" +[[package]] +name = "bottle" +version = "0.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/71/cca6167c06d00c81375fd668719df245864076d284f7cb46a694cbeb5454/bottle-0.13.4.tar.gz", hash = "sha256:787e78327e12b227938de02248333d788cfe45987edca735f8f88e03472c3f47", size = 98717, upload-time = "2025-06-15T10:08:59.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/f6/b55ec74cfe68c6584163faa311503c20b0da4c09883a41e8e00d6726c954/bottle-0.13.4-py2.py3-none-any.whl", hash = "sha256:045684fbd2764eac9cdeb824861d1551d113e8b683d8d26e296898d3dd99a12e", size = 103807, upload-time = "2025-06-15T10:08:57.691Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -62,6 +71,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.46" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, +] + [[package]] name = "identify" version = "2.6.18" @@ -80,6 +125,174 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-shadcn" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bottle" }, + { name = "gitpython" }, + { name = "mkdocs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/c6/d86f2494c1e1bd80d07d4ccd83862e6b4306dd8a39a97e22c0d15e54156c/mkdocs_shadcn-0.10.2.tar.gz", hash = "sha256:cc37a5a2d998dfec2fbfa24c6b67d20c5bd8b53ad36a17e51ef6e1b865be08a3", size = 3161105, upload-time = "2026-03-19T10:12:32.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/73/ce6ebaaec752d90e774997e890b7a7cff6a0642c8dec9afd7ee8e047c9e0/mkdocs_shadcn-0.10.2-py3-none-any.whl", hash = "sha256:f3c4c5f7f4bf80506d6cf834c6a4201d0e447980020ad8fca145bdb46c20ede1", size = 1410494, upload-time = "2026-03-19T10:12:30.421Z" }, +] + [[package]] name = "nodeenv" version = "1.10.0" @@ -98,6 +311,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + [[package]] name = "platformdirs" version = "4.9.4" @@ -141,6 +363,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pymdown-extensions" +version = "10.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, +] + [[package]] name = "pytest" version = "9.0.2" @@ -159,6 +394,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "python-dependency-linter" source = { editable = "." } @@ -175,10 +422,16 @@ dev = [ { name = "ruff" }, { name = "ty" }, ] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-shadcn" }, +] [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.0" }, + { name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.6" }, + { name = "mkdocs-shadcn", marker = "extra == 'docs'" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" }, { name = "pyyaml", specifier = ">=6.0" }, @@ -186,7 +439,7 @@ requires-dist = [ { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0" }, { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.26" }, ] -provides-extras = ["dev"] +provides-extras = ["dev", "docs"] [[package]] name = "python-discovery" @@ -265,6 +518,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + [[package]] name = "ruff" version = "0.15.8" @@ -290,6 +555,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, +] + [[package]] name = "tomli" version = "2.4.1" @@ -392,3 +675,35 @@ sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703 wheels = [ { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, ] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] From 579ba60f77ec02160308f12908bbf6fb41a88d7c Mon Sep 17 00:00:00 2001 From: heumsi Date: Tue, 31 Mar 2026 14:50:49 +0900 Subject: [PATCH 02/13] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Add=20Getting=20St?= =?UTF-8?q?arted=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/installation.md | 20 ++++++++++++ docs/getting-started/pre-commit.md | 19 +++++++++++ docs/getting-started/quick-start.md | 49 ++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 25267fe..780f5a7 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -1 +1,21 @@ # Installation + +## pip + +```bash +pip install python-dependency-linter +``` + +## uv + +```bash +uv add python-dependency-linter +``` + +## Verify + +After installation, verify the CLI is available: + +```bash +pdl --help +``` diff --git a/docs/getting-started/pre-commit.md b/docs/getting-started/pre-commit.md index 3c2db93..31ec819 100644 --- a/docs/getting-started/pre-commit.md +++ b/docs/getting-started/pre-commit.md @@ -1 +1,20 @@ # Pre-commit + +Add to your `.pre-commit-config.yaml`: + +```yaml +- repo: https://github.com/heumsi/python-dependency-linter + rev: '' # Use the tag you want to point at (e.g., v0.5.0) + hooks: + - id: python-dependency-linter +``` + +To pass custom options (e.g., a different config file): + +```yaml +- repo: https://github.com/heumsi/python-dependency-linter + rev: '' + hooks: + - id: python-dependency-linter + args: [--config, custom-config.yaml] +``` diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index 05cf8c1..b35c5e3 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -1 +1,50 @@ # Quick Start + +## 1. Create a config file + +Create `.python-dependency-linter.yaml` in your project root: + +```yaml +rules: + - name: domain-isolation + modules: contexts.*.domain + allow: + standard_library: [dataclasses, typing] + third_party: [pydantic] + local: [contexts.*.domain] + + - name: application-dependency + modules: contexts.*.application + allow: + standard_library: ["*"] + third_party: [pydantic] + local: + - contexts.*.application + - contexts.*.domain +``` + +You can also use `pyproject.toml`. See [Configuration](../guide/configuration.md) for details. + +## 2. Run the linter + +```bash +pdl check +``` + +## 3. Review violations + +``` +contexts/boards/domain/models.py:6 + [domain-isolation] contexts.boards.domain.models → contexts.boards.application.service (local) + +contexts/boards/domain/models.py:9 + [domain-isolation] contexts.boards.domain.models → sqlalchemy (third_party) + +Found 2 violation(s). +``` + +Exit codes: + +- `0` — No violations +- `1` — Violations found +- `2` — Config file not found From 3bc0028dc4a5bd99eb57699abdee867579c685cd Mon Sep 17 00:00:00 2001 From: heumsi Date: Tue, 31 Mar 2026 14:53:29 +0900 Subject: [PATCH 03/13] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Add=20Guide=20page?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/guide/cli.md | 28 ++++++++++++++ docs/guide/configuration.md | 73 +++++++++++++++++++++++++++++++++++++ docs/guide/inline-ignore.md | 18 +++++++++ docs/guide/patterns.md | 59 ++++++++++++++++++++++++++++++ docs/guide/rule-merging.md | 17 +++++++++ docs/guide/rules.md | 56 ++++++++++++++++++++++++++++ 6 files changed, 251 insertions(+) diff --git a/docs/guide/cli.md b/docs/guide/cli.md index 3f213d4..b5a387b 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -1 +1,29 @@ # CLI + +## Usage + +```bash +# Check with auto-discovered config (searches upward from cwd) +pdl check + +# Specify config file (project root = config file's parent directory) +pdl check --config path/to/config.yaml +``` + +## Exit Codes + +| Code | Meaning | +|------|---------| +| `0` | No violations | +| `1` | Violations found | +| `2` | Config file not found | + +## Config Discovery + +If no `--config` is given, the tool searches upward from the current directory for `.python-dependency-linter.yaml` or `pyproject.toml` (with `[tool.python-dependency-linter]`). The config file's parent directory is used as the project root. + +If no config file is found: + +``` +Error: Config file not found. Create .python-dependency-linter.yaml or configure [tool.python-dependency-linter] in pyproject.toml. +``` diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index a025a48..ee14bd9 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -1 +1,74 @@ # Configuration + +python-dependency-linter supports two config formats: YAML and TOML. + +## YAML + +Create `.python-dependency-linter.yaml` in your project root: + +```yaml +rules: + - name: domain-isolation + modules: contexts.*.domain + allow: + standard_library: [dataclasses, typing] + third_party: [pydantic] + local: [contexts.*.domain] +``` + +## TOML (pyproject.toml) + +```toml +[[tool.python-dependency-linter.rules]] +name = "domain-isolation" +modules = "contexts.*.domain" + +[tool.python-dependency-linter.rules.allow] +standard_library = ["dataclasses", "typing"] +third_party = ["pydantic"] +local = ["contexts.*.domain"] +``` + +## Config Discovery + +If no `--config` is given, the tool searches upward from the current directory for: + +1. `.python-dependency-linter.yaml` +2. `pyproject.toml` (with `[tool.python-dependency-linter]` section) + +The config file's parent directory is used as the project root. + +If no config file is found, the tool exits with code `2`: + +``` +Error: Config file not found. Create .python-dependency-linter.yaml or configure [tool.python-dependency-linter] in pyproject.toml. +``` + +## Include / Exclude + +Control which files are scanned: + +```yaml +include: + - src +exclude: + - src/generated/** + +rules: + - name: ... +``` + +- **No `include` or `exclude`** — All `.py` files under the project root are scanned +- **`include` only** — Only files matching the given paths are scanned +- **`exclude` only** — All files except those matching the given paths are scanned +- **Both** — `include` is applied first, then `exclude` filters within that result + +Bare directory names (e.g., `src`) and trailing-slash forms (e.g., `src/`) are treated the same as `src/**`. + +In `pyproject.toml`: + +```toml +[tool.python-dependency-linter] +include = ["src"] +exclude = ["src/generated/**"] +``` diff --git a/docs/guide/inline-ignore.md b/docs/guide/inline-ignore.md index efb37ab..74a7462 100644 --- a/docs/guide/inline-ignore.md +++ b/docs/guide/inline-ignore.md @@ -1 +1,19 @@ # Inline Ignore + +Suppress violations on specific import lines using `# pdl: ignore` comments: + +```python +import boto3 # pdl: ignore +``` + +To suppress only specific rules, specify rule names in brackets: + +```python +import boto3 # pdl: ignore[no-boto-in-domain] +``` + +Multiple rules can be listed with commas: + +```python +import boto3 # pdl: ignore[no-boto-in-domain, other-rule] +``` diff --git a/docs/guide/patterns.md b/docs/guide/patterns.md index 3c9daa3..8f962e8 100644 --- a/docs/guide/patterns.md +++ b/docs/guide/patterns.md @@ -1 +1,60 @@ # Patterns + +## Wildcard + +`*` matches a single level in dotted module paths: + +```yaml +modules: contexts.*.domain # matches contexts.boards.domain, contexts.auth.domain, ... +``` + +`**` matches one or more levels in dotted module paths: + +```yaml +modules: contexts.**.domain # matches contexts.boards.domain, contexts.boards.sub.domain, ... +``` + +## Named Capture + +`{name}` captures a single level (like `*`) and allows back-referencing the captured value in `allow` and `deny`: + +```yaml +rules: + - name: domain-isolation + modules: contexts.{context}.domain + allow: + local: [contexts.{context}.domain, shared.domain] +``` + +When this rule matches `contexts.boards.domain`, `{context}` captures `"boards"`. The `allow` pattern `contexts.{context}.domain` resolves to `contexts.boards.domain`, so only the same context's domain is allowed. + +You can use multiple captures in a single rule: + +```yaml +rules: + - name: bounded-context-layers + modules: contexts.{context}.{layer} + allow: + local: + - contexts.{context}.{layer} + - contexts.{context}.domain + - shared +``` + +Named captures coexist with `*` and `**` wildcards. `{name}` always matches exactly one level. + +## Submodule Matching + +When a pattern is used in `modules`, `allow`, or `deny`, it also matches submodules of the matched module. + +For example, the following rule applies to `contexts.boards.domain` as well as its submodules like `contexts.boards.domain.models` or `contexts.boards.domain.entities.metric`: + +```yaml +rules: + - name: domain-layer + modules: contexts.*.domain + allow: + local: [contexts.*.domain] +``` + +> **Note:** `contexts.*.domain` matches the module itself (`__init__.py`) **and** all submodules beneath it, while `contexts.*.domain.**` matches submodules only. diff --git a/docs/guide/rule-merging.md b/docs/guide/rule-merging.md index 4638497..1931013 100644 --- a/docs/guide/rule-merging.md +++ b/docs/guide/rule-merging.md @@ -1 +1,18 @@ # Rule Merging + +When multiple rules match a module, they are merged. Specific rules override wildcard rules per field: + +```yaml +rules: + - name: base + modules: contexts.*.domain + allow: + third_party: [pydantic] + + - name: boards-extra + modules: contexts.boards.domain + allow: + third_party: [attrs] # merged: [pydantic, attrs] +``` + +In this example, `contexts.boards.domain` matches both rules. The `allow.third_party` lists are merged, so both `pydantic` and `attrs` are allowed. diff --git a/docs/guide/rules.md b/docs/guide/rules.md index 779ea24..91af267 100644 --- a/docs/guide/rules.md +++ b/docs/guide/rules.md @@ -1 +1,57 @@ # Rules + +## Rule Structure + +Each rule has: + +- `name` — Rule identifier, shown in violation output +- `modules` — Module pattern to apply the rule to (supports `*` wildcard) +- `allow` — Whitelist: only listed dependencies are allowed +- `deny` — Blacklist: listed dependencies are denied + +```yaml +rules: + - name: rule-name + modules: my_package.*.domain + allow: + standard_library: [dataclasses] + third_party: [pydantic] + local: [my_package.*.domain] + deny: + third_party: [boto3] +``` + +## Import Categories + +Dependencies are classified into three categories (per PEP 8): + +- `standard_library` — Python built-in modules (`os`, `sys`, `typing`, ...) +- `third_party` — Installed packages (`pydantic`, `sqlalchemy`, ...) +- `local` — Modules in your project + +Both absolute imports (`from contexts.boards.domain import models`) and relative imports (`from ..domain import models`) are analyzed. Relative imports are resolved to absolute module names based on the file's location. + +## Behavior + +- **No rule** — Everything is allowed +- **`allow` only** — Whitelist mode. Only listed dependencies are allowed +- **`deny` only** — Blacklist mode. Listed dependencies are denied, rest allowed +- **`allow` + `deny`** — Allow first, then deny removes exceptions +- If `allow` exists but a category is omitted, that category allows all. For example: + +```yaml +rules: + - name: domain-isolation + modules: contexts.*.domain + allow: + third_party: [pydantic] + local: [contexts.*.domain] + # standard_library is omitted → all standard library imports are allowed +``` + +Use `"*"` to allow all within a category: + +```yaml +allow: + standard_library: ["*"] # allow all standard library imports +``` From 9d1edbfa84336810f8bfd18a757bdcc7c4717bd7 Mon Sep 17 00:00:00 2001 From: heumsi Date: Tue, 31 Mar 2026 14:56:20 +0900 Subject: [PATCH 04/13] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Add=20Cookbook=20p?= =?UTF-8?q?ages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/cookbook/clean-architecture.md | 66 ++++++++++++++++++ docs/cookbook/gradual-adoption.md | 93 +++++++++++++++++++++++++ docs/cookbook/hexagonal-architecture.md | 39 +++++++++++ docs/cookbook/layered-architecture.md | 55 +++++++++++++++ docs/cookbook/monorepo.md | 63 +++++++++++++++++ docs/cookbook/named-capture-recipes.md | 77 ++++++++++++++++++++ 6 files changed, 393 insertions(+) diff --git a/docs/cookbook/clean-architecture.md b/docs/cookbook/clean-architecture.md index 7a1828a..9629a11 100644 --- a/docs/cookbook/clean-architecture.md +++ b/docs/cookbook/clean-architecture.md @@ -1 +1,67 @@ # Clean Architecture + +## Problem + +You follow Clean Architecture with concentric layers: `entities → use_cases → interface_adapters → frameworks`. Inner layers must not depend on outer layers. + +## Configuration + +```yaml +rules: + - name: entities-isolation + modules: my_app.entities + allow: + standard_library: [dataclasses, typing, abc, enum] + third_party: [] + local: [my_app.entities] + + - name: use-cases + modules: my_app.use_cases + allow: + standard_library: ["*"] + third_party: [] + local: + - my_app.use_cases + - my_app.entities + + - name: interface-adapters + modules: my_app.interface_adapters + allow: + standard_library: ["*"] + third_party: [pydantic, sqlalchemy] + local: + - my_app.interface_adapters + - my_app.use_cases + - my_app.entities + + - name: frameworks + modules: my_app.frameworks + allow: + standard_library: ["*"] + third_party: ["*"] + local: + - my_app.frameworks + - my_app.interface_adapters + - my_app.use_cases + - my_app.entities +``` + +## Result + +If `my_app.entities.user` imports `pydantic`: + +``` +my_app/entities/user.py:1 + [entities-isolation] my_app.entities.user → pydantic (third_party) + +Found 1 violation(s). +``` + +If `my_app.use_cases.create_user` imports from `my_app.interface_adapters`: + +``` +my_app/use_cases/create_user.py:3 + [use-cases] my_app.use_cases.create_user → my_app.interface_adapters.repo (local) + +Found 1 violation(s). +``` diff --git a/docs/cookbook/gradual-adoption.md b/docs/cookbook/gradual-adoption.md index 1ce765d..37c07b7 100644 --- a/docs/cookbook/gradual-adoption.md +++ b/docs/cookbook/gradual-adoption.md @@ -1 +1,94 @@ # Gradual Adoption + +## Problem + +You want to introduce dependency linting to an existing project without fixing all violations at once. + +## Strategy 1: Start with deny rules + +Instead of a strict allowlist, start by denying only the most problematic dependencies: + +```yaml +rules: + - name: no-orm-in-domain + modules: my_app.domain + deny: + third_party: [sqlalchemy, django] +``` + +This catches new violations without flagging existing ones that don't match the deny list. + +## Strategy 2: Scope with include + +Lint only new or well-structured parts of your codebase: + +```yaml +include: + - my_app/new_module + +rules: + - name: new-module-rules + modules: my_app.new_module + allow: + standard_library: ["*"] + third_party: [pydantic] + local: [my_app.new_module, my_app.shared] +``` + +Expand the `include` list as you clean up more modules. + +## Strategy 3: Use inline ignore for known violations + +Add `# pdl: ignore` to existing violations you plan to fix later, so the linter passes in CI: + +```python +import sqlalchemy # pdl: ignore[no-orm-in-domain] +``` + +Then remove the ignore comments as you refactor. + +## Strategy 4: One rule at a time + +Start with the most important boundary (usually domain isolation) and add rules incrementally: + +```yaml +# Week 1: Just domain isolation +rules: + - name: domain-isolation + modules: my_app.domain + allow: + standard_library: ["*"] + third_party: [] + local: [my_app.domain] +``` + +```yaml +# Week 2: Add application layer +rules: + - name: domain-isolation + modules: my_app.domain + allow: + standard_library: ["*"] + third_party: [] + local: [my_app.domain] + + - name: application-layer + modules: my_app.application + allow: + standard_library: ["*"] + third_party: [pydantic] + local: [my_app.application, my_app.domain] +``` + +## Result + +With Strategy 1, only the specific denied imports are flagged: + +``` +my_app/domain/repo.py:1 + [no-orm-in-domain] my_app.domain.repo → sqlalchemy (third_party) + +Found 1 violation(s). +``` + +Other third-party imports in domain are still allowed until you switch to a stricter allowlist. diff --git a/docs/cookbook/hexagonal-architecture.md b/docs/cookbook/hexagonal-architecture.md index 4160036..82311b0 100644 --- a/docs/cookbook/hexagonal-architecture.md +++ b/docs/cookbook/hexagonal-architecture.md @@ -1 +1,40 @@ # Hexagonal Architecture + +## Problem + +You want to isolate domain from infrastructure. Ports (interfaces) live in domain, adapters depend on domain but not vice versa. Each bounded context should only depend on its own domain. + +## Configuration + +Using [named captures](../guide/patterns.md#named-capture), you can enforce that each bounded context only depends on its own domain: + +```yaml +rules: + - name: domain-no-infra + modules: contexts.{context}.domain + allow: + standard_library: [dataclasses, typing, abc] + third_party: [] + local: [contexts.{context}.domain, shared.domain] + + - name: adapters-depend-on-domain + modules: contexts.{context}.adapters + allow: + standard_library: ["*"] + third_party: ["*"] + local: + - contexts.{context}.adapters + - contexts.{context}.domain + - shared +``` + +## Result + +With `{context}`, `contexts.boards.domain` can only import from `contexts.boards.domain` and `shared.domain` — not from `contexts.auth.domain`: + +``` +contexts/boards/domain/service.py:2 + [domain-no-infra] contexts.boards.domain.service → contexts.auth.domain.models (local) + +Found 1 violation(s). +``` diff --git a/docs/cookbook/layered-architecture.md b/docs/cookbook/layered-architecture.md index 691f2a1..779380f 100644 --- a/docs/cookbook/layered-architecture.md +++ b/docs/cookbook/layered-architecture.md @@ -1 +1,56 @@ # Layered Architecture + +## Problem + +You have a layered architecture (`presentation → application → domain`) and want to enforce that dependencies only flow downward. The `domain` layer should have no outward dependencies. + +## Configuration + +```yaml +rules: + - name: domain-isolation + modules: my_app.domain + allow: + standard_library: ["*"] + third_party: [] + local: [my_app.domain] + + - name: application-layer + modules: my_app.application + allow: + standard_library: ["*"] + third_party: [pydantic] + local: + - my_app.application + - my_app.domain + + - name: presentation-layer + modules: my_app.presentation + allow: + standard_library: ["*"] + third_party: [fastapi, pydantic] + local: + - my_app.presentation + - my_app.application + - my_app.domain +``` + +## Result + +If `my_app.domain.models` imports `sqlalchemy`: + +``` +my_app/domain/models.py:3 + [domain-isolation] my_app.domain.models → sqlalchemy (third_party) + +Found 1 violation(s). +``` + +If `my_app.application.service` imports `fastapi`: + +``` +my_app/application/service.py:1 + [application-layer] my_app.application.service → fastapi (third_party) + +Found 1 violation(s). +``` diff --git a/docs/cookbook/monorepo.md b/docs/cookbook/monorepo.md index 1163655..55ab376 100644 --- a/docs/cookbook/monorepo.md +++ b/docs/cookbook/monorepo.md @@ -1 +1,64 @@ # Monorepo + +## Problem + +Your project has multiple packages in a monorepo and you want to lint each package's dependencies separately, or enforce boundaries between packages. + +## Configuration + +### Per-package config + +Each package has its own `.python-dependency-linter.yaml`: + +``` +monorepo/ +├── packages/ +│ ├── auth/ +│ │ ├── .python-dependency-linter.yaml +│ │ └── auth/ +│ ├── billing/ +│ │ ├── .python-dependency-linter.yaml +│ │ └── billing/ +``` + +Run per package: + +```bash +cd packages/auth && pdl check +cd packages/billing && pdl check +``` + +### Shared config with include + +Use a single config at the repo root with `include` to scope each rule: + +```yaml +include: + - packages + +rules: + - name: auth-isolation + modules: auth + allow: + standard_library: ["*"] + third_party: ["*"] + local: [auth, shared] + + - name: billing-isolation + modules: billing + allow: + standard_library: ["*"] + third_party: ["*"] + local: [billing, shared] +``` + +## Result + +If `billing.service` imports `auth.models`: + +``` +packages/billing/billing/service.py:1 + [billing-isolation] billing.service → auth.models (local) + +Found 1 violation(s). +``` diff --git a/docs/cookbook/named-capture-recipes.md b/docs/cookbook/named-capture-recipes.md index 01b1855..ef5a9d9 100644 --- a/docs/cookbook/named-capture-recipes.md +++ b/docs/cookbook/named-capture-recipes.md @@ -1 +1,78 @@ # Named Capture Recipes + +## Problem + +You have multiple bounded contexts or feature modules and want to enforce that each module only depends on its own internals, without writing a separate rule for each module. + +## Recipe 1: Isolate bounded contexts + +Each context can only import from its own domain: + +```yaml +rules: + - name: context-boundary + modules: contexts.{context}.{layer} + allow: + standard_library: ["*"] + third_party: ["*"] + local: + - contexts.{context} + - shared +``` + +This prevents `contexts.orders` from importing `contexts.users` internals. Cross-context communication must go through `shared`. + +## Recipe 2: Layer enforcement within each context + +Combine context isolation with layer direction: + +```yaml +rules: + - name: domain-isolation + modules: contexts.{context}.domain + allow: + standard_library: ["*"] + third_party: [] + local: + - contexts.{context}.domain + - shared.domain + + - name: application-layer + modules: contexts.{context}.application + allow: + standard_library: ["*"] + third_party: ["*"] + local: + - contexts.{context}.application + - contexts.{context}.domain + - shared +``` + +## Recipe 3: Feature-scoped modules + +For a flat feature structure like `features.{feature}.api`, `features.{feature}.service`, `features.{feature}.repo`: + +```yaml +rules: + - name: feature-boundary + modules: features.{feature} + allow: + standard_library: ["*"] + third_party: ["*"] + local: + - features.{feature} + - core +``` + +Each feature is isolated — `features.billing` cannot import from `features.auth`. + +## Result + +With Recipe 1, if `contexts.orders.service` imports `contexts.users.models`: + +``` +contexts/orders/service.py:2 + [context-boundary] contexts.orders.service → contexts.users.models (local) + +Found 1 violation(s). +``` From 747f64147aa0b52709d08f8af0c2bbdcc917cc4c Mon Sep 17 00:00:00 2001 From: heumsi Date: Tue, 31 Mar 2026 14:58:20 +0900 Subject: [PATCH 05/13] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Add=20Reference=20?= =?UTF-8?q?page=20and=20update=20landing=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/index.md | 36 ++++++++++++++++++++++++++++++++- docs/reference/config-schema.md | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 26e59d4..524261f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,37 @@ # python-dependency-linter -A dependency linter for Python projects. +A dependency linter for Python projects. Define rules for which modules can depend on what, and catch violations. + +## What It Does + +- Define dependency rules between modules using a simple YAML or TOML config +- Detect imports that violate your rules with a single CLI command +- Integrate into CI or pre-commit to keep your architecture consistent + +For Python developers who care about module boundaries and dependency direction — whether you're applying Layered, Hexagonal, Clean Architecture, or your own conventions. + +## Quick Example + +```yaml +rules: + - name: domain-isolation + modules: contexts.*.domain + allow: + standard_library: [dataclasses, typing] + third_party: [pydantic] + local: [contexts.*.domain] +``` + +```bash +$ pdl check +contexts/boards/domain/models.py:6 + [domain-isolation] contexts.boards.domain.models → contexts.boards.application.service (local) + +Found 1 violation(s). +``` + +## Next Steps + +- [Installation](getting-started/installation.md) — Install the package +- [Quick Start](getting-started/quick-start.md) — Set up your first config and run the linter +- [Cookbook](cookbook/layered-architecture.md) — See examples for common architecture patterns diff --git a/docs/reference/config-schema.md b/docs/reference/config-schema.md index c54431e..fee833b 100644 --- a/docs/reference/config-schema.md +++ b/docs/reference/config-schema.md @@ -1 +1,36 @@ # Config Schema + +Complete reference for all configuration fields. + +## Top-level Fields + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `rules` | list of [Rule](#rule) | Yes | — | List of dependency rules | +| `include` | list of str | No | `null` (scan all) | File paths to include in scanning | +| `exclude` | list of str | No | `null` (no exclusions) | File paths to exclude from scanning | + +## Rule + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `name` | str | Yes | — | Rule identifier, shown in violation output. Must match `[a-zA-Z0-9_-]+` | +| `modules` | str | Yes | — | Module pattern to apply the rule to. Supports `*`, `**`, and `{name}` captures | +| `allow` | [DependencyFilter](#dependencyfilter) | No | `null` | Whitelist of allowed dependencies | +| `deny` | [DependencyFilter](#dependencyfilter) | No | `null` | Blacklist of denied dependencies | + +## DependencyFilter + +Used in both `allow` and `deny` fields. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `standard_library` | list of str | `null` (allow all if in `allow`, deny none if in `deny`) | Standard library module patterns | +| `third_party` | list of str | `null` (allow all if in `allow`, deny none if in `deny`) | Third-party package patterns | +| `local` | list of str | `null` (allow all if in `allow`, deny none if in `deny`) | Local module patterns | + +Each list entry can be: +- An exact module name: `pydantic` +- A wildcard pattern: `contexts.*.domain` +- A named capture reference: `contexts.{context}.domain` +- `"*"` to match all modules in the category From 8d5962e57806f6d4c769d3144671e9b7f826766c Mon Sep 17 00:00:00 2001 From: heumsi Date: Tue, 31 Mar 2026 14:59:25 +0900 Subject: [PATCH 06/13] =?UTF-8?q?=F0=9F=91=B7=20ci:=20Add=20GitHub=20Actio?= =?UTF-8?q?ns=20docs=20deployment=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docs.yaml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/docs.yaml diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000..a8b8afa --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,23 @@ +name: Deploy Docs + +on: + push: + branches: [main] + paths: + - "docs/**" + - "mkdocs.yml" + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v4 + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + - run: uv pip install --system mkdocs mkdocs-shadcn + - run: mkdocs gh-deploy --force From b83df9db2186ef528898ca91fea76a881d7211ff Mon Sep 17 00:00:00 2001 From: heumsi Date: Tue, 31 Mar 2026 14:59:58 +0900 Subject: [PATCH 07/13] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Slim=20down=20READ?= =?UTF-8?q?ME,=20link=20to=20docs=20site?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 349 +----------------------------------------------------- 1 file changed, 2 insertions(+), 347 deletions(-) diff --git a/README.md b/README.md index 1bdfccb..90d1c41 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,6 @@ A dependency linter for Python projects. Define rules for which modules can depend on what, and catch violations. -## What It Does - -- Define dependency rules between modules using a simple YAML or TOML config -- Detect imports that violate your rules with a single CLI command -- Integrate into CI or pre-commit to keep your architecture consistent - -For Python developers who care about module boundaries and dependency direction — whether you're applying Layered, Hexagonal, Clean Architecture, or your own conventions. - ## Installation ```bash @@ -34,15 +26,6 @@ rules: standard_library: [dataclasses, typing] third_party: [pydantic] local: [contexts.*.domain] - - - name: application-dependency - modules: contexts.*.application - allow: - standard_library: ["*"] - third_party: [pydantic] - local: - - contexts.*.application - - contexts.*.domain ``` Run: @@ -51,337 +34,9 @@ Run: pdl check ``` -Output: - -``` -contexts/boards/domain/models.py:6 - [domain-isolation] contexts.boards.domain.models → contexts.boards.application.service (local) - -contexts/boards/domain/models.py:9 - [domain-isolation] contexts.boards.domain.models → sqlalchemy (third_party) - -Found 2 violation(s). -``` - -## Examples - -### Layered Architecture - -Enforce dependency direction: `presentation → application → domain`, where `domain` has no outward dependencies. - -```yaml -rules: - - name: domain-isolation - modules: my_app.domain - allow: - standard_library: ["*"] - third_party: [] - local: [my_app.domain] - - - name: application-layer - modules: my_app.application - allow: - standard_library: ["*"] - third_party: [pydantic] - local: - - my_app.application - - my_app.domain - - - name: presentation-layer - modules: my_app.presentation - allow: - standard_library: ["*"] - third_party: [fastapi, pydantic] - local: - - my_app.presentation - - my_app.application - - my_app.domain -``` - -### Hexagonal Architecture - -Isolate domain from infrastructure. Ports (interfaces) live in domain, adapters depend on domain but not vice versa. - -Using named captures (`{context}`), you can enforce that each bounded context only depends on its own domain — not other contexts' domains: - -```yaml -rules: - - name: domain-no-infra - modules: contexts.{context}.domain - allow: - standard_library: [dataclasses, typing, abc] - third_party: [] - local: [contexts.{context}.domain, shared.domain] - - - name: adapters-depend-on-domain - modules: contexts.{context}.adapters - allow: - standard_library: ["*"] - third_party: ["*"] - local: - - contexts.{context}.adapters - - contexts.{context}.domain - - shared -``` - -With `{context}`, `contexts.boards.domain` can only import from `contexts.boards.domain` and `shared.domain` — not from `contexts.auth.domain`. See [Named Capture](#named-capture) for details. - -## Configuration - -### Include / Exclude - -Control which files are scanned using `include` and `exclude`: - -```yaml -include: - - src -exclude: - - src/generated/** - -rules: - - name: ... -``` - -- **No `include` or `exclude`** — All `.py` files under the project root are scanned -- **`include` only** — Only files matching the given paths are scanned -- **`exclude` only** — All files except those matching the given paths are scanned -- **Both** — `include` is applied first, then `exclude` filters within that result - -Bare directory names (e.g., `src`) and trailing-slash forms (e.g., `src/`) are treated the same as `src/**`. - -In `pyproject.toml`: - -```toml -[tool.python-dependency-linter] -include = ["src"] -exclude = ["src/generated/**"] -``` - -### Rule Structure - -Each rule has: - -- `name` — Rule identifier, shown in violation output -- `modules` — Module pattern to apply the rule to (supports `*` wildcard) -- `allow` — Whitelist: only listed dependencies are allowed -- `deny` — Blacklist: listed dependencies are denied - -```yaml -rules: - - name: rule-name - modules: my_package.*.domain - allow: - standard_library: [dataclasses] - third_party: [pydantic] - local: [my_package.*.domain] - deny: - third_party: [boto3] -``` - -### Import Categories - -Dependencies are classified into three categories (per PEP 8): - -- `standard_library` — Python built-in modules (`os`, `sys`, `typing`, ...) -- `third_party` — Installed packages (`pydantic`, `sqlalchemy`, ...) -- `local` — Modules in your project - -Both absolute imports (`from contexts.boards.domain import models`) and relative imports (`from ..domain import models`) are analyzed. Relative imports are resolved to absolute module names based on the file's location. - -### Behavior - -- **No rule** — Everything is allowed -- **`allow` only** — Whitelist mode. Only listed dependencies are allowed -- **`deny` only** — Blacklist mode. Listed dependencies are denied, rest allowed -- **`allow` + `deny`** — Allow first, then deny removes exceptions -- If `allow` exists but a category is omitted, that category allows all. For example: - -```yaml -rules: - - name: domain-isolation - modules: contexts.*.domain - allow: - third_party: [pydantic] - local: [contexts.*.domain] - # standard_library is omitted → all standard library imports are allowed -``` - -Use `"*"` to allow all within a category: - -```yaml -allow: - standard_library: ["*"] # allow all standard library imports -``` - -### Wildcard - -`*` matches a single level in dotted module paths: - -```yaml -modules: contexts.*.domain # matches contexts.boards.domain, contexts.auth.domain, ... -``` +## Documentation -`**` matches one or more levels in dotted module paths: - -```yaml -modules: contexts.**.domain # matches contexts.boards.domain, contexts.boards.sub.domain, ... -``` - -### Named Capture - -`{name}` captures a single level (like `*`) and allows back-referencing the captured value in `allow` and `deny`: - -```yaml -rules: - - name: domain-isolation - modules: contexts.{context}.domain - allow: - local: [contexts.{context}.domain, shared.domain] -``` - -When this rule matches `contexts.boards.domain`, `{context}` captures `"boards"`. The `allow` pattern `contexts.{context}.domain` resolves to `contexts.boards.domain`, so only the same context's domain is allowed. - -You can use multiple captures in a single rule: - -```yaml -rules: - - name: bounded-context-layers - modules: contexts.{context}.{layer} - allow: - local: - - contexts.{context}.{layer} - - contexts.{context}.domain - - shared -``` - -Named captures coexist with `*` and `**` wildcards. `{name}` always matches exactly one level. - -### Submodule Matching - -When a pattern is used in `modules`, `allow`, or `deny`, it also matches submodules of the matched module. - -For example, the following rule applies to `contexts.boards.domain` as well as its submodules like `contexts.boards.domain.models` or `contexts.boards.domain.entities.metric`: - -```yaml -rules: - - name: domain-layer - modules: contexts.*.domain - allow: - local: [contexts.*.domain] -``` - -> **Note:** `contexts.*.domain` matches the module itself (`__init__.py`) **and** all submodules beneath it, while `contexts.*.domain.**` matches submodules only. - -### Rule Merging - -When multiple rules match a module, they are merged. Specific rules override wildcard rules per field: - -```yaml -rules: - - name: base - modules: contexts.*.domain - allow: - third_party: [pydantic] - - - name: boards-extra - modules: contexts.boards.domain - allow: - third_party: [attrs] # merged: [pydantic, attrs] -``` - -### pyproject.toml - -You can also configure in `pyproject.toml`: - -```toml -[[tool.python-dependency-linter.rules]] -name = "domain-isolation" -modules = "contexts.*.domain" - -[tool.python-dependency-linter.rules.allow] -standard_library = ["dataclasses", "typing"] -third_party = ["pydantic"] -local = ["contexts.*.domain"] - -[[tool.python-dependency-linter.rules]] -name = "application-dependency" -modules = "contexts.*.application" - -[tool.python-dependency-linter.rules.allow] -standard_library = ["*"] -third_party = ["pydantic"] -local = ["contexts.*.application", "contexts.*.domain"] - -[[tool.python-dependency-linter.rules]] -name = "no-boto-in-domain" -modules = "contexts.*.domain" - -[tool.python-dependency-linter.rules.deny] -third_party = ["boto3"] -``` - -### Inline Ignore - -Suppress violations on specific import lines using `# pdl: ignore` comments: - -```python -import boto3 # pdl: ignore -``` - -To suppress only specific rules, specify rule names in brackets: - -```python -import boto3 # pdl: ignore[no-boto-in-domain] -``` - -Multiple rules can be listed with commas: - -```python -import boto3 # pdl: ignore[no-boto-in-domain, other-rule] -``` - -## CLI - -```bash -# Check with auto-discovered config (searches upward from cwd) -pdl check - -# Specify config file (project root = config file's parent directory) -pdl check --config path/to/config.yaml -``` - -Exit codes: - -- `0` — No violations -- `1` — Violations found -- `2` — Config file not found - -If no `--config` is given, the tool searches upward from the current directory for `.python-dependency-linter.yaml` or `pyproject.toml` (with `[tool.python-dependency-linter]`). The config file's parent directory is used as the project root. If no config file is found, the tool prints an error and exits with code `2`: - -``` -Error: Config file not found. Create .python-dependency-linter.yaml or configure [tool.python-dependency-linter] in pyproject.toml. -``` - -## Pre-commit - -Add to `.pre-commit-config.yaml`: - -```yaml -- repo: https://github.com/heumsi/python-dependency-linter - rev: '' # Use the tag you want to point at (e.g., v0.5.0) - hooks: - - id: python-dependency-linter -``` - -To pass custom options (e.g., a different config file): - -```yaml -- repo: https://github.com/heumsi/python-dependency-linter - rev: '' # Use the tag you want to point at (e.g., v0.5.0) - hooks: - - id: python-dependency-linter - args: [--config, custom-config.yaml] -``` +For full documentation, visit [heumsi.github.io/python-dependency-linter](https://heumsi.github.io/python-dependency-linter/). ## License From fca8e6c4f4975b69ba760ae7faf49c8f5f4b8c1e Mon Sep 17 00:00:00 2001 From: heumsi Date: Tue, 31 Mar 2026 17:14:43 +0900 Subject: [PATCH 08/13] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Restructure=20docs?= =?UTF-8?q?=20to=20match=20python-naming-linter=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move configuration to getting-started, CLI and pre-commit to reference, add cookbook overview, contributing, and changelog pages. Update mkdocs.yml with theme options and markdown extensions. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/changelog.md | 121 ++++++++++++++++++ docs/{guide => }/cli.md | 0 docs/contributing.md | 84 ++++++++++++ docs/cookbook/index.md | 14 ++ .../configuration.md | 0 docs/getting-started/quick-start.md | 2 +- docs/{getting-started => }/pre-commit.md | 0 mkdocs.yml | 23 +++- 8 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 docs/changelog.md rename docs/{guide => }/cli.md (100%) create mode 100644 docs/contributing.md create mode 100644 docs/cookbook/index.md rename docs/{guide => getting-started}/configuration.md (100%) rename docs/{getting-started => }/pre-commit.md (100%) diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..f6d0d63 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,121 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.5.1] - 2026-03-30 + +### Bug Fixes + +- Match submodules in modules pattern consistent with allow/deny (#19) + +### Documentation + +- Replace hardcoded pre-commit rev with placeholder +- Update submodule matching section to include modules pattern + +## [0.5.0] - 2026-03-30 + +### Features + +- Add named capture in module patterns for back-referencing in allow/deny (#17) + +## [0.4.0] - 2026-03-30 + +### Bug Fixes + +- Use fnmatch for include/exclude pattern matching (#15) + +### Features + +- Remove --project-root, auto-detect from config file location (#14) + +## [0.3.0] - 2026-03-30 + +### Bug Fixes + +- Use exit code 2 for config file not found (#11) + +### Documentation + +- Add CONTRIBUTING.md and CLAUDE.md +- Add PR title convention to template and CONTRIBUTING.md +- Add release process to CONTRIBUTING.md and /release skill + +### Features + +- Resolve relative imports to absolute module names (#10) +- Add include/exclude file filtering options (#12) + +### Miscellaneous + +- Add /commit skill for Claude Code +- Add uv.lock for reproducible builds + +## [0.2.0] - 2026-03-30 + +### Documentation + +- Remove design spec and implementation plan docs +- Add GitHub issue and PR templates +- Add example for omitted category behavior in allow rules +- Add architecture examples to README +- Add pre-commit hook custom options example +- Expand pyproject.toml examples with multiple rules and deny +- Add "What It Does" section to README + +### Features + +- Support `**` glob pattern for matching nested submodules (#6) +- Add undocumented features to README + +### Miscellaneous + +- Use hatch-vcs for dynamic versioning from git tags +- Add gitmoji preprocessor to git-cliff config +- Move git-cliff config from cliff.toml to pyproject.toml +- Skip CHANGELOG update commits in git-cliff output + +### Testing + +- Limit CI to source and test file changes + +### Build + +- Bump actions/checkout from 4 to 6 (#3) +- Bump actions/setup-python from 5 to 6 (#2) + +## [0.1.0] - 2026-03-30 + +### Bug Fixes + +- Add isort config and fix plan typo +- Add tomli fallback for Python 3.10 compatibility +- Add import content to fixture files + +### CI/CD + +- Add GitHub Actions for CI, PyPI publish, and Dependabot + +### Documentation + +- Add design spec for python-dependency-linter +- Add implementation plan +- Add README +- Add MIT LICENSE + +### Features + +- Add config loading for YAML and pyproject.toml +- Add AST-based import parser +- Add import resolver for classification +- Add wildcard matcher and rule merging +- Add dependency checker with allow/deny logic +- Add violation reporter +- Add CLI with check command +- Add pre-commit hook definition + +### Miscellaneous + +- Initialize project scaffolding +- Add license, readme, and classifiers to pyproject.toml +- Add .gitignore diff --git a/docs/guide/cli.md b/docs/cli.md similarity index 100% rename from docs/guide/cli.md rename to docs/cli.md diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..cf2d59b --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,84 @@ +# Contributing + +## Commit Convention + +Commit messages must follow [Conventional Commits](https://www.conventionalcommits.org/) with [gitmoji](https://gitmoji.dev/) prefix. + +### Format + +``` + : +``` + +- The first letter after the colon must be **capitalized**. +- The description must be in **English**. + +### Types + +| Gitmoji | Type | Description | +|---------|------------|--------------------------| +| ✨ | `feat` | New feature | +| 🐛 | `fix` | Bug fix | +| ♻️ | `refactor` | Code refactoring | +| 📝 | `docs` | Documentation | +| ✅ | `test` | Adding or updating tests | +| 🔧 | `chore` | Maintenance tasks | +| 👷 | `ci` | CI/CD changes | +| ⚡ | `perf` | Performance improvement | + +### Examples + +``` +✨ feat: Add support for relative imports +🐛 fix: Use exit code 2 for config file not found +♻️ refactor: Simplify module resolver logic +``` + +## Pull Request Convention + +- PRs are always **squash merged**, so the PR title becomes the final commit message. +- PR titles must follow the same format as commit messages (` : `). +- PR descriptions must be written in **English**. + +## Pre-commit Hooks + +This project uses [pre-commit](https://pre-commit.com/) for linting, formatting, and type checking. + +```bash +# Install pre-commit hooks +pre-commit install + +# Run manually +pre-commit run --all-files +``` + +All commits must pass the pre-commit hooks before being accepted. + +## Release + +Releases are automated via GitHub Actions. You only need to create and push a version tag. + +### Steps + +1. Calculate the next version based on conventional commits: + ```bash + uvx git-cliff --bumped-version + ``` +2. Review the commits since the last tag: + ```bash + git log $(git describe --tags --abbrev=0)..HEAD --oneline + ``` +3. Push the latest commits to `main`: + ```bash + git push origin main + ``` +4. Create and push the tag: + ```bash + git tag + git push origin + ``` + +The GitHub Actions workflow will then automatically: +- Generate `CHANGELOG.md` and commit it to `main` +- Create a GitHub Release with release notes +- Publish the package to PyPI diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md new file mode 100644 index 0000000..c91c418 --- /dev/null +++ b/docs/cookbook/index.md @@ -0,0 +1,14 @@ +# Cookbook + +The cookbook provides ready-to-use recipes for common dependency rule scenarios. Each recipe shows the problem, a complete configuration, and the expected result so you can adapt it to your project immediately. + +## Recipes + +| Recipe | Description | +|--------|-------------| +| [Layered Architecture](./layered-architecture.md) | Enforce dependency direction in a layered architecture | +| [Hexagonal Architecture](./hexagonal-architecture.md) | Isolate domain from infrastructure with bounded contexts | +| [Clean Architecture](./clean-architecture.md) | Enforce concentric layer dependencies | +| [Named Capture Recipes](./named-capture-recipes.md) | Reusable patterns with `{name}` captures | +| [Monorepo](./monorepo.md) | Lint multiple packages in a monorepo | +| [Gradual Adoption](./gradual-adoption.md) | Introduce dependency linting incrementally | diff --git a/docs/guide/configuration.md b/docs/getting-started/configuration.md similarity index 100% rename from docs/guide/configuration.md rename to docs/getting-started/configuration.md diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index b35c5e3..99ef42c 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -23,7 +23,7 @@ rules: - contexts.*.domain ``` -You can also use `pyproject.toml`. See [Configuration](../guide/configuration.md) for details. +You can also use `pyproject.toml`. See [Configuration](./configuration.md) for details. ## 2. Run the linter diff --git a/docs/getting-started/pre-commit.md b/docs/pre-commit.md similarity index 100% rename from docs/getting-started/pre-commit.md rename to docs/pre-commit.md diff --git a/mkdocs.yml b/mkdocs.yml index c66981e..c3a64c0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,21 +8,34 @@ exclude_docs: | theme: name: shadcn + show_title: true + show_stargazers: true + pygments_style: + light: shadcn-light + dark: github-dark + +markdown_extensions: + - admonition + - codehilite + - fenced_code + - footnotes + - attr_list + - toc: + permalink: true nav: - Home: index.md - Getting Started: - Installation: getting-started/installation.md - Quick Start: getting-started/quick-start.md - - Pre-commit: getting-started/pre-commit.md + - Configuration: getting-started/configuration.md - Guide: - - Configuration: guide/configuration.md - Rules: guide/rules.md - Patterns: guide/patterns.md - Rule Merging: guide/rule-merging.md - Inline Ignore: guide/inline-ignore.md - - CLI: guide/cli.md - Cookbook: + - Overview: cookbook/index.md - Layered Architecture: cookbook/layered-architecture.md - Hexagonal Architecture: cookbook/hexagonal-architecture.md - Clean Architecture: cookbook/clean-architecture.md @@ -31,3 +44,7 @@ nav: - Gradual Adoption: cookbook/gradual-adoption.md - Reference: - Config Schema: reference/config-schema.md + - CLI: cli.md + - Pre-commit: pre-commit.md + - Contributing: contributing.md + - Changelog: changelog.md From 52b0c2e43bcd1884f7227ec58b8b945e0ce8d3ee Mon Sep 17 00:00:00 2001 From: heumsi Date: Tue, 31 Mar 2026 17:19:47 +0900 Subject: [PATCH 09/13] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Merge=20Guide=20pa?= =?UTF-8?q?ges=20into=20single=20rules.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidate patterns, rule-merging, and inline-ignore into guide/rules.md for better cohesion. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/cookbook/hexagonal-architecture.md | 2 +- docs/guide/inline-ignore.md | 19 ----- docs/guide/patterns.md | 60 -------------- docs/guide/rule-merging.md | 18 ----- docs/guide/rules.md | 100 ++++++++++++++++++++++++ mkdocs.yml | 3 - 6 files changed, 101 insertions(+), 101 deletions(-) delete mode 100644 docs/guide/inline-ignore.md delete mode 100644 docs/guide/patterns.md delete mode 100644 docs/guide/rule-merging.md diff --git a/docs/cookbook/hexagonal-architecture.md b/docs/cookbook/hexagonal-architecture.md index 82311b0..4620a28 100644 --- a/docs/cookbook/hexagonal-architecture.md +++ b/docs/cookbook/hexagonal-architecture.md @@ -6,7 +6,7 @@ You want to isolate domain from infrastructure. Ports (interfaces) live in domai ## Configuration -Using [named captures](../guide/patterns.md#named-capture), you can enforce that each bounded context only depends on its own domain: +Using [named captures](../guide/rules.md#named-capture), you can enforce that each bounded context only depends on its own domain: ```yaml rules: diff --git a/docs/guide/inline-ignore.md b/docs/guide/inline-ignore.md deleted file mode 100644 index 74a7462..0000000 --- a/docs/guide/inline-ignore.md +++ /dev/null @@ -1,19 +0,0 @@ -# Inline Ignore - -Suppress violations on specific import lines using `# pdl: ignore` comments: - -```python -import boto3 # pdl: ignore -``` - -To suppress only specific rules, specify rule names in brackets: - -```python -import boto3 # pdl: ignore[no-boto-in-domain] -``` - -Multiple rules can be listed with commas: - -```python -import boto3 # pdl: ignore[no-boto-in-domain, other-rule] -``` diff --git a/docs/guide/patterns.md b/docs/guide/patterns.md deleted file mode 100644 index 8f962e8..0000000 --- a/docs/guide/patterns.md +++ /dev/null @@ -1,60 +0,0 @@ -# Patterns - -## Wildcard - -`*` matches a single level in dotted module paths: - -```yaml -modules: contexts.*.domain # matches contexts.boards.domain, contexts.auth.domain, ... -``` - -`**` matches one or more levels in dotted module paths: - -```yaml -modules: contexts.**.domain # matches contexts.boards.domain, contexts.boards.sub.domain, ... -``` - -## Named Capture - -`{name}` captures a single level (like `*`) and allows back-referencing the captured value in `allow` and `deny`: - -```yaml -rules: - - name: domain-isolation - modules: contexts.{context}.domain - allow: - local: [contexts.{context}.domain, shared.domain] -``` - -When this rule matches `contexts.boards.domain`, `{context}` captures `"boards"`. The `allow` pattern `contexts.{context}.domain` resolves to `contexts.boards.domain`, so only the same context's domain is allowed. - -You can use multiple captures in a single rule: - -```yaml -rules: - - name: bounded-context-layers - modules: contexts.{context}.{layer} - allow: - local: - - contexts.{context}.{layer} - - contexts.{context}.domain - - shared -``` - -Named captures coexist with `*` and `**` wildcards. `{name}` always matches exactly one level. - -## Submodule Matching - -When a pattern is used in `modules`, `allow`, or `deny`, it also matches submodules of the matched module. - -For example, the following rule applies to `contexts.boards.domain` as well as its submodules like `contexts.boards.domain.models` or `contexts.boards.domain.entities.metric`: - -```yaml -rules: - - name: domain-layer - modules: contexts.*.domain - allow: - local: [contexts.*.domain] -``` - -> **Note:** `contexts.*.domain` matches the module itself (`__init__.py`) **and** all submodules beneath it, while `contexts.*.domain.**` matches submodules only. diff --git a/docs/guide/rule-merging.md b/docs/guide/rule-merging.md deleted file mode 100644 index 1931013..0000000 --- a/docs/guide/rule-merging.md +++ /dev/null @@ -1,18 +0,0 @@ -# Rule Merging - -When multiple rules match a module, they are merged. Specific rules override wildcard rules per field: - -```yaml -rules: - - name: base - modules: contexts.*.domain - allow: - third_party: [pydantic] - - - name: boards-extra - modules: contexts.boards.domain - allow: - third_party: [attrs] # merged: [pydantic, attrs] -``` - -In this example, `contexts.boards.domain` matches both rules. The `allow.third_party` lists are merged, so both `pydantic` and `attrs` are allowed. diff --git a/docs/guide/rules.md b/docs/guide/rules.md index 91af267..42467b0 100644 --- a/docs/guide/rules.md +++ b/docs/guide/rules.md @@ -55,3 +55,103 @@ Use `"*"` to allow all within a category: allow: standard_library: ["*"] # allow all standard library imports ``` + +## Patterns + +### Wildcard + +`*` matches a single level in dotted module paths: + +```yaml +modules: contexts.*.domain # matches contexts.boards.domain, contexts.auth.domain, ... +``` + +`**` matches one or more levels in dotted module paths: + +```yaml +modules: contexts.**.domain # matches contexts.boards.domain, contexts.boards.sub.domain, ... +``` + +### Named Capture + +`{name}` captures a single level (like `*`) and allows back-referencing the captured value in `allow` and `deny`: + +```yaml +rules: + - name: domain-isolation + modules: contexts.{context}.domain + allow: + local: [contexts.{context}.domain, shared.domain] +``` + +When this rule matches `contexts.boards.domain`, `{context}` captures `"boards"`. The `allow` pattern `contexts.{context}.domain` resolves to `contexts.boards.domain`, so only the same context's domain is allowed. + +You can use multiple captures in a single rule: + +```yaml +rules: + - name: bounded-context-layers + modules: contexts.{context}.{layer} + allow: + local: + - contexts.{context}.{layer} + - contexts.{context}.domain + - shared +``` + +Named captures coexist with `*` and `**` wildcards. `{name}` always matches exactly one level. + +### Submodule Matching + +When a pattern is used in `modules`, `allow`, or `deny`, it also matches submodules of the matched module. + +For example, the following rule applies to `contexts.boards.domain` as well as its submodules like `contexts.boards.domain.models` or `contexts.boards.domain.entities.metric`: + +```yaml +rules: + - name: domain-layer + modules: contexts.*.domain + allow: + local: [contexts.*.domain] +``` + +> **Note:** `contexts.*.domain` matches the module itself (`__init__.py`) **and** all submodules beneath it, while `contexts.*.domain.**` matches submodules only. + +## Rule Merging + +When multiple rules match a module, they are merged. Specific rules override wildcard rules per field: + +```yaml +rules: + - name: base + modules: contexts.*.domain + allow: + third_party: [pydantic] + + - name: boards-extra + modules: contexts.boards.domain + allow: + third_party: [attrs] # merged: [pydantic, attrs] +``` + +In this example, `contexts.boards.domain` matches both rules. The `allow.third_party` lists are merged, so both `pydantic` and `attrs` are allowed. + +## Inline Ignore + +Suppress violations on specific import lines using `# pdl: ignore` comments: + +```python +import boto3 # pdl: ignore +``` + +To suppress only specific rules, specify rule names in brackets: + +```python +import boto3 # pdl: ignore[no-boto-in-domain] +``` + +Multiple rules can be listed with commas: + +```python +import boto3 # pdl: ignore[no-boto-in-domain, other-rule] +``` diff --git a/mkdocs.yml b/mkdocs.yml index c3a64c0..ec723ee 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -31,9 +31,6 @@ nav: - Configuration: getting-started/configuration.md - Guide: - Rules: guide/rules.md - - Patterns: guide/patterns.md - - Rule Merging: guide/rule-merging.md - - Inline Ignore: guide/inline-ignore.md - Cookbook: - Overview: cookbook/index.md - Layered Architecture: cookbook/layered-architecture.md From 0cd26fb3940a1167396c295b148390b67a6739e7 Mon Sep 17 00:00:00 2001 From: heumsi Date: Tue, 31 Mar 2026 17:25:29 +0900 Subject: [PATCH 10/13] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Align=20page=20sty?= =?UTF-8?q?les=20with=20python-naming-linter=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unify page structure, headings, and tone across all docs pages to be consistent with the python-naming-linter documentation. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/cli.md | 33 ++++++---- docs/getting-started/configuration.md | 57 ++++++++++------- docs/getting-started/installation.md | 8 ++- docs/getting-started/quick-start.md | 30 ++++++--- docs/index.md | 92 +++++++++++++++++++++++++-- docs/pre-commit.md | 10 ++- 6 files changed, 176 insertions(+), 54 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index b5a387b..ffe7475 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,6 +1,8 @@ # CLI -## Usage +## `pdl check` + +Run the linter against your project: ```bash # Check with auto-discovered config (searches upward from cwd) @@ -10,20 +12,25 @@ pdl check pdl check --config path/to/config.yaml ``` -## Exit Codes +### Options -| Code | Meaning | -|------|---------| -| `0` | No violations | -| `1` | Violations found | -| `2` | Config file not found | +| Option | Description | +|--------|-------------| +| `--config` | Path to a config file. The config file's parent directory is used as the project root. | -## Config Discovery +### Config Auto-Discovery -If no `--config` is given, the tool searches upward from the current directory for `.python-dependency-linter.yaml` or `pyproject.toml` (with `[tool.python-dependency-linter]`). The config file's parent directory is used as the project root. +If `--config` is not provided, `pdl check` searches upward from the current working directory for either: -If no config file is found: +- `.python-dependency-linter.yaml` +- `pyproject.toml` (with a `[tool.python-dependency-linter]` section) -``` -Error: Config file not found. Create .python-dependency-linter.yaml or configure [tool.python-dependency-linter] in pyproject.toml. -``` +The first matching file found is used, and its parent directory becomes the project root. + +## Exit Codes + +| Code | Meaning | +|------|---------| +| `0` | No violations found | +| `1` | One or more violations found | +| `2` | Config file not found | diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index ee14bd9..e4e308c 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -1,8 +1,23 @@ # Configuration -python-dependency-linter supports two config formats: YAML and TOML. +`pdl` supports two config file formats: a standalone YAML file or an inline section inside `pyproject.toml`. -## YAML +## Config File Discovery + +When you run `pdl check` without `--config`, the tool searches **upward from the current working directory** for one of: + +- `.python-dependency-linter.yaml` +- `pyproject.toml` (containing a `[tool.python-dependency-linter]` section) + +The first matching file is used, and its parent directory becomes the project root. + +To use a specific config file, pass it explicitly: + +```bash +pdl check --config path/to/config.yaml +``` + +## YAML Format Create `.python-dependency-linter.yaml` in your project root: @@ -16,7 +31,9 @@ rules: local: [contexts.*.domain] ``` -## TOML (pyproject.toml) +## pyproject.toml Format + +You can embed the same configuration inside `pyproject.toml` using the `[tool.python-dependency-linter]` namespace: ```toml [[tool.python-dependency-linter.rules]] @@ -29,22 +46,17 @@ third_party = ["pydantic"] local = ["contexts.*.domain"] ``` -## Config Discovery +Both formats are equivalent — use whichever fits your project's conventions. -If no `--config` is given, the tool searches upward from the current directory for: +## Top-Level Keys -1. `.python-dependency-linter.yaml` -2. `pyproject.toml` (with `[tool.python-dependency-linter]` section) +| Key | Description | +|-----|-------------| +| `rules` | List of dependency rule definitions | +| `include` | Paths to include when scanning (optional) | +| `exclude` | Paths to exclude when scanning (optional) | -The config file's parent directory is used as the project root. - -If no config file is found, the tool exits with code `2`: - -``` -Error: Config file not found. Create .python-dependency-linter.yaml or configure [tool.python-dependency-linter] in pyproject.toml. -``` - -## Include / Exclude +### include / exclude Control which files are scanned: @@ -53,15 +65,14 @@ include: - src exclude: - src/generated/** - -rules: - - name: ... ``` -- **No `include` or `exclude`** — All `.py` files under the project root are scanned -- **`include` only** — Only files matching the given paths are scanned -- **`exclude` only** — All files except those matching the given paths are scanned -- **Both** — `include` is applied first, then `exclude` filters within that result +Behavior: + +- **Neither** — all `.py` files under the project root are scanned. +- **`include` only** — only files matching the given paths are scanned. +- **`exclude` only** — all files except those matching the given paths are scanned. +- **Both** — `include` is applied first, then `exclude` filters within that result. Bare directory names (e.g., `src`) and trailing-slash forms (e.g., `src/`) are treated the same as `src/**`. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 780f5a7..1295916 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -1,5 +1,7 @@ # Installation +`python-dependency-linter` is available on PyPI and can be installed with any standard Python package manager. + ## pip ```bash @@ -12,10 +14,12 @@ pip install python-dependency-linter uv add python-dependency-linter ``` -## Verify +## Verify the Installation -After installation, verify the CLI is available: +After installation, confirm the CLI is available: ```bash pdl --help ``` + +You should see the help output listing the available commands. diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index 99ef42c..067fbad 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -1,8 +1,10 @@ # Quick Start -## 1. Create a config file +Get `pdl` running in your project in three steps. -Create `.python-dependency-linter.yaml` in your project root: +## Step 1: Create a Config File + +Create `.python-dependency-linter.yaml` in your project root and define your dependency rules: ```yaml rules: @@ -23,15 +25,26 @@ rules: - contexts.*.domain ``` +This config defines two rules: + +- `domain-isolation` — modules under `contexts.*.domain` can only import `dataclasses`, `typing`, `pydantic`, and other domain modules. +- `application-dependency` — modules under `contexts.*.application` can import any standard library, `pydantic`, and application or domain modules. + You can also use `pyproject.toml`. See [Configuration](./configuration.md) for details. -## 2. Run the linter +## Step 2: Run the Linter + +From your project root, run: ```bash pdl check ``` -## 3. Review violations +`pdl` automatically discovers the config file by searching upward from the current working directory. + +## Step 3: Review the Output + +Violations are reported with the file path, line number, rule name, and the dependency direction: ``` contexts/boards/domain/models.py:6 @@ -43,8 +56,9 @@ contexts/boards/domain/models.py:9 Found 2 violation(s). ``` -Exit codes: +Fix the reported imports and re-run `pdl check` until no violations remain. + +## Next Steps -- `0` — No violations -- `1` — Violations found -- `2` — Config file not found +- Learn all available config options in [Configuration](./configuration.md). +- See rule details and pattern options in the [Guide](../guide/rules.md). diff --git a/docs/index.md b/docs/index.md index 524261f..15cf9b7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,7 +10,27 @@ A dependency linter for Python projects. Define rules for which modules can depe For Python developers who care about module boundaries and dependency direction — whether you're applying Layered, Hexagonal, Clean Architecture, or your own conventions. -## Quick Example +## Key Features + +| Feature | Description | +|---------|-------------| +| **Allow / Deny Rules** | Whitelist or blacklist dependencies per module | +| **Import Categories** | Separate rules for standard library, third-party, and local imports | +| **Wildcard Patterns** | Target modules with `*` and `**` glob-style patterns | +| **Named Captures** | Use `{name}` to enforce same-context boundaries without repeating rules | +| **Rule Merging** | Combine base rules with specific overrides | +| **Inline Ignore** | Suppress violations on specific lines with `# pdl: ignore` | +| **Pre-commit** | Drop-in integration with pre-commit hooks | + +## Quick Start + +**Install:** + +```bash +pip install python-dependency-linter +``` + +**Create `.python-dependency-linter.yaml` in your project root:** ```yaml rules: @@ -20,18 +40,78 @@ rules: standard_library: [dataclasses, typing] third_party: [pydantic] local: [contexts.*.domain] + + - name: application-dependency + modules: contexts.*.application + allow: + standard_library: ["*"] + third_party: [pydantic] + local: + - contexts.*.application + - contexts.*.domain ``` +**Run:** + ```bash -$ pdl check +pdl check +``` + +**Output:** + +``` contexts/boards/domain/models.py:6 [domain-isolation] contexts.boards.domain.models → contexts.boards.application.service (local) -Found 1 violation(s). +contexts/boards/domain/models.py:9 + [domain-isolation] contexts.boards.domain.models → sqlalchemy (third_party) + +Found 2 violation(s). +``` + +## More Examples + +### Hexagonal Architecture — Bounded Context Isolation + +Using named captures to enforce that each context only depends on its own domain: + +```yaml +rules: + - name: domain-no-infra + modules: contexts.{context}.domain + allow: + standard_library: [dataclasses, typing, abc] + third_party: [] + local: [contexts.{context}.domain, shared.domain] + + - name: adapters-depend-on-domain + modules: contexts.{context}.adapters + allow: + standard_library: ["*"] + third_party: ["*"] + local: + - contexts.{context}.adapters + - contexts.{context}.domain + - shared +``` + +This prevents `contexts.boards.domain` from importing `contexts.auth.domain`. + +### Deny Rules — Block Specific Dependencies + +Use deny rules to block specific dependencies without a full allowlist: + +```yaml +rules: + - name: no-orm-in-domain + modules: my_app.domain + deny: + third_party: [sqlalchemy, django] ``` ## Next Steps -- [Installation](getting-started/installation.md) — Install the package -- [Quick Start](getting-started/quick-start.md) — Set up your first config and run the linter -- [Cookbook](cookbook/layered-architecture.md) — See examples for common architecture patterns +- [Installation](getting-started/installation.md) — detailed install instructions +- [Quick Start](getting-started/quick-start.md) — step-by-step setup guide +- [Configuration](getting-started/configuration.md) — full configuration reference +- [Cookbook](cookbook/index.md) — real-world usage patterns diff --git a/docs/pre-commit.md b/docs/pre-commit.md index 31ec819..0ebcc82 100644 --- a/docs/pre-commit.md +++ b/docs/pre-commit.md @@ -1,6 +1,10 @@ # Pre-commit -Add to your `.pre-commit-config.yaml`: +`pdl` can be used as a [pre-commit](https://pre-commit.com/) hook to enforce dependency rules before every commit. + +## Setup + +Add the following to your `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/heumsi/python-dependency-linter @@ -9,7 +13,9 @@ Add to your `.pre-commit-config.yaml`: - id: python-dependency-linter ``` -To pass custom options (e.g., a different config file): +## Custom Options + +To pass custom options (e.g., a specific config file path), use `args`: ```yaml - repo: https://github.com/heumsi/python-dependency-linter From a3f1789bba3fcb876e7a848e831cc2a4ade3a97e Mon Sep 17 00:00:00 2001 From: heumsi Date: Tue, 31 Mar 2026 17:30:46 +0900 Subject: [PATCH 11/13] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Enhance=20Rules=20?= =?UTF-8?q?page=20with=20examples=20and=20result=20tables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Fields table, behavior mode examples with Pass/Violation tables, pattern matching tables, practical inline ignore examples, and summary section to match python-naming-linter documentation style. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/guide/rules.md | 262 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 235 insertions(+), 27 deletions(-) diff --git a/docs/guide/rules.md b/docs/guide/rules.md index 42467b0..dde4e91 100644 --- a/docs/guide/rules.md +++ b/docs/guide/rules.md @@ -1,13 +1,49 @@ # Rules -## Rule Structure +Rules are the core building blocks of `pdl`. Each rule targets a set of modules and enforces which dependencies are allowed or denied. -Each rule has: +## Structure -- `name` — Rule identifier, shown in violation output -- `modules` — Module pattern to apply the rule to (supports `*` wildcard) -- `allow` — Whitelist: only listed dependencies are allowed -- `deny` — Blacklist: listed dependencies are denied +Every rule has two required fields and two optional ones: + +```yaml +rules: + - name: my-rule # Unique identifier for this rule + modules: my_app.domain # Which modules this rule applies to + allow: { ... } # (optional) Whitelist of allowed dependencies + deny: { ... } # (optional) Blacklist of denied dependencies +``` + +The `name` is used in violation output and in `# pdl: ignore` comments. + +### Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Unique identifier. Must match `[a-zA-Z0-9_-]+`. Shown in violation output and referenced in `# pdl: ignore` comments | +| `modules` | Yes | Module pattern to apply the rule to. Supports `*`, `**`, and `{name}` captures (see [Patterns](#patterns)) | +| `allow` | No | Whitelist of allowed dependencies (see [Allow / Deny](#allow-deny)) | +| `deny` | No | Blacklist of denied dependencies (see [Allow / Deny](#allow-deny)) | + +--- + +## Import Categories + +Dependencies are classified into three categories (per PEP 8): + +| Category | What it covers | Examples | +|----------|---------------|----------| +| `standard_library` | Python built-in modules | `os`, `sys`, `typing`, `dataclasses` | +| `third_party` | Installed packages | `pydantic`, `sqlalchemy`, `fastapi` | +| `local` | Modules in your project | `my_app.domain`, `contexts.boards` | + +Both absolute imports (`from contexts.boards.domain import models`) and relative imports (`from ..domain import models`) are analyzed. Relative imports are resolved to absolute module names based on the file's location. + +--- + +## Allow / Deny + +Each category can be configured independently in the `allow` and `deny` blocks: ```yaml rules: @@ -21,23 +57,56 @@ rules: third_party: [boto3] ``` -## Import Categories +### Behavior Modes -Dependencies are classified into three categories (per PEP 8): +| Mode | When | Effect | +|------|------|--------| +| No rule | Module has no matching rule | Everything is allowed | +| `allow` only | Only `allow` is set | Whitelist mode — only listed dependencies are allowed | +| `deny` only | Only `deny` is set | Blacklist mode — listed dependencies are denied, rest allowed | +| `allow` + `deny` | Both are set | Allow first, then deny removes exceptions | -- `standard_library` — Python built-in modules (`os`, `sys`, `typing`, ...) -- `third_party` — Installed packages (`pydantic`, `sqlalchemy`, ...) -- `local` — Modules in your project +**Example — allow only (whitelist mode):** -Both absolute imports (`from contexts.boards.domain import models`) and relative imports (`from ..domain import models`) are analyzed. Relative imports are resolved to absolute module names based on the file's location. +```yaml +rules: + - name: domain-isolation + modules: my_app.domain + allow: + standard_library: [dataclasses, typing] + third_party: [pydantic] + local: [my_app.domain] +``` -## Behavior +| Import in `my_app.domain` | Result | +|---------------------------|--------| +| `import dataclasses` | Pass — in allow list | +| `import typing` | Pass — in allow list | +| `import pydantic` | Pass — in allow list | +| `from my_app.domain import models` | Pass — in allow list | +| `import sqlalchemy` | **Violation** — not in allow list | +| `from my_app.application import service` | **Violation** — not in allow list | -- **No rule** — Everything is allowed -- **`allow` only** — Whitelist mode. Only listed dependencies are allowed -- **`deny` only** — Blacklist mode. Listed dependencies are denied, rest allowed -- **`allow` + `deny`** — Allow first, then deny removes exceptions -- If `allow` exists but a category is omitted, that category allows all. For example: +**Example — deny only (blacklist mode):** + +```yaml +rules: + - name: no-orm-in-domain + modules: my_app.domain + deny: + third_party: [sqlalchemy, django] +``` + +| Import in `my_app.domain` | Result | +|---------------------------|--------| +| `import pydantic` | Pass — not in deny list | +| `import os` | Pass — not in deny list | +| `import sqlalchemy` | **Violation** — in deny list | +| `import django` | **Violation** — in deny list | + +### Omitted Categories + +If `allow` exists but a category is omitted, that category allows all: ```yaml rules: @@ -49,29 +118,64 @@ rules: # standard_library is omitted → all standard library imports are allowed ``` +| Import | Result | +|--------|--------| +| `import os` | Pass — `standard_library` omitted, so all allowed | +| `import typing` | Pass — `standard_library` omitted, so all allowed | +| `import pydantic` | Pass — in `third_party` allow list | +| `import sqlalchemy` | **Violation** — not in `third_party` allow list | + +### Wildcard Allow + Use `"*"` to allow all within a category: ```yaml allow: - standard_library: ["*"] # allow all standard library imports + standard_library: ["*"] + third_party: [pydantic] + local: [my_app.domain] ``` +| Import | Result | +|--------|--------| +| `import os` | Pass — `"*"` allows all standard library | +| `import collections` | Pass — `"*"` allows all standard library | +| `import pydantic` | Pass — in allow list | +| `import sqlalchemy` | **Violation** — not in `third_party` allow list | + +--- + ## Patterns ### Wildcard -`*` matches a single level in dotted module paths: +`*` matches exactly one level in dotted module paths: ```yaml -modules: contexts.*.domain # matches contexts.boards.domain, contexts.auth.domain, ... +modules: contexts.*.domain ``` +| Module | Match? | +|--------|--------| +| `contexts.boards.domain` | Yes — `*` matches `boards` | +| `contexts.auth.domain` | Yes — `*` matches `auth` | +| `contexts.domain` | No — nothing to match `*` | +| `contexts.boards.sub.domain` | No — `*` matches only one level | + `**` matches one or more levels in dotted module paths: ```yaml -modules: contexts.**.domain # matches contexts.boards.domain, contexts.boards.sub.domain, ... +modules: contexts.**.domain ``` +| Module | Match? | +|--------|--------| +| `contexts.boards.domain` | Yes — `**` matches `boards` | +| `contexts.boards.sub.domain` | Yes — `**` matches `boards.sub` | +| `contexts.domain` | No — `**` requires at least one level | + +--- + ### Named Capture `{name}` captures a single level (like `*`) and allows back-referencing the captured value in `allow` and `deny`: @@ -86,6 +190,12 @@ rules: When this rule matches `contexts.boards.domain`, `{context}` captures `"boards"`. The `allow` pattern `contexts.{context}.domain` resolves to `contexts.boards.domain`, so only the same context's domain is allowed. +| Import in `contexts.boards.domain` | Result | +|------------------------------------|--------| +| `from contexts.boards.domain import models` | Pass — same context's domain | +| `from shared.domain import base` | Pass — in allow list | +| `from contexts.auth.domain import user` | **Violation** — different context's domain | + You can use multiple captures in a single rule: ```yaml @@ -101,6 +211,8 @@ rules: Named captures coexist with `*` and `**` wildcards. `{name}` always matches exactly one level. +--- + ### Submodule Matching When a pattern is used in `modules`, `allow`, or `deny`, it also matches submodules of the matched module. @@ -115,8 +227,17 @@ rules: local: [contexts.*.domain] ``` +| Module | Matched by `contexts.*.domain`? | +|--------|-------------------------------| +| `contexts.boards.domain` | Yes — exact match | +| `contexts.boards.domain.models` | Yes — submodule of match | +| `contexts.boards.domain.entities.metric` | Yes — nested submodule of match | +| `contexts.boards.application` | No — different path | + > **Note:** `contexts.*.domain` matches the module itself (`__init__.py`) **and** all submodules beneath it, while `contexts.*.domain.**` matches submodules only. +--- + ## Rule Merging When multiple rules match a module, they are merged. Specific rules override wildcard rules per field: @@ -131,27 +252,114 @@ rules: - name: boards-extra modules: contexts.boards.domain allow: - third_party: [attrs] # merged: [pydantic, attrs] + third_party: [attrs] ``` -In this example, `contexts.boards.domain` matches both rules. The `allow.third_party` lists are merged, so both `pydantic` and `attrs` are allowed. +In this example, `contexts.boards.domain` matches both rules. The `allow.third_party` lists are merged: + +| Import in `contexts.boards.domain` | Result | +|------------------------------------|--------| +| `import pydantic` | Pass — from `base` rule | +| `import attrs` | Pass — from `boards-extra` rule | +| `import sqlalchemy` | **Violation** — not in either rule | + +| Import in `contexts.auth.domain` | Result | +|----------------------------------|--------| +| `import pydantic` | Pass — from `base` rule | +| `import attrs` | **Violation** — `boards-extra` doesn't match `auth` | + +--- ## Inline Ignore -Suppress violations on specific import lines using `# pdl: ignore` comments: +Suppress violations on specific import lines using `# pdl: ignore` comments. + +### Ignore All Rules on a Line + +Add `# pdl: ignore` at the end of a line to suppress all `pdl` violations reported for that line: ```python import boto3 # pdl: ignore ``` -To suppress only specific rules, specify rule names in brackets: +Any rule that would have flagged the import on this line is silenced. + +### Ignore a Specific Rule on a Line + +To suppress only one rule, specify the rule name in brackets: ```python import boto3 # pdl: ignore[no-boto-in-domain] ``` -Multiple rules can be listed with commas: +Only the `no-boto-in-domain` rule is suppressed on this line. Any other rules that match this line will still report violations. + +### Ignore Multiple Specific Rules on a Line + +To suppress more than one rule on the same line, list rule names separated by commas: ```python import boto3 # pdl: ignore[no-boto-in-domain, other-rule] ``` + +### Practical Examples + +**Suppressing a legacy import that you plan to refactor later:** + +```python +from contexts.auth.domain import user # pdl: ignore[domain-isolation] +``` + +**Suppressing a necessary cross-context dependency:** + +```python +from contexts.shared.utils import helper # pdl: ignore +``` + +### Summary + +| Topic | Detail | +|---|---| +| Scope | Comments apply only to the line they appear on; other lines are unaffected. | +| Case sensitivity | Rule names are case-sensitive and must match exactly. | +| Unknown rule names | If a rule name does not exist in your config, the comment is silently ignored — no error is raised. | +| Prefer targeted suppression | Use `# pdl: ignore[rule-name]` over `# pdl: ignore` so that future rules are not accidentally silenced. | + +--- + +## Summary + +### Rule fields + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Unique identifier, shown in output and referenced in `# pdl: ignore` | +| `modules` | Yes | Module pattern to apply the rule to | +| `allow` | No | Whitelist of allowed dependencies | +| `deny` | No | Blacklist of denied dependencies | + +### Import categories + +| Category | What it covers | +|----------|---------------| +| `standard_library` | Python built-in modules | +| `third_party` | Installed packages | +| `local` | Modules in your project | + +### Behavior modes + +| Mode | Effect | +|------|--------| +| No rule | Everything is allowed | +| `allow` only | Whitelist — only listed dependencies allowed | +| `deny` only | Blacklist — listed dependencies denied, rest allowed | +| `allow` + `deny` | Allow first, then deny removes exceptions | + +### Pattern types + +| Pattern | Matches | Example | +|---------|---------|---------| +| `*` | Exactly one level | `contexts.*.domain` matches `contexts.boards.domain` | +| `**` | One or more levels | `contexts.**.domain` matches `contexts.boards.sub.domain` | +| `{name}` | One level with back-reference | `contexts.{ctx}.domain` captures and reuses in allow/deny | +| `"*"` (in allow/deny) | All modules in a category | `standard_library: ["*"]` allows all stdlib imports | From 0bd194d8917b138e5cbf24425e9339dec8eba292 Mon Sep 17 00:00:00 2001 From: heumsi Date: Tue, 31 Mar 2026 17:32:12 +0900 Subject: [PATCH 12/13] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Rename=20Problem?= =?UTF-8?q?=20to=20Purpose=20in=20cookbook,=20update=20site=5Fname?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/cookbook/clean-architecture.md | 2 +- docs/cookbook/gradual-adoption.md | 2 +- docs/cookbook/hexagonal-architecture.md | 2 +- docs/cookbook/layered-architecture.md | 2 +- docs/cookbook/monorepo.md | 2 +- docs/cookbook/named-capture-recipes.md | 2 +- mkdocs.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/cookbook/clean-architecture.md b/docs/cookbook/clean-architecture.md index 9629a11..ce4263f 100644 --- a/docs/cookbook/clean-architecture.md +++ b/docs/cookbook/clean-architecture.md @@ -1,6 +1,6 @@ # Clean Architecture -## Problem +## Purpose You follow Clean Architecture with concentric layers: `entities → use_cases → interface_adapters → frameworks`. Inner layers must not depend on outer layers. diff --git a/docs/cookbook/gradual-adoption.md b/docs/cookbook/gradual-adoption.md index 37c07b7..55c7d92 100644 --- a/docs/cookbook/gradual-adoption.md +++ b/docs/cookbook/gradual-adoption.md @@ -1,6 +1,6 @@ # Gradual Adoption -## Problem +## Purpose You want to introduce dependency linting to an existing project without fixing all violations at once. diff --git a/docs/cookbook/hexagonal-architecture.md b/docs/cookbook/hexagonal-architecture.md index 4620a28..201f832 100644 --- a/docs/cookbook/hexagonal-architecture.md +++ b/docs/cookbook/hexagonal-architecture.md @@ -1,6 +1,6 @@ # Hexagonal Architecture -## Problem +## Purpose You want to isolate domain from infrastructure. Ports (interfaces) live in domain, adapters depend on domain but not vice versa. Each bounded context should only depend on its own domain. diff --git a/docs/cookbook/layered-architecture.md b/docs/cookbook/layered-architecture.md index 779380f..eb9dedc 100644 --- a/docs/cookbook/layered-architecture.md +++ b/docs/cookbook/layered-architecture.md @@ -1,6 +1,6 @@ # Layered Architecture -## Problem +## Purpose You have a layered architecture (`presentation → application → domain`) and want to enforce that dependencies only flow downward. The `domain` layer should have no outward dependencies. diff --git a/docs/cookbook/monorepo.md b/docs/cookbook/monorepo.md index 55ab376..7827d38 100644 --- a/docs/cookbook/monorepo.md +++ b/docs/cookbook/monorepo.md @@ -1,6 +1,6 @@ # Monorepo -## Problem +## Purpose Your project has multiple packages in a monorepo and you want to lint each package's dependencies separately, or enforce boundaries between packages. diff --git a/docs/cookbook/named-capture-recipes.md b/docs/cookbook/named-capture-recipes.md index ef5a9d9..64513cd 100644 --- a/docs/cookbook/named-capture-recipes.md +++ b/docs/cookbook/named-capture-recipes.md @@ -1,6 +1,6 @@ # Named Capture Recipes -## Problem +## Purpose You have multiple bounded contexts or feature modules and want to enforce that each module only depends on its own internals, without writing a separate rule for each module. diff --git a/mkdocs.yml b/mkdocs.yml index ec723ee..3dd7ece 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -site_name: python-dependency-linter +site_name: Python Dependency Linter site_url: https://heumsi.github.io/python-dependency-linter/ repo_url: https://github.com/heumsi/python-dependency-linter repo_name: heumsi/python-dependency-linter From c6ad50e42cfd2916a98a5b9efcd1b073fe217e9c Mon Sep 17 00:00:00 2001 From: heumsi Date: Tue, 31 Mar 2026 17:35:06 +0900 Subject: [PATCH 13/13] =?UTF-8?q?=F0=9F=91=B7=20ci:=20Move=20docs=20deploy?= =?UTF-8?q?ment=20to=20publish=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove separate docs.yaml workflow. Add docs job to publish.yaml using GitHub Pages official deployment (upload-pages-artifact + deploy-pages) to match python-naming-linter. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/docs.yaml | 23 ----------------------- .github/workflows/publish.yaml | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 23 deletions(-) delete mode 100644 .github/workflows/docs.yaml diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml deleted file mode 100644 index a8b8afa..0000000 --- a/.github/workflows/docs.yaml +++ /dev/null @@ -1,23 +0,0 @@ -name: Deploy Docs - -on: - push: - branches: [main] - paths: - - "docs/**" - - "mkdocs.yml" - -permissions: - contents: write - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: astral-sh/setup-uv@v4 - - uses: actions/setup-python@v6 - with: - python-version: "3.13" - - run: uv pip install --system mkdocs mkdocs-shadcn - - run: mkdocs gh-deploy --force diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 86430d5..cec7c9f 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -57,3 +57,27 @@ jobs: - run: pip install build - run: python -m build - uses: pypa/gh-action-pypi-publish@release/v1 + + docs: + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: astral-sh/setup-uv@v4 + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + - run: uv pip install --system mkdocs mkdocs-shadcn + - run: mkdocs build --strict + - uses: actions/upload-pages-artifact@v4 + with: + path: site/ + - id: deployment + uses: actions/deploy-pages@v4