├── tests ├── __init__.py ├── examples │ ├── empty.toml │ ├── sorted │ │ ├── empty.toml │ │ ├── gradle-version-catalog.toml │ │ ├── dotted-key.toml │ │ ├── weird.toml │ │ ├── pyproject-weird-order.toml │ │ ├── comment-no-comments.toml │ │ ├── inline-no-comments-no-table-sort.toml │ │ ├── inline.toml │ │ ├── inline-default.toml │ │ ├── comment-comments-preserved.toml │ │ ├── comment-header-footer.toml │ │ ├── from-toml-lang.toml │ │ ├── from-toml-lang-first.toml │ │ └── from-toml-lang-overrides.toml │ ├── single-comment.toml │ ├── gradle-version-catalog.toml │ ├── dotted-key.toml │ ├── pyproject-weird-order.toml │ ├── weird.toml │ ├── inline.toml │ ├── from-toml-lang.toml │ └── comment.toml ├── conftest.py ├── test_toml_sort.py └── test_cli.py ├── toml_sort ├── py.typed ├── __init__.py ├── cli.py └── tomlsort.py ├── instance └── .gitignore ├── .pre-commit-hooks.yaml ├── example.toml ├── .github └── workflows │ ├── release.yaml │ └── testing.yaml ├── noxfile.py ├── LICENSE ├── pyproject.toml ├── Makefile ├── .gitignore ├── CHANGELOG.md ├── README.md └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /toml_sort/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/examples/empty.toml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/examples/sorted/empty.toml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /instance/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/examples/single-comment.toml: -------------------------------------------------------------------------------- 1 | # This is a single comment 2 | -------------------------------------------------------------------------------- /toml_sort/__init__.py: -------------------------------------------------------------------------------- 1 | """Toml Sort. 2 | 3 | A library to easily sort toml files. 4 | """ 5 | 6 | from .tomlsort import TomlSort 7 | 8 | __all__ = ["TomlSort"] 9 | -------------------------------------------------------------------------------- /tests/examples/gradle-version-catalog.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | fooVersion = "1.0.0" 3 | barVersion = "2.0.0" 4 | 5 | [libraries] 6 | fooLib = { module = "com.example:foo", version.ref = "fooVersion" } 7 | barLib = { module = "com.example:bar", version.ref = "barVersion" } 8 | -------------------------------------------------------------------------------- /tests/examples/sorted/gradle-version-catalog.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | barVersion = "2.0.0" 3 | fooVersion = "1.0.0" 4 | 5 | [libraries] 6 | barLib = {module = "com.example:bar", version.ref = "barVersion"} 7 | fooLib = {module = "com.example:foo", version.ref = "fooVersion"} 8 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: toml-sort 2 | name: toml-sort 3 | entry: toml-sort 4 | args: [--check] 5 | language: python 6 | types: [toml] 7 | - id: toml-sort-fix 8 | name: toml-sort-fix 9 | entry: toml-sort 10 | args: [--in-place] 11 | language: python 12 | types: [toml] 13 | -------------------------------------------------------------------------------- /tests/examples/dotted-key.toml: -------------------------------------------------------------------------------- 1 | # Header comment 2 | 3 | # Attached to project 4 | [project] 5 | # This is my homepage 6 | urls.homepage = "https://example.com" 7 | # Documentation 8 | urls.documentation = "https://readthedocs.org" 9 | 10 | [other-project] 11 | # test2 12 | deeply.deeply.deeply.nested.key.test2 = "test2" 13 | # test1 14 | deeply.deeply.deeply.nested.key.test1 = "test1" 15 | -------------------------------------------------------------------------------- /tests/examples/sorted/dotted-key.toml: -------------------------------------------------------------------------------- 1 | # Header comment 2 | 3 | [other-project] 4 | # test1 5 | deeply.deeply.deeply.nested.key.test1 = "test1" 6 | # test2 7 | deeply.deeply.deeply.nested.key.test2 = "test2" 8 | 9 | # Attached to project 10 | [project] 11 | # Documentation 12 | urls.documentation = "https://readthedocs.org" 13 | # This is my homepage 14 | urls.homepage = "https://example.com" 15 | -------------------------------------------------------------------------------- /tests/examples/sorted/weird.toml: -------------------------------------------------------------------------------- 1 | # My great TOML example 2 | 3 | title = "The example" 4 | 5 | [a-section] 6 | date = "2019" 7 | name = "Samuel Roeca" 8 | 9 | [[a-section.hello]] 10 | ports = [ 8001, 8001, 8002 ] 11 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 12 | 13 | [[a-section.hello]] 14 | ports = [ 80 ] 15 | dob = 1920-05-27T07:32:00Z # Another date! 16 | 17 | [b-section] 18 | date = "2018" 19 | name = "Richard Stallman" 20 | -------------------------------------------------------------------------------- /tests/examples/pyproject-weird-order.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry>=0.12"] 3 | 4 | [tool.poetry] 5 | name = "toml-sort" 6 | version = "0.14.0" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.6" 10 | 11 | [tool.poetry.scripts] 12 | toml-sort = 'toml_sort.cli:cli' 13 | 14 | [tool.tox] 15 | legacy_tox_ini = """ 16 | [tox] 17 | envlist = py36,py37 18 | """ 19 | 20 | [tool.black] 21 | line-length = 79 22 | 23 | [tool.poetry.dev-dependencies] 24 | pylint = "^2.3" 25 | -------------------------------------------------------------------------------- /tests/examples/sorted/pyproject-weird-order.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry>=0.12"] 3 | 4 | [tool.black] 5 | line-length = 79 6 | 7 | [tool.poetry] 8 | name = "toml-sort" 9 | version = "0.14.0" 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.6" 13 | 14 | [tool.poetry.dev-dependencies] 15 | pylint = "^2.3" 16 | 17 | [tool.poetry.scripts] 18 | toml-sort = 'toml_sort.cli:cli' 19 | 20 | [tool.tox] 21 | legacy_tox_ini = """ 22 | [tox] 23 | envlist = py36,py37 24 | """ 25 | -------------------------------------------------------------------------------- /tests/examples/weird.toml: -------------------------------------------------------------------------------- 1 | # My great TOML example 2 | 3 | title = "The example" 4 | 5 | [[a-section.hello]] 6 | ports = [ 8001, 8001, 8002 ] 7 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 8 | 9 | 10 | 11 | [b-section] 12 | date = "2018" 13 | name = "Richard Stallman" 14 | 15 | [[a-section.hello]] 16 | ports = [ 80 ] 17 | dob = 1920-05-27T07:32:00Z # Another date! 18 | 19 | [a-section] 20 | date = "2019" 21 | name = "Samuel Roeca" 22 | -------------------------------------------------------------------------------- /tests/examples/sorted/comment-no-comments.toml: -------------------------------------------------------------------------------- 1 | title = "The example" 2 | 3 | [POWER] 4 | play = true 5 | 6 | [a-section] 7 | date = "2019" 8 | name = "Samuel Roeca" 9 | 10 | [[a-section.hello]] 11 | dob = 1979-05-27T07:32:00Z 12 | ports = [8001, 8001, 8002] 13 | 14 | [[a-section.hello]] 15 | dob = 1920-05-27T07:32:00Z 16 | ports = [80] 17 | 18 | [b-section] 19 | date = "2018" 20 | name = "Richard Stallman" 21 | 22 | [c-section] 23 | foo = "bar" 24 | 25 | [c-section.test] 26 | name = "Jonathan Green" 27 | 28 | [c-section.test.x.y.z.bar] 29 | foo = "foo" 30 | 31 | [c-section.test.x.y.z.baz] 32 | foo = "bar" 33 | -------------------------------------------------------------------------------- /example.toml: -------------------------------------------------------------------------------- 1 | # My great TOML example 2 | 3 | title = "The example" 4 | 5 | [[a-section.hello]] 6 | ports = [ 8001, 8001, 8002 ] 7 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 8 | 9 | 10 | 11 | [b-section] 12 | date = "2018" 13 | name = "Richard Stallman" 14 | 15 | [[a-section.hello]] 16 | ports = [ 80 ] 17 | dob = 1920-05-27T07:32:00Z # Another date! 18 | 19 | [a-section] 20 | date = "2019" 21 | name = "Samuel Roeca" 22 | Date = "hello" 23 | Noodle = "Noodle" 24 | 25 | -------------------------------------------------------------------------------- /tests/examples/sorted/inline-no-comments-no-table-sort.toml: -------------------------------------------------------------------------------- 1 | [b] 2 | aaalist = [ 3 | [ 4 | {a-test = "Did this sort?", name = "Lisa Simpson", town = "Springfield"}, 5 | ], 6 | [ 7 | {name = "Bart Simpson", town = "Springfield"}, 8 | ], 9 | [ 10 | {name = "Homer Simpson", town = "Springfield"}, 11 | ], 12 | ] 13 | 'aalist' = {a = "another test", c = "test"} 14 | alist = [ 15 | 'a', 16 | 'g', 17 | 'w', 18 | ] 19 | blist = ['a', 'b', 'c'] 20 | q = {} 21 | z = [] 22 | 23 | [a] 24 | test = [ 25 | {a = [ 26 | "", 27 | ], x = [ 28 | "bar", 29 | "foo", 30 | ]}, 31 | {y = [ 32 | "baz", 33 | "foo", 34 | ]}, 35 | ] 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | - name: Setup Python 12 | uses: actions/setup-python@v5 13 | with: 14 | python-version: "3.12" 15 | architecture: x64 16 | - name: Deploy 17 | env: 18 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.POETRY_PYPI_TOKEN_PYPI }} 19 | run: | 20 | python -m pip install -U pip 21 | python -m pip install wheel 22 | python -m pip install poetry 23 | poetry build 24 | poetry publish 25 | -------------------------------------------------------------------------------- /tests/examples/sorted/inline.toml: -------------------------------------------------------------------------------- 1 | [a] 2 | test = [ 3 | {a = [ 4 | "" 5 | ], x = [ 6 | "bar", 7 | "foo" 8 | ]}, 9 | {y = [ 10 | "baz", 11 | "foo" 12 | ]} 13 | ] 14 | 15 | [b] 16 | # Multilevel inline array 17 | aaalist = [ 18 | # Weird block comment 19 | [ 20 | # Another weird block comment 21 | {a-test = "Did this sort?", name = "Lisa Simpson", town = "Springfield"} # Should get sorted 22 | ], 23 | [ 24 | {name = "Bart Simpson", town = "Springfield"} 25 | ], 26 | [ 27 | {name = "Homer Simpson", town = "Springfield"} 28 | ] 29 | ] 30 | 'aalist' = {a = "another test", c = "test"} # list comment 31 | alist = [ 32 | # Multiline 33 | # Second attached comment 34 | 'a', 35 | 'g', 36 | # Comment attached 37 | 'w' # Comment inline 38 | ] 39 | blist = ['a', 'b', 'c'] # Comment 40 | q = {} 41 | z = [] 42 | -------------------------------------------------------------------------------- /tests/examples/sorted/inline-default.toml: -------------------------------------------------------------------------------- 1 | [a] 2 | test = [ 3 | {x = [ 4 | "foo", 5 | "bar" 6 | ], a = [ 7 | "" 8 | ]}, 9 | {y = [ 10 | "foo", 11 | "baz" 12 | ]} 13 | ] 14 | 15 | [b] 16 | z = [] 17 | q = {} 18 | blist = ['c', 'a', 'b'] # Comment 19 | alist = [ 20 | 'g', 21 | # Comment attached 22 | 'w', # Comment inline 23 | # Multiline 24 | # Second attached comment 25 | 'a' 26 | ] 27 | 'aalist' = {c = "test", a = "another test"} # list comment 28 | # Multilevel inline array 29 | aaalist = [ 30 | [ 31 | {name = "Homer Simpson", town = "Springfield"} 32 | ], 33 | [ 34 | {name = "Bart Simpson", town = "Springfield"} 35 | ], 36 | # Weird block comment 37 | [ 38 | # Another weird block comment 39 | {name = "Lisa Simpson", town = "Springfield", a-test = "Did this sort?"} # Should get sorted 40 | ] 41 | ] 42 | -------------------------------------------------------------------------------- /tests/examples/inline.toml: -------------------------------------------------------------------------------- 1 | [b] 2 | z = [] 3 | q = {} 4 | blist=[ 'c', 'a', 'b'] # Comment 5 | alist = [ 6 | 'g', 7 | 8 | 9 | 10 | # Comment attached 11 | 'w', # Comment inline 12 | 13 | # Orphan comment 14 | 15 | #Multiline 16 | # Second attached comment 17 | 'a' 18 | 19 | ] 20 | 'aalist' = { c="test", a= "another test" }#list comment 21 | # Multilevel inline array 22 | aaalist = [ 23 | [ 24 | { name="Homer Simpson", town ="Springfield"} 25 | ], 26 | [ 27 | { name="Bart Simpson", town ="Springfield"} 28 | ], 29 | # Weird block comment 30 | [ 31 | # Another weird block comment 32 | {name="Lisa Simpson", town ="Springfield", a-test = "Did this sort?"}#Should get sorted 33 | ], 34 | ] 35 | [a] 36 | test = [ 37 | {x = [ 38 | "foo", 39 | "bar" 40 | ], a = [ 41 | "" 42 | ]}, 43 | {y = [ 44 | "foo", 45 | "baz" 46 | ]} 47 | ] -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Fixtures shared across tests.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pathlib import Path 6 | from typing import Callable, List 7 | 8 | import pytest 9 | 10 | 11 | @pytest.fixture(name="fixture_path") 12 | def get_fixture_path() -> Path: 13 | """Path for where TOML files for the tests.""" 14 | return Path(__file__).parent / "examples" 15 | 16 | 17 | @pytest.fixture() 18 | def get_fixture(fixture_path: Path) -> Callable[[str | List[str]], Path]: 19 | """Callable for fixture file Path object.""" 20 | 21 | def getter(fixture_names: str | List[str]) -> Path: 22 | if isinstance(fixture_names, str): 23 | fixture_names = [fixture_names] 24 | 25 | fixture_names[-1] = f"{fixture_names[-1]}.toml" 26 | 27 | path = fixture_path 28 | for name in fixture_names: 29 | path /= name 30 | 31 | return path 32 | 33 | return getter 34 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | """Configure nox.""" 2 | 3 | import nox 4 | 5 | NOX_SESSION = nox.session(python=False) 6 | 7 | 8 | @NOX_SESSION 9 | def fix(session: nox.Session): 10 | """Fix files inplace.""" 11 | session.run("ruff", "format", "-s", ".") 12 | session.run("ruff", "check", "-se", "--fix", ".") 13 | 14 | 15 | @NOX_SESSION 16 | def lint(session: nox.Session): 17 | """Check file formatting that only have to do with formatting.""" 18 | session.run("ruff", "format", "--check", ".") 19 | session.run("ruff", "check", ".") 20 | 21 | 22 | @NOX_SESSION 23 | def typecheck(session: nox.Session): 24 | session.run("mypy", "toml_sort") 25 | 26 | 27 | @NOX_SESSION 28 | def tests(session: nox.Session): 29 | session.run("pytest", "tests") 30 | 31 | 32 | @NOX_SESSION 33 | def coverage(session: nox.Session): 34 | session.run( 35 | "pytest", 36 | "--cov", 37 | "toml_sort", 38 | "--cov-report", 39 | "term-missing", 40 | "tests", 41 | ) 42 | -------------------------------------------------------------------------------- /tests/examples/sorted/comment-comments-preserved.toml: -------------------------------------------------------------------------------- 1 | # Comment attached to title 2 | title = "The example" # test 3 | 4 | # Wacky Waving Inflatable Arm-Flailing Tubeman 5 | [POWER] 6 | play = true # test 7 | 8 | # I have a comment as well 9 | [a-section] 10 | # last one 11 | date = "2019" 12 | name = "Samuel Roeca" 13 | 14 | # start test 15 | [[a-section.hello]] 16 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 17 | # test comment 18 | ports = [8001, 8001, 8002] 19 | 20 | # Another test comment 21 | # make sure multiline comments work 22 | [[a-section.hello]] # Comment here? 23 | # multi line comment 24 | # test comment 25 | dob = 1920-05-27T07:32:00Z # Another date! 26 | ports = [80] 27 | 28 | # Another comment 29 | [b-section] # Comment there? 30 | date = "2018" # one more test 31 | name = "Richard Stallman" 32 | 33 | [c-section] 34 | foo = "bar" 35 | 36 | # Comment 123 37 | [c-section.test] 38 | name = "Jonathan Green" 39 | 40 | # Comment 213 41 | [c-section.test.x.y.z.bar] 42 | foo = "foo" 43 | 44 | # Comment 321 45 | [c-section.test.x.y.z.baz] 46 | foo = "bar" 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sam Roeca 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/examples/sorted/comment-header-footer.toml: -------------------------------------------------------------------------------- 1 | # My great TOML example 2 | # it has multiple lines 3 | 4 | # Comment attached to title 5 | title = "The example" # test 6 | 7 | # Wacky Waving Inflatable Arm-Flailing Tubeman 8 | [POWER] 9 | play = true # test 10 | 11 | # I have a comment as well 12 | [a-section] 13 | # last one 14 | date = "2019" 15 | name = "Samuel Roeca" 16 | 17 | # start test 18 | [[a-section.hello]] 19 | # test comment 20 | ports = [8001, 8001, 8002] 21 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 22 | 23 | # Another test comment 24 | # make sure multiline comments work 25 | [[a-section.hello]] # Comment here? 26 | ports = [80] 27 | # multi line comment 28 | # test comment 29 | dob = 1920-05-27T07:32:00Z # Another date! 30 | 31 | # Another comment 32 | [b-section] # Comment there? 33 | date = "2018" # one more test 34 | name = "Richard Stallman" 35 | 36 | [c-section] 37 | foo = "bar" 38 | 39 | # Comment 123 40 | [c-section.test] 41 | name = "Jonathan Green" 42 | 43 | # Comment 213 44 | [c-section.test.x.y.z.bar] 45 | foo = "foo" 46 | 47 | # Comment 321 48 | [c-section.test.x.y.z.baz] 49 | foo = "bar" 50 | 51 | # end test 52 | -------------------------------------------------------------------------------- /tests/examples/sorted/from-toml-lang.toml: -------------------------------------------------------------------------------- 1 | # This is a TOML document. Boom. 2 | 3 | title = "TOML Example" 4 | 5 | [clients] 6 | data = [["gamma", "delta"], [1, 2]] # just an update to make sure parsers support it 7 | # Line breaks are OK when inside arrays 8 | hosts = [ 9 | "alpha", 10 | "omega" 11 | ] 12 | 13 | [database] 14 | server = "192.168.1.1" 15 | ports = [8001, 8001, 8002] 16 | connection_max = 5000 17 | enabled = true # Comment after a boolean 18 | 19 | [inline] 20 | # Test "first" sorting for inline tables 21 | c = {limit = "<3.12", version = "4.12.2"} 22 | 23 | [owner] 24 | name = "Tom Preston-Werner" 25 | organization = "GitHub" 26 | bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." 27 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 28 | 29 | [[products]] 30 | name = "Hammer" 31 | sku = 738594937 32 | 33 | [[products]] 34 | name = "Nail" 35 | sku = 284758393 36 | color = "gray" 37 | 38 | [servers] 39 | 40 | # You can indent as you please. Tabs or spaces. TOML don't care. 41 | [servers.alpha] 42 | ip = "10.0.0.1" 43 | dc = "eqdc10" 44 | 45 | [servers.beta] 46 | ip = "10.0.0.2" 47 | dc = "eqdc10" 48 | country = "中国" # This should be parsed as UTF-8 49 | -------------------------------------------------------------------------------- /tests/examples/sorted/from-toml-lang-first.toml: -------------------------------------------------------------------------------- 1 | # This is a TOML document. Boom. 2 | 3 | title = "TOML Example" 4 | 5 | [servers] 6 | 7 | # You can indent as you please. Tabs or spaces. TOML don't care. 8 | [servers.alpha] 9 | dc = "eqdc10" 10 | ip = "10.0.0.1" 11 | 12 | [servers.beta] 13 | country = "中国" # This should be parsed as UTF-8 14 | dc = "eqdc10" 15 | ip = "10.0.0.2" 16 | 17 | [[products]] 18 | name = "Hammer" 19 | sku = 738594937 20 | 21 | [[products]] 22 | color = "gray" 23 | name = "Nail" 24 | sku = 284758393 25 | 26 | [clients] 27 | data = [["delta", "gamma"], [1, 2]] # just an update to make sure parsers support it 28 | # Line breaks are OK when inside arrays 29 | hosts = [ 30 | "alpha", 31 | "omega" 32 | ] 33 | 34 | [database] 35 | ports = [8001, 8001, 8002] 36 | connection_max = 5000 37 | enabled = true # Comment after a boolean 38 | server = "192.168.1.1" 39 | 40 | [inline] 41 | # Test "first" sorting for inline tables 42 | c = {version = "4.12.2", limit = "<3.12"} 43 | 44 | [owner] 45 | name = "Tom Preston-Werner" 46 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 47 | bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." 48 | organization = "GitHub" 49 | -------------------------------------------------------------------------------- /tests/examples/sorted/from-toml-lang-overrides.toml: -------------------------------------------------------------------------------- 1 | # This is a TOML document. Boom. 2 | 3 | title = "TOML Example" 4 | 5 | [clients] 6 | data = [["gamma", "delta"], [1, 2]] # just an update to make sure parsers support it 7 | # Line breaks are OK when inside arrays 8 | hosts = [ 9 | "alpha", 10 | "omega" 11 | ] 12 | 13 | [database] 14 | connection_max = 5000 15 | enabled = true # Comment after a boolean 16 | ports = [8001, 8001, 8002] 17 | server = "192.168.1.1" 18 | 19 | [inline] 20 | # Test "first" sorting for inline tables 21 | c = {limit = "<3.12", version = "4.12.2"} 22 | 23 | [owner] 24 | bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." 25 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 26 | name = "Tom Preston-Werner" 27 | organization = "GitHub" 28 | 29 | [[products]] 30 | name = "Hammer" 31 | sku = 738594937 32 | 33 | [[products]] 34 | color = "gray" 35 | name = "Nail" 36 | sku = 284758393 37 | 38 | [servers] 39 | 40 | # You can indent as you please. Tabs or spaces. TOML don't care. 41 | [servers.alpha] 42 | dc = "eqdc10" 43 | ip = "10.0.0.1" 44 | 45 | [servers.beta] 46 | ip = "10.0.0.2" 47 | dc = "eqdc10" 48 | country = "中国" # This should be parsed as UTF-8 49 | -------------------------------------------------------------------------------- /tests/examples/from-toml-lang.toml: -------------------------------------------------------------------------------- 1 | # This is a TOML document. Boom. 2 | 3 | title = "TOML Example" 4 | 5 | [owner] 6 | name = "Tom Preston-Werner" 7 | organization = "GitHub" 8 | bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." 9 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 10 | 11 | [database] 12 | server = "192.168.1.1" 13 | ports = [ 8001, 8001, 8002 ] 14 | connection_max = 5000 15 | enabled = true # Comment after a boolean 16 | 17 | [servers] 18 | 19 | # You can indent as you please. Tabs or spaces. TOML don't care. 20 | [servers.alpha] 21 | ip = "10.0.0.1" 22 | dc = "eqdc10" 23 | 24 | [servers.beta] 25 | ip = "10.0.0.2" 26 | dc = "eqdc10" 27 | country = "中国" # This should be parsed as UTF-8 28 | 29 | [clients] 30 | data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it 31 | 32 | # Line breaks are OK when inside arrays 33 | hosts = [ 34 | "alpha", 35 | "omega" 36 | ] 37 | 38 | # Products 39 | 40 | [[products]] 41 | name = "Hammer" 42 | sku = 738594937 43 | 44 | [[products]] 45 | name = "Nail" 46 | sku = 284758393 47 | color = "gray" 48 | 49 | [inline] 50 | # Test "first" sorting for inline tables 51 | c = { limit="<3.12", version ="4.12.2"} -------------------------------------------------------------------------------- /tests/examples/comment.toml: -------------------------------------------------------------------------------- 1 | # My great TOML example 2 | # it has multiple lines 3 | 4 | # Orphan <- will be removed 5 | 6 | #Comment attached to title 7 | title = "The example" # test 8 | [c-section] 9 | foo = "bar" 10 | 11 | #start test 12 | [[a-section.hello]] 13 | #test comment 14 | ports = [ 8001, 8001, 8002 ] 15 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 16 | 17 | # Another test comment 18 | # make sure multiline comments work 19 | [[a-section.hello]] # Comment here? 20 | ports = [ 80 ] 21 | # multi line comment 22 | # test comment 23 | dob = 1920-05-27T07:32:00Z # Another date! 24 | 25 | # Another comment 26 | [b-section] #Comment there? 27 | date = "2018" # one more test 28 | name = "Richard Stallman" 29 | 30 | # I have a comment as well 31 | [a-section] 32 | # last one 33 | date = "2019" 34 | name = "Samuel Roeca" 35 | 36 | # Comment 123 37 | [c-section.test] 38 | name = "Jonathan Green" 39 | 40 | #Wacky Waving Inflatable Arm-Flailing Tubeman 41 | [POWER] 42 | play = true#test 43 | 44 | # Comment 321 45 | [c-section.test.x.y.z.baz] 46 | foo = "bar" 47 | 48 | # Comment 213 49 | [c-section.test.x.y.z.bar] 50 | foo = "foo" 51 | 52 | #end test -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.mypy] 6 | python_version = "3.9" 7 | strict = true 8 | enable_error_code = "ignore-without-code,redundant-expr,truthy-bool" 9 | 10 | [tool.poetry] 11 | name = "toml-sort" 12 | version = "0.24.3" 13 | description = "Toml sorting library" 14 | authors = ["Sam Roeca "] 15 | readme = "README.md" 16 | homepage = "https://github.com/pappasam/toml-sort" 17 | repository = "https://github.com/pappasam/toml-sort" 18 | keywords = ["toml", "sort", "cli", "unix", "utility"] 19 | classifiers = [ 20 | "Development Status :: 4 - Beta", 21 | "Environment :: Console", 22 | "Topic :: Software Development :: Code Generators", 23 | "Topic :: Utilities", 24 | "Typing :: Typed", 25 | ] 26 | include = ["README.md"] 27 | license = "MIT" 28 | 29 | [tool.poetry.dependencies] 30 | python = ">=3.9" 31 | tomlkit = ">=0.13.2" 32 | 33 | [tool.poetry.group.dev.dependencies] 34 | mypy = "*" 35 | nox = "*" 36 | pytest = "*" 37 | pytest-cov = "*" 38 | ruff = "*" 39 | 40 | [tool.poetry.scripts] 41 | toml-sort = 'toml_sort.cli:cli' 42 | 43 | [tool.ruff.lint] 44 | select = [ 45 | "D", # pydocstyle 46 | "E", # pycodestyle 47 | "F", # pyflakes 48 | "I", # isort 49 | ] 50 | ignore = [ 51 | "D10", # missing-docstring 52 | "D206", # indent-with-spaces 53 | "D203", # one-blank-line-before-class 54 | "D213", # multiline-summary-second-line 55 | "D300", # triple-single-quotes 56 | "D401", # imperative-mood 57 | "E111", # indentation-with-invalid-multiple 58 | "E114", # indentation-with-invalid-multiple-comment 59 | "E117", # over-indented 60 | "E501", # line-too-long 61 | ] 62 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | help: ## Print this help menu 3 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ 4 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 5 | 6 | .PHONY: require 7 | require: ## Check that prerequisites are installed. 8 | @if ! command -v python3 > /dev/null; then \ 9 | printf "\033[1m\033[31mERROR\033[0m: python3 not installed\n" >&2 ; \ 10 | exit 1; \ 11 | fi 12 | @if ! python3 -c "import sys; sys.exit(sys.version_info < (3,8))"; then \ 13 | printf "\033[1m\033[31mERROR\033[0m: python 3.8+ required\n" >&2 ; \ 14 | exit 1; \ 15 | fi 16 | @if ! command -v poetry > /dev/null; then \ 17 | printf "\033[1m\033[31mERROR\033[0m: poetry not installed.\n" >&2 ; \ 18 | printf "Please install with 'python3 -mpip install --user poetry'\n" >&2 ; \ 19 | exit 1; \ 20 | fi 21 | 22 | .PHONY: setup 23 | setup: require .setup_complete ## Set up the local development environment 24 | 25 | .setup_complete: poetry.lock ## Internal helper to run the setup. 26 | poetry install 27 | touch .setup_complete 28 | 29 | .PHONY: fix 30 | fix: ## Fix all files in-place 31 | poetry run nox -s $@ 32 | 33 | .PHONY: lint 34 | lint: ## Run linters on all files 35 | poetry run nox -s $@ 36 | 37 | .PHONY: typecheck 38 | typecheck: ## Run static type checks 39 | poetry run nox -s $@ 40 | 41 | .PHONY: tests 42 | tests: ## Run unit tests 43 | poetry run nox -s $@ 44 | 45 | .PHONY: publish 46 | publish: ## Build & publish the new version 47 | poetry build 48 | poetry publish 49 | 50 | .PHONY: clean 51 | clean: ## Remove local development environment 52 | if poetry env list | grep -q Activated; then \ 53 | poetry env remove python3; \ 54 | fi 55 | rm -f .setup_complete 56 | -------------------------------------------------------------------------------- /.github/workflows/testing.yaml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | - name: Select Python 3.13 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: "3.13" 17 | architecture: x64 18 | - name: Install Dependencies 19 | run: | 20 | python -m pip install -U pip 21 | python -m pip install wheel 22 | python -m pip install poetry 23 | poetry install 24 | - name: Run linting 25 | run: poetry run nox -s lint 26 | - name: Run static type checking 27 | run: poetry run nox -s typecheck 28 | tests: 29 | needs: [lint] 30 | runs-on: ${{ matrix.os }} 31 | strategy: 32 | matrix: 33 | os: [ubuntu-latest] 34 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | - name: Select Python ${{ matrix.python-version }} 39 | uses: actions/setup-python@v5 40 | with: 41 | python-version: ${{ matrix.python-version }} 42 | architecture: x64 43 | - name: Install Dependencies 44 | run: | 45 | python -m pip install -U pip 46 | python -m pip install wheel 47 | python -m pip install poetry 48 | poetry install 49 | - name: Run Tests 50 | run: poetry run nox -s tests 51 | coverage: 52 | needs: [lint] 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | - name: Select Python 3.13 58 | uses: actions/setup-python@v5 59 | with: 60 | python-version: "3.13" 61 | architecture: x64 62 | - name: Install Dependencies 63 | run: | 64 | python -m pip install -U pip 65 | python -m pip install wheel 66 | python -m pip install poetry 67 | poetry install 68 | - name: Run Coverage 69 | env: 70 | WITH_COVERAGE: true 71 | run: poetry run nox -s coverage 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################# 2 | # Python.gitignore 3 | ################################################################# 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | 65 | # Flask stuff: 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## 0.24.3 8 | 9 | ### Fixed 10 | 11 | - Handle dotted keys in inline tables correctly (usimd) 12 | - Remove redundant condition in single file write logic (liblaf) 13 | - Remove python 4 max cap from pyproject.toml (Gregorio L. Trevisan) 14 | 15 | ## 0.24.2 16 | 17 | ### Fixed 18 | 19 | - Regression from 0.24.1 and 0.24.0 where sorting inline tables would throw a ValueError if the key in the table had trailing spaces. Resolves 20 | 21 | ## 0.24.1 22 | 23 | ### Fixed 24 | 25 | - Re-add .pre-commit-hooks. Resolves 26 | 27 | ## 0.24.0 28 | 29 | ### Fixed 30 | 31 | - Only write to disk if file contents changed 32 | 33 | ### Removed 34 | 35 | - Dropped support for Python 3.7 and 3.8. 36 | - Dropped support for tomlkit < 0.13.2 37 | - Removed sphinx documentation. 38 | 39 | ## 0.23.1 40 | 41 | ### Fixed 42 | 43 | - Error where the `first` override introduced tomlkit wrappers into the configuration and caused serialization problems for the dataclass: 44 | 45 | ## 0.23.0 46 | 47 | ### Added 48 | 49 | - Ability to override sort configuration per key in configuration: 50 | - Option to sort particular keys first in output: , updated by 51 | 52 | ### Fixed 53 | 54 | - Issue where files that contain only comments are doubled: 55 | 56 | ## 0.22.4 57 | 58 | ### Fixed 59 | 60 | - Trailing whitespace is no longer added to blank comments 61 | - An issue where dotted keys raised a `TypeError` 62 | 63 | ## 0.22.3 64 | 65 | ### Fixed 66 | 67 | - Turns out that, at this time, `toml-sort` is only compatible with `tomlkit` `0.11.2`+. We now make this clear in our dependencies. See: and . 68 | 69 | ## 0.22.2 70 | 71 | ### Added 72 | 73 | - New pre-commit hook (`toml-sort-fix`) that enables users to change, instead of check, their toml files. 74 | 75 | ## 0.22.1 76 | 77 | ### Fixed 78 | 79 | - Issue where an IndexError exception was raised on an empty array. See: 80 | 81 | ## 0.22.0 82 | 83 | Release entirely related to this PR: 84 | 85 | ### Added 86 | 87 | - Optionally add the ability to sort inline tables and arrays. New switches added for this functionality: `--sort-inline-tables` and `--sort-inline-arrays`, which are implied by the existing `--all` option 88 | - New options groups to the CLI, to group the related formatting, comment, and sorting arguments 89 | - Switch to add trailing comma to multi-line inline arrays `--trailing-comma-inline-array` 90 | - Some additional formatting checks 91 | 92 | ### Changed 93 | 94 | - Make sure inline arrays and tables are consistently formatted 95 | - Normalize the formatting for `key = value` pairs, always one space on either side of equals sign 96 | 97 | ## 0.21.0 98 | 99 | This is a pretty big comment-related release. Resolves the long-standing issue: . 100 | 101 | ### Changed 102 | 103 | - Header, Footer, and Block comments are retained by default and can be disabled with new CLI options. 104 | 105 | ### Added 106 | 107 | - New CLI comment-removal options: `--no-comments`, `--no-header-comments`, `--no-footer-comments`, `--no-inline-comments`, `--no-block-comments` 108 | - The ability to configure spaces before inline comments with `--spaces-before-inline-comment` 109 | 110 | ### Deprecated 111 | 112 | - The CLI option: `--no-header`. This will be removed in a future release and is replaced by `--no-header-comments` 113 | 114 | ## 0.20.2 115 | 116 | ### Fixed 117 | 118 | - An issue where the sorter fails with boolean values in file. See and 119 | 120 | ## 0.20.1 121 | 122 | ### Fixed 123 | 124 | - Preserve inline comments for boolean values. See: 125 | 126 | ## 0.20.0 127 | 128 | ### Added 129 | 130 | - Support configuring CLI with `pyproject.toml` configuration file. 131 | 132 | ### Changed 133 | 134 | - Moved CLI from click to `argparse`. 135 | - Update minimum version of `tomlkit` to 0.8.0. This drops support for Python 3.6. 136 | 137 | ## 0.19.0 138 | 139 | ### Added 140 | 141 | - CLI option to ignore case while sorting 142 | 143 | ## 0.18.0 144 | 145 | Note: in this release, we've discovered a strange bug (we believe in `tomlkit`) where, sometimes, AOT elements are ordered without our consent. One of the CLI tests has been marked `xfail`. If you'd like to help figure out why, we're always looking for contributors! 146 | 147 | ### Added 148 | 149 | - Support for multiple FILENAME arguments in the CLI. Used for `precommit` / linting multiple files. Thanks @atugushev! 150 | - Pre-commit hook 151 | 152 | ### Changed 153 | 154 | - Provide more-comprehensive error messages for interactive CLI usage. 155 | 156 | ## 0.17.0 157 | 158 | ### Added 159 | 160 | - This `CHANGELOG.md` 161 | 162 | ### Changed 163 | 164 | - `tomlkit>=0.5.8`. Upstream library made some important fixes. 165 | - Add `isort`, make part of pre-commit pipeline. 166 | 167 | ### Fixed 168 | 169 | - `mypy`, `pylint`, `black`, and `isort` all pass. 170 | 171 | ### Removed 172 | 173 | - AOT workaround in AOT parser. Latest version of `tomlkit` no longer spits out duplicate elements for AOT. 174 | -------------------------------------------------------------------------------- /tests/test_toml_sort.py: -------------------------------------------------------------------------------- 1 | """Test the toml_sort module.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pathlib import Path 6 | from typing import Any, Callable, Dict, List 7 | 8 | import pytest 9 | 10 | from toml_sort import TomlSort 11 | from toml_sort.tomlsort import ( 12 | CommentConfiguration, 13 | FormattingConfiguration, 14 | SortConfiguration, 15 | SortOverrideConfiguration, 16 | ) 17 | 18 | 19 | def test_sort_toml_is_str() -> None: 20 | """Take a TOML string, sort it, and return the sorted string.""" 21 | sorted_result = TomlSort("[hello]").sorted() 22 | assert isinstance(sorted_result, str) 23 | 24 | 25 | @pytest.mark.parametrize( 26 | "unsorted_fixture,sorted_fixture,args", 27 | [ 28 | ( 29 | "gradle-version-catalog", 30 | "gradle-version-catalog", 31 | { 32 | "sort_config": SortConfiguration( 33 | tables=False, 34 | ), 35 | }, 36 | ), 37 | ( 38 | "dotted-key", 39 | "dotted-key", 40 | { 41 | "sort_config": SortConfiguration( 42 | inline_arrays=True, inline_tables=True 43 | ), 44 | }, 45 | ), 46 | ( 47 | "inline", 48 | "inline", 49 | { 50 | "sort_config": SortConfiguration( 51 | inline_arrays=True, inline_tables=True 52 | ), 53 | }, 54 | ), 55 | ( 56 | "comment", 57 | "comment-comments-preserved", 58 | {"comment_config": CommentConfiguration(header=False, footer=False)}, 59 | ), 60 | ( 61 | "comment", 62 | "comment-no-comments", 63 | { 64 | "comment_config": CommentConfiguration( 65 | header=False, 66 | footer=False, 67 | block=False, 68 | inline=False, 69 | ) 70 | }, 71 | ), 72 | ( 73 | "comment", 74 | "comment-header-footer", 75 | { 76 | "sort_config": SortConfiguration(table_keys=False), 77 | "format_config": FormattingConfiguration( 78 | spaces_before_inline_comment=1 79 | ), 80 | }, 81 | ), 82 | ( 83 | "from-toml-lang", 84 | "from-toml-lang", 85 | { 86 | "sort_config": SortConfiguration(table_keys=False), 87 | "format_config": FormattingConfiguration( 88 | spaces_before_inline_comment=1 89 | ), 90 | }, 91 | ), 92 | ( 93 | "from-toml-lang", 94 | "from-toml-lang-overrides", 95 | { 96 | "sort_config": SortConfiguration( 97 | inline_arrays=True, inline_tables=True 98 | ), 99 | "format_config": FormattingConfiguration( 100 | spaces_before_inline_comment=1 101 | ), 102 | "sort_config_overrides": { 103 | "servers.beta": SortOverrideConfiguration(table_keys=False), 104 | "clients.data": SortOverrideConfiguration(inline_arrays=False), 105 | }, 106 | }, 107 | ), 108 | ( 109 | "from-toml-lang", 110 | "from-toml-lang-first", 111 | { 112 | "sort_config": SortConfiguration( 113 | inline_arrays=True, 114 | inline_tables=True, 115 | first=["servers", "products"], 116 | ), 117 | "format_config": FormattingConfiguration( 118 | spaces_before_inline_comment=1 119 | ), 120 | "sort_config_overrides": { 121 | "database": SortOverrideConfiguration(first=["ports"]), 122 | "owner": SortOverrideConfiguration(first=["name", "dob"]), 123 | "inline.c": SortOverrideConfiguration(first=["version"]), 124 | }, 125 | }, 126 | ), 127 | ( 128 | "pyproject-weird-order", 129 | "pyproject-weird-order", 130 | { 131 | "sort_config": SortConfiguration(table_keys=False), 132 | "comment_config": CommentConfiguration(block=False), 133 | "format_config": FormattingConfiguration( 134 | spaces_before_inline_comment=1 135 | ), 136 | }, 137 | ), 138 | pytest.param( 139 | "weird", 140 | "weird", 141 | { 142 | "sort_config": SortConfiguration(table_keys=False), 143 | "comment_config": CommentConfiguration( 144 | block=False, 145 | ), 146 | "format_config": FormattingConfiguration( 147 | spaces_before_inline_comment=1 148 | ), 149 | }, 150 | marks=[pytest.mark.xfail], 151 | ), 152 | pytest.param( 153 | "single-comment", 154 | "../single-comment", # Input is the same as the output 155 | {}, 156 | ), 157 | pytest.param( 158 | "empty", 159 | "empty", 160 | {}, 161 | ), 162 | ], 163 | ) 164 | def test_tomlsort( 165 | get_fixture: Callable[[str | List[str]], Path], 166 | unsorted_fixture: str, 167 | sorted_fixture: str, 168 | args: Dict[str, Any], 169 | ) -> None: 170 | """Output of TomlSort.sorted() function matches expected.""" 171 | path_unsorted = get_fixture(unsorted_fixture) 172 | path_sorted = get_fixture(["sorted", sorted_fixture]) 173 | 174 | toml_unsorted_fixture = path_unsorted.read_text() 175 | toml_sorted_fixture = path_sorted.read_text() 176 | 177 | sort_output = TomlSort(toml_unsorted_fixture, **args).sorted() 178 | 179 | assert sort_output == toml_sorted_fixture 180 | assert TomlSort(sort_output, **args).sorted() == sort_output 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # toml-sort 2 | 3 | [![pypi-version](https://img.shields.io/pypi/v/toml-sort.svg)](https://python.org/pypi/toml-sort) 4 | [![license](https://img.shields.io/pypi/l/toml-sort.svg)](https://python.org/pypi/toml-sort) 5 | [![image-python-versions](https://img.shields.io/pypi/pyversions/toml-sort)](https://python.org/pypi/toml-sort) 6 | [![image-pypi-downloads](https://pepy.tech/badge/toml-sort)](https://pepy.tech/project/toml-sort) 7 | 8 | A command line utility to sort and format [toml](https://toml.io/en/) files. 9 | 10 | ## Installation 11 | 12 | ```bash 13 | pip install toml-sort 14 | ``` 15 | 16 | ## Motivation 17 | 18 | This library sorts TOML files, providing the following features: 19 | 20 | - Sort tables and Arrays of Tables (AoT) 21 | - Option to sort non-tables / non-AoT's, or not 22 | - Preserve comments, where possible 23 | - Standardize whitespace and indentation 24 | 25 | I wrote this library/application because I couldn't find any "good" sorting utilities for TOML files. Now, I use this as part of my daily workflow. Hopefully it helps you too! 26 | 27 | ## Command line usage 28 | 29 | This project can be used as either a command line utility or a Python library. Read the docs for an overview of its library capabilities. For command line usage, see below: 30 | 31 | ```console 32 | $ toml-sort --help 33 | usage: toml-sort [-h] [--version] [-o OUTPUT] [-i] [-I] [-a] [--no-sort-tables] [--sort-table-keys] 34 | [--sort-inline-tables] [--sort-inline-arrays] [--sort-first KEYS] [--no-header] [--no-comments] 35 | [--no-header-comments] [--no-footer-comments] [--no-inline-comments] [--no-block-comments] 36 | [--spaces-before-inline-comment {1,2,3,4}] [--spaces-indent-inline-array {2,4,6,8}] 37 | [--trailing-comma-inline-array] [--check] 38 | [F ...] 39 | 40 | Toml sort: a sorting utility for toml files. 41 | 42 | positional arguments: 43 | F filename(s) to be processed by toml-sort (default: -) 44 | 45 | optional arguments: 46 | -h, --help show this help message and exit 47 | --version display version information and exit 48 | -o OUTPUT, --output OUTPUT 49 | output filepath (default: '-') 50 | -i, --in-place overwrite the original input file with changes 51 | --check silently check if an original file would be changed by the formatter 52 | 53 | sort: 54 | change sorting behavior 55 | 56 | -I, --ignore-case ignore case when sorting 57 | -a, --all sort ALL keys. This implies sort table-keys, inline-tables and inline arrays. (default: only 58 | sort non-inline 'tables and arrays of tables') 59 | --no-sort-tables Disables the default behavior of sorting tables and arrays of tables by their header value. 60 | Setting this option will keep the order of tables in the toml file the same. 61 | --sort-table-keys Sort the keys in tables and arrays of tables (excluding inline tables and arrays). 62 | --sort-inline-tables Sort inline tables. 63 | --sort-inline-arrays Sort inline arrays. 64 | --sort-first KEYS Table keys that will be sorted first in the output. Multiple keys can be given separated by a 65 | comma. 66 | 67 | comments: 68 | exclude comments from output 69 | 70 | --no-header Deprecated. See --no-header-comments 71 | --no-comments remove all comments. Implies no header, footer, inline, or block comments 72 | --no-header-comments remove a document's leading comments 73 | --no-footer-comments remove a document's trailing comments 74 | --no-inline-comments remove a document's inline comments 75 | --no-block-comments remove a document's block comments 76 | 77 | formatting: 78 | options to change output formatting 79 | 80 | --spaces-before-inline-comment {1,2,3,4} 81 | the number of spaces before an inline comment (default: 1) 82 | --spaces-indent-inline-array {2,4,6,8} 83 | the number of spaces to indent a multiline inline array (default: 2) 84 | --trailing-comma-inline-array 85 | add trailing comma to the last item in a multiline inline array 86 | 87 | Examples: 88 | 89 | - **Stdin -> Stdout**: cat input.toml | toml-sort 90 | - **Disk -> Disk**: toml-sort -o output.toml input.toml 91 | - **Linting**: toml-sort --check input.toml input2.toml input3.toml 92 | - **Inplace Disk**: toml-sort --in-place input.toml input2.toml 93 | 94 | Return codes: 95 | 96 | - 0 : success. 97 | - 1 : errors were found 98 | 99 | Notes: 100 | 101 | - You cannot redirect from a file to itself in Bash. POSIX shells process 102 | redirections first, then execute commands. --in-place exists for this 103 | reason 104 | ``` 105 | 106 | ## Configuration file 107 | 108 | toml-sort can also be configured by using the `pyproject.toml` file. If the file exists and has a `tool.tomlsort` section, the configuration is used. If both command line arguments and the configuration are used, the options are merged. In the case of conflicts, the command line option is used. 109 | 110 | In short, the names are the same as on the command line (and have the same meaning), but `-` is replaced with `_`. Please note, that only the below options are supported: 111 | 112 | ```toml 113 | [tool.tomlsort] 114 | all = true 115 | in_place = true 116 | no_comments = true 117 | no_header_comments = true 118 | no_footer_comments = true 119 | no_inline_comments = true 120 | no_block_comments = true 121 | no_sort_tables = true 122 | sort_first = ["key1", "key2"] 123 | sort_table_keys = true 124 | sort_inline_tables = true 125 | sort_inline_arrays = true 126 | spaces_before_inline_comment = 2 127 | spaces_indent_inline_array = 4 128 | trailing_comma_inline_array = true 129 | check = true 130 | ignore_case = true 131 | ``` 132 | 133 | ### Configuration Overrides 134 | 135 | The `pyproject.toml` configuration file also supports configuration overrides, which are not available as command-line arguments. These overrides allow for fine-grained control of sort options for particular keys. 136 | 137 | Only the following options can be included in an override: 138 | 139 | ```toml 140 | [tool.tomlsort.overrides."path.to.key"] 141 | first = ["key1", "key2"] 142 | table_keys = true 143 | inline_tables = true 144 | inline_arrays = true 145 | ``` 146 | 147 | In the example configuration, `path.to.key` is the key to match. Keys are matched using the [Python fnmatch function](https://docs.python.org/3/library/fnmatch.html), so glob-style wildcards are supported. 148 | 149 | For instance, to disable sorting the table in the following TOML file: 150 | 151 | ```toml 152 | [servers.beta] 153 | ip = "10.0.0.2" 154 | dc = "eqdc10" 155 | country = "中国" 156 | ``` 157 | 158 | You can use any of the following overrides: 159 | 160 | ```toml 161 | # Overrides in own table 162 | [tool.tomlsort.overrides."servers.beta"] 163 | table_keys = false 164 | 165 | # Overrides in the tomlsort table 166 | [tool.tomlsort] 167 | overrides."servers.beta".table_keys = false 168 | 169 | # Override using a wildcard if config should be applied to all servers keys 170 | [tool.tomlsort] 171 | overrides."servers.*".table_keys = false 172 | ``` 173 | 174 | ## Comments 175 | 176 | Due to the free form nature of comments, it is hard to include them in a sort in a generic way that will work for everyone. `toml-sort` deals with four different types of comments. They are all enabled by default, but can be disabled using CLI switches, in which case comments of that type will be removed from the output. 177 | 178 | ### Header 179 | 180 | The first comments in a document, that are followed by a blank line, are treated as a header, and will always remain at the top of the document. If there is no blank line, the comment will be treated as a block comment instead. 181 | 182 | ```toml 183 | # This is a header 184 | # it can be multiple lines, as long as it is followed with a blank line 185 | # it will always stay at the top of the sorted document 186 | 187 | title = "The example" 188 | ``` 189 | 190 | ### Footer 191 | 192 | Any comments at the end of the document, after the last item in the toml, will be treated as a footer, and will always remain at the bottom of the document. 193 | 194 | ```toml 195 | title = "The example" 196 | 197 | # this is a footer comment 198 | ``` 199 | 200 | ### Inline 201 | 202 | Inline comments are comments that are at the end of a line where the start of the line is a toml item. 203 | 204 | ```toml 205 | title = "The example" # This is a inline comment 206 | ``` 207 | 208 | ### Block 209 | 210 | Block comments, are any comments that are on their own line. These comments are treated as _attached_ to the item in the toml that is directly below them, not separated by whitespace. These comments can be multiple lines. Inline comments will appear in the sorted output above the item they were attached to in the input toml. 211 | 212 | ```toml 213 | # Comment attached to title 214 | title = "The example" 215 | 216 | # This comment is an orphan because it 217 | # is separated from a-section by whitespace 218 | 219 | # This comment is attached to a-section 220 | # attached comments can be multiple lines 221 | [a-section] 222 | # This comment is attached to date 223 | date = "2019" 224 | ``` 225 | 226 | ### Orphan 227 | 228 | Orphan comments are any comments that don't fall into the above categories, they will be removed from the output document. 229 | 230 | ```toml 231 | # Header comment 232 | 233 | # Orphan comment, not attached to any item 234 | # because there is whitespace before title 235 | 236 | title = "The example" 237 | 238 | # This comment is an orphan because it 239 | # is separated from a-section by whitespace 240 | 241 | # This comment is attached to a-section 242 | [a-section] 243 | ``` 244 | 245 | ## Example 246 | 247 | The following example shows the input, and output, from the CLI with default options. 248 | 249 | ### Unformatted, unsorted input 250 | 251 | ```toml 252 | # My great TOML example 253 | 254 | title = "The example" 255 | 256 | [[a-section.hello]] 257 | ports = [ 8001, 8001, 8002 ] 258 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 259 | 260 | 261 | # Attached to b-section 262 | [b-section] 263 | date = "2018" 264 | name = "Richard Stallman" 265 | 266 | [[a-section.hello]] 267 | ports = [ 80 ] 268 | #Attached to dob 269 | dob = 1920-05-27T07:32:00Z # Another date! 270 | 271 | [a-section] 272 | date = "2019" 273 | name = "Samuel Roeca" 274 | ``` 275 | 276 | ### Formatted, sorted output 277 | 278 | ```toml 279 | # My great TOML example 280 | 281 | title = "The example" 282 | 283 | [a-section] 284 | date = "2019" 285 | name = "Samuel Roeca" 286 | 287 | [[a-section.hello]] 288 | ports = [ 8001, 8001, 8002 ] 289 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 290 | 291 | [[a-section.hello]] 292 | ports = [ 80 ] 293 | # Attached to dob 294 | dob = 1920-05-27T07:32:00Z # Another date! 295 | 296 | # Attached to b-section 297 | [b-section] 298 | date = "2018" 299 | name = "Richard Stallman" 300 | ``` 301 | 302 | ## Local Development 303 | 304 | Local development for this project is quite simple. 305 | 306 | **Dependencies** 307 | 308 | Install the following tools manually. 309 | 310 | - [Poetry>=1.0](https://github.com/sdispater/poetry#installation) 311 | - [GNU Make](https://www.gnu.org/software/make/) 312 | 313 | ```bash 314 | make setup # set up dev environment 315 | make tests # run tests 316 | ``` 317 | 318 | ## Written by 319 | 320 | Samuel Roeca, *samuel.roeca@gmail.com* 321 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | """Test the CLI.""" 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | import shutil 7 | import subprocess 8 | from pathlib import Path 9 | from typing import Callable, List, NamedTuple, Optional 10 | from unittest import mock 11 | 12 | import pytest 13 | 14 | from toml_sort import cli 15 | from toml_sort.cli import parse_sort_first 16 | from toml_sort.tomlsort import SortOverrideConfiguration 17 | 18 | PATH_EXAMPLES = "tests/examples" 19 | 20 | # NOTE: weird.toml currently exposes what I interpret to be some buggy 21 | # functionality in AOT parsing. It seems like the latest version of tomlkit 22 | # doesn't handle edge cases elegantly here and sometimes elements in AOT's are 23 | # ordered randomly or sorted somewhere in tomlkit itself. I've xfail'd the test 24 | # case for now. 25 | 26 | 27 | class SubprocessReturn(NamedTuple): 28 | """Organize the return results when of running a cli subprocess.""" 29 | 30 | stdout: str 31 | stderr: str 32 | returncode: int 33 | 34 | 35 | def capture( 36 | command: List[str], 37 | stdin: Optional[str] = None, 38 | ) -> SubprocessReturn: 39 | """Capture the output of a subprocess.""" 40 | with subprocess.Popen( 41 | command, 42 | text=True, 43 | encoding="UTF-8", 44 | stdin=subprocess.PIPE, 45 | stdout=subprocess.PIPE, 46 | stderr=subprocess.PIPE, 47 | ) as proc: 48 | stdout, stderr = proc.communicate(input=stdin) 49 | return SubprocessReturn( 50 | stdout=stdout, 51 | stderr=stderr, 52 | returncode=proc.returncode, 53 | ) 54 | 55 | 56 | @pytest.mark.parametrize( 57 | "path_unsorted,path_sorted", 58 | [ 59 | ("from-toml-lang", "sorted/from-toml-lang"), 60 | pytest.param("weird", "sorted/weird", marks=[pytest.mark.xfail]), 61 | ("pyproject-weird-order", "sorted/pyproject-weird-order"), 62 | ("comment", "sorted/comment-header-footer"), 63 | ("inline", "sorted/inline-default"), 64 | ], 65 | ) 66 | def test_cli_defaults( 67 | get_fixture: Callable[[str | List[str]], Path], 68 | path_unsorted: str, 69 | path_sorted: str, 70 | ) -> None: 71 | """Test the basic cli behavior with default arguments. 72 | 73 | Test parameters are relative to the tests/examples directory within 74 | this project. 75 | """ 76 | with get_fixture(path_sorted).open(encoding="UTF-8") as infile: 77 | expected = infile.read() 78 | result_filepath = capture(["toml-sort", str(get_fixture(path_unsorted))]) 79 | assert result_filepath.returncode == 0 80 | assert result_filepath.stdout == expected 81 | 82 | with get_fixture(path_unsorted).open(encoding="UTF-8") as infile: 83 | original = infile.read() 84 | result_stdin = capture(["toml-sort"], stdin=original) 85 | assert result_stdin.returncode == 0 86 | assert result_stdin.stdout == expected 87 | 88 | 89 | @pytest.mark.parametrize( 90 | "path_unsorted,path_sorted,args", 91 | [ 92 | ( 93 | "comment", 94 | ["sorted", "comment-no-comments"], 95 | ["--no-comments", "--all"], 96 | ), 97 | ( 98 | "comment", 99 | ["sorted", "comment-no-comments"], 100 | [ 101 | "--no-inline-comments", 102 | "--no-block-comments", 103 | "--no-header-comments", 104 | "--no-footer-comments", 105 | "--all", 106 | ], 107 | ), 108 | ( 109 | "comment", 110 | ["sorted", "comment-comments-preserved"], 111 | [ 112 | "--no-header-comments", 113 | "--no-footer-comments", 114 | "--spaces-before-inline-comment", 115 | "2", 116 | "--all", 117 | ], 118 | ), 119 | ( 120 | "inline", 121 | ["sorted", "inline"], 122 | [ 123 | "--all", 124 | "--spaces-before-inline-comment", 125 | "2", 126 | ], 127 | ), 128 | ( 129 | "inline", 130 | ["sorted", "inline"], 131 | [ 132 | "--sort-table-keys", 133 | "--sort-inline-array", 134 | "--sort-inline-table", 135 | "--spaces-before-inline-comment", 136 | "2", 137 | ], 138 | ), 139 | ( 140 | "inline", 141 | ["sorted", "inline-no-comments-no-table-sort"], 142 | [ 143 | "--no-sort-tables", 144 | "--sort-table-keys", 145 | "--sort-inline-array", 146 | "--sort-inline-table", 147 | "--no-comments", 148 | "--trailing-comma-inline-array", 149 | ], 150 | ), 151 | ], 152 | ) 153 | def test_cli_args( 154 | get_fixture: Callable[[str | List[str]], Path], 155 | path_unsorted: str, 156 | path_sorted: List[str], 157 | args: List[str], 158 | ) -> None: 159 | """Test the basic cli behavior with different arguments.""" 160 | with get_fixture(path_sorted).open(encoding="UTF-8") as infile: 161 | expected = infile.read() 162 | result_filepath = capture(["toml-sort"] + args + [str(get_fixture(path_unsorted))]) 163 | assert result_filepath.returncode == 0 164 | assert result_filepath.stdout == expected 165 | 166 | 167 | @pytest.mark.parametrize( 168 | "paths, expected_exit_code", 169 | ( 170 | pytest.param( 171 | ["from-toml-lang.toml", "weird.toml"], 172 | 1, 173 | id="multiple unsorted files failed", 174 | ), 175 | pytest.param( 176 | ["from-toml-lang.toml", "sorted/weird.toml"], 177 | 1, 178 | id="single unsorted file failed", 179 | ), 180 | pytest.param( 181 | [ 182 | "sorted/from-toml-lang.toml", 183 | "sorted/pyproject-weird-order.toml", 184 | ], 185 | 0, 186 | id="none failed, no output", 187 | ), 188 | ), 189 | ) 190 | def test_multiple_files_check(paths, expected_exit_code): 191 | """Unsorted files should be checked.""" 192 | paths_unsorted = [os.path.join(PATH_EXAMPLES, path) for path in paths] 193 | result = capture(["toml-sort", "--check"] + paths_unsorted) 194 | assert result.returncode == expected_exit_code, result.stderr 195 | 196 | 197 | def test_multiple_files_in_place(tmpdir): 198 | """Unsorted files should be sorted in-place.""" 199 | paths_sorted = [ 200 | os.path.join(PATH_EXAMPLES, "sorted/from-toml-lang.toml"), 201 | os.path.join(PATH_EXAMPLES, "sorted/pyproject-weird-order.toml"), 202 | ] 203 | 204 | filenames_unsorted = [ 205 | "from-toml-lang.toml", 206 | "pyproject-weird-order.toml", 207 | ] 208 | temp_paths_unsorted = [] 209 | for filename in filenames_unsorted: 210 | orig_path = os.path.join(PATH_EXAMPLES, filename) 211 | temp_path = tmpdir / filename 212 | shutil.copy(orig_path, temp_path) 213 | temp_paths_unsorted.append(str(temp_path)) 214 | 215 | result = capture(["toml-sort", "--in-place"] + temp_paths_unsorted) 216 | assert result.returncode == 0, result.stderr 217 | 218 | for path_unsorted, path_sorted in zip(temp_paths_unsorted, paths_sorted): 219 | with open(path_unsorted, encoding="UTF-8") as file_unsorted: 220 | actual = file_unsorted.read() 221 | with open(path_sorted, encoding="UTF-8") as file_sorted: 222 | expected = file_sorted.read() 223 | assert actual == expected 224 | 225 | 226 | @pytest.mark.parametrize( 227 | "options", 228 | ( 229 | pytest.param( 230 | [], 231 | id="--check or --in-place must be specified", 232 | ), 233 | pytest.param( 234 | ["--check", "--output", "output.toml"], 235 | id="cannot specify output with --check", 236 | ), 237 | pytest.param( 238 | ["--in-place", "--output", "output.toml"], 239 | id="cannot specify output with --in-place", 240 | ), 241 | ), 242 | ) 243 | def test_multiple_files_and_errors(options): 244 | """Test errors if two or more files are given.""" 245 | paths = [ 246 | os.path.join(PATH_EXAMPLES, "from-toml-lang.toml"), 247 | os.path.join(PATH_EXAMPLES, "weird.toml"), 248 | ] 249 | result = capture(["toml-sort"] + options + paths) 250 | assert result.returncode == 1, result.stdout 251 | 252 | 253 | def test_load_config_file_read(): 254 | """Test no error if pyproject.toml cannot be read.""" 255 | with mock.patch("toml_sort.cli.open", side_effect=OSError): 256 | section = cli.load_pyproject() 257 | assert not cli.parse_config(section) 258 | 259 | 260 | @pytest.mark.parametrize( 261 | "toml,expected", 262 | [ 263 | ("", {}), 264 | ("[tool.other]\nfoo=2", {}), 265 | ("[tool.tomlsort]", {}), 266 | ("[tool.tomlsort]\nall=true", {"all": True}), 267 | ( 268 | """ 269 | [tool.tomlsort] 270 | all=true 271 | [tool.tomlsort.overrides] 272 | "a.b.c".inline_array = false 273 | """, 274 | {"all": True}, 275 | ), 276 | ( 277 | "[tool.tomlsort]\nspaces_before_inline_comment=4", 278 | {"spaces_before_inline_comment": 4}, 279 | ), 280 | ("[tool.tomlsort]\nsort_first=['x', 'y']", {"sort_first": "x,y"}), 281 | ], 282 | ) 283 | def test_load_config_file(toml, expected): 284 | """Test load_config_file.""" 285 | open_mock = mock.mock_open(read_data=toml) 286 | with mock.patch("toml_sort.cli.open", open_mock): 287 | section = cli.load_pyproject() 288 | assert cli.parse_config(section) == expected 289 | 290 | 291 | @pytest.mark.parametrize( 292 | "toml", ["[tool.tomlsort]\nunknown=2", "[tool.tomlsort]\nall=42"] 293 | ) 294 | def test_load_config_file_invalid(toml): 295 | """Test error if pyproject.toml is not valid.""" 296 | open_mock = mock.mock_open(read_data=toml) 297 | with mock.patch("toml_sort.cli.open", open_mock): 298 | with pytest.raises(SystemExit): 299 | section = cli.load_pyproject() 300 | cli.parse_config(section) 301 | 302 | 303 | @pytest.mark.parametrize( 304 | "toml,expected", 305 | [ 306 | ( 307 | """ 308 | [tool.tomlsort.overrides."a.b.c"] 309 | table_keys = false 310 | """, 311 | {"a.b.c": SortOverrideConfiguration(table_keys=False)}, 312 | ), 313 | ( 314 | """ 315 | [tool.tomlsort.overrides."test.123"] 316 | table_keys = false 317 | [tool.tomlsort.overrides."test.456"] 318 | inline_tables = false 319 | [tool.tomlsort.overrides."test.789"] 320 | inline_arrays = false 321 | """, 322 | { 323 | "test.123": SortOverrideConfiguration(table_keys=False), 324 | "test.456": SortOverrideConfiguration(inline_tables=False), 325 | "test.789": SortOverrideConfiguration(inline_arrays=False), 326 | }, 327 | ), 328 | ( 329 | """ 330 | [tool.tomlsort.overrides] 331 | "test.123".table_keys = false 332 | "test.456".inline_tables = false 333 | "test.789".inline_arrays = false 334 | """, 335 | { 336 | "test.123": SortOverrideConfiguration(table_keys=False), 337 | "test.456": SortOverrideConfiguration(inline_tables=False), 338 | "test.789": SortOverrideConfiguration(inline_arrays=False), 339 | }, 340 | ), 341 | ( 342 | """ 343 | [tool.tomlsort.overrides] 344 | "test.123".first = ["one", "two", "three"] 345 | """, 346 | { 347 | "test.123": SortOverrideConfiguration(first=["one", "two", "three"]), 348 | }, 349 | ), 350 | ], 351 | ) 352 | def test_load_config_overrides(toml, expected): 353 | """Turn settings tomldocument into SortOverrideConfiguration.""" 354 | open_mock = mock.mock_open(read_data=toml) 355 | with mock.patch("toml_sort.cli.open", open_mock): 356 | section = cli.load_pyproject() 357 | assert isinstance(section, dict) 358 | parsed = cli.parse_config_overrides(section) 359 | assert expected == parsed 360 | # Make sure we are returning normal python types rather 361 | # than TOMLKit types. 362 | for key, value in parsed.items(): 363 | assert type(key) is str 364 | assert type(value) is SortOverrideConfiguration 365 | assert type(value.first) is list 366 | 367 | 368 | @pytest.mark.parametrize( 369 | "toml", 370 | [ 371 | """ 372 | [tool.tomlsort.overrides."a.b.c"] 373 | unknown = false 374 | """, 375 | """ 376 | [tool.tomlsort.overrides."a.b.c"] 377 | table_keys = false 378 | inline_tables = false 379 | inline_arrays = false 380 | foo = "bar" 381 | """, 382 | ], 383 | ) 384 | def test_load_config_overrides_fail(toml): 385 | """parse_config_overrides exits if config contains unexpected key.""" 386 | open_mock = mock.mock_open(read_data=toml) 387 | with mock.patch("toml_sort.cli.open", open_mock): 388 | with pytest.raises(SystemExit): 389 | section = cli.load_pyproject() 390 | cli.parse_config_overrides(section) 391 | 392 | 393 | @pytest.mark.parametrize( 394 | "arg,expected_first,expected_overrides", 395 | [ 396 | ( 397 | "a.b.c.d", 398 | [], 399 | {"a.b.c": SortOverrideConfiguration(first=["d"])}, 400 | ), 401 | ( 402 | "a.b.c.d, a", 403 | ["a"], 404 | {"a.b.c": SortOverrideConfiguration(first=["d"])}, 405 | ), 406 | ], 407 | ) 408 | def test_parse_sort_first(arg, expected_first, expected_overrides): 409 | """Test that we correctly parse the arg given for sort-first.""" 410 | first, overrides = parse_sort_first(arg, {}) 411 | assert first == expected_first 412 | assert overrides == expected_overrides 413 | -------------------------------------------------------------------------------- /toml_sort/cli.py: -------------------------------------------------------------------------------- 1 | """Toml Sort command line interface.""" 2 | 3 | import argparse 4 | import dataclasses 5 | import sys 6 | from argparse import ArgumentParser 7 | from typing import Any, Dict, List, Optional, Tuple, Type, cast 8 | 9 | import tomlkit 10 | from tomlkit import TOMLDocument 11 | 12 | from .tomlsort import ( 13 | CommentConfiguration, 14 | FormattingConfiguration, 15 | SortConfiguration, 16 | SortOverrideConfiguration, 17 | TomlSort, 18 | ) 19 | 20 | __all__ = ["cli"] 21 | 22 | STD_STREAM = "-" # The standard stream 23 | ENCODING = "UTF-8" # Currently, we only support UTF-8 24 | 25 | 26 | def get_version() -> str: 27 | """Get the program version.""" 28 | from importlib.metadata import version 29 | 30 | return version("toml-sort") 31 | 32 | 33 | def printerr(arg: str) -> None: 34 | """Print to stderr.""" 35 | print(arg, file=sys.stderr) 36 | 37 | 38 | def read_file(path: str) -> str: 39 | """Read contents from a file.""" 40 | if path == STD_STREAM: 41 | return sys.stdin.read() 42 | with open(path, "r", encoding=ENCODING) as fileobj: 43 | return fileobj.read() 44 | 45 | 46 | def write_file(path: str, content: str) -> None: 47 | """Write content to a path.""" 48 | if path == STD_STREAM: 49 | print(content, end="") 50 | return 51 | with open(path, "w", encoding=ENCODING) as fileobj: 52 | fileobj.write(content) 53 | 54 | 55 | def validate_and_copy( 56 | data: Dict[str, Any], target: Dict[str, Any], key: str, type_: Type[Any] 57 | ) -> None: 58 | """Validate a configuration key.""" 59 | if key not in data: 60 | return 61 | if not isinstance(data[key], type_): 62 | printerr(f"Value of tool.tomlsort.{key} should be of type {type_}.") 63 | sys.exit(1) 64 | target[key] = data.pop(key) 65 | 66 | 67 | def load_pyproject() -> TOMLDocument: 68 | """Load pyproject file, and return tool.tomlsort section.""" 69 | try: 70 | with open("pyproject.toml", encoding="utf-8") as file: 71 | content = file.read() 72 | except OSError: 73 | return tomlkit.document() 74 | 75 | document = tomlkit.parse(content) 76 | tool_section = document.get("tool", tomlkit.document()) 77 | return cast(TOMLDocument, tool_section.get("tomlsort", tomlkit.document())) 78 | 79 | 80 | def parse_config(tomlsort_section: TOMLDocument) -> Dict[str, Any]: 81 | """Load the toml_sort configuration from a TOMLDocument.""" 82 | config = dict(tomlsort_section) 83 | 84 | # remove the overrides key, since it is parsed separately 85 | # in parse_config_overrides. 86 | config.pop("overrides", None) 87 | 88 | clean_config: Dict[str, Any] = {} 89 | validate_and_copy(config, clean_config, "all", bool) 90 | validate_and_copy(config, clean_config, "in_place", bool) 91 | validate_and_copy(config, clean_config, "no_header", bool) 92 | validate_and_copy(config, clean_config, "no_comments", bool) 93 | validate_and_copy(config, clean_config, "no_header_comments", bool) 94 | validate_and_copy(config, clean_config, "no_footer_comments", bool) 95 | validate_and_copy(config, clean_config, "no_inline_comments", bool) 96 | validate_and_copy(config, clean_config, "no_block_comments", bool) 97 | validate_and_copy(config, clean_config, "check", bool) 98 | validate_and_copy(config, clean_config, "ignore_case", bool) 99 | validate_and_copy(config, clean_config, "no_sort_tables", bool) 100 | validate_and_copy(config, clean_config, "sort_inline_tables", bool) 101 | validate_and_copy(config, clean_config, "sort_inline_arrays", bool) 102 | validate_and_copy(config, clean_config, "sort_table_keys", bool) 103 | validate_and_copy(config, clean_config, "spaces_before_inline_comment", int) 104 | validate_and_copy(config, clean_config, "spaces_indent_inline_array", int) 105 | validate_and_copy(config, clean_config, "trailing_comma_inline_array", bool) 106 | validate_and_copy(config, clean_config, "sort_first", list) 107 | if "sort_first" in clean_config: 108 | clean_config["sort_first"] = ",".join(clean_config["sort_first"]) 109 | 110 | if config: 111 | printerr(f"Unexpected configuration values: {config}") 112 | sys.exit(1) 113 | 114 | return clean_config 115 | 116 | 117 | def parse_config_overrides( 118 | tomlsort_section: TOMLDocument, 119 | ) -> Dict[str, SortOverrideConfiguration]: 120 | """Parse the tool.tomlsort.overrides section of the config.""" 121 | fields = dataclasses.fields(SortOverrideConfiguration) 122 | settings_definition = {field.name: field.type for field in fields} 123 | override_settings = tomlsort_section.get("overrides", tomlkit.document()).unwrap() 124 | 125 | overrides = {} 126 | for path, settings in override_settings.items(): 127 | if not settings.keys() <= settings_definition.keys(): 128 | unknown_settings = settings.keys() - settings_definition.keys() 129 | printerr("Unexpected configuration override settings:") 130 | for unknown_setting in unknown_settings: 131 | printerr(f' "{path}".{unknown_setting}') 132 | sys.exit(1) 133 | 134 | overrides[path] = SortOverrideConfiguration(**settings) 135 | 136 | return overrides 137 | 138 | 139 | def parse_sort_first( 140 | sort_first_arg: str, overrides: Dict[str, SortOverrideConfiguration] 141 | ) -> Tuple[List[str], Dict[str, SortOverrideConfiguration]]: 142 | """Parse the sort_first arg given on the command line. 143 | 144 | If a dotted key is given on the command line for the sort-first 145 | argument this function parses it, and adds it to the appropriate 146 | override. 147 | """ 148 | sort_first = [] 149 | for full_key in sort_first_arg.split(","): 150 | full_key = full_key.strip() 151 | split = full_key.rsplit(".", 1) 152 | if len(split) == 1: 153 | sort_first.append(full_key) 154 | else: 155 | path, base_key = split 156 | if path in overrides: 157 | overrides[path].first.append(base_key) 158 | else: 159 | sort_override = SortOverrideConfiguration(first=[base_key]) 160 | overrides[path] = sort_override 161 | 162 | return sort_first, overrides 163 | 164 | 165 | def get_parser(defaults: Dict[str, Any]) -> ArgumentParser: 166 | """Get the argument parser.""" 167 | parser = ArgumentParser( 168 | prog="toml-sort", 169 | formatter_class=argparse.RawDescriptionHelpFormatter, 170 | description="Toml sort: a sorting utility for toml files.", 171 | epilog="""\ 172 | Examples: 173 | 174 | - **Stdin -> Stdout**: cat input.toml | toml-sort 175 | - **Disk -> Disk**: toml-sort -o output.toml input.toml 176 | - **Linting**: toml-sort --check input.toml input2.toml input3.toml 177 | - **Inplace Disk**: toml-sort --in-place input.toml input2.toml 178 | 179 | Return codes: 180 | 181 | - 0 : success. 182 | - 1 : errors were found 183 | 184 | Notes: 185 | 186 | - You cannot redirect from a file to itself in Bash. POSIX shells process 187 | redirections first, then execute commands. --in-place exists for this 188 | reason 189 | """, 190 | ) 191 | parser.add_argument( 192 | "--version", 193 | help="display version information and exit", 194 | action="store_true", 195 | ) 196 | parser.add_argument( 197 | "-o", 198 | "--output", 199 | help=f"output filepath (default: '{STD_STREAM}')", 200 | type=str, 201 | ) 202 | parser.add_argument( 203 | "-i", 204 | "--in-place", 205 | help="overwrite the original input file with changes", 206 | action="store_true", 207 | ) 208 | sort = parser.add_argument_group("sort", "change sorting behavior") 209 | sort.add_argument( 210 | "-I", 211 | "--ignore-case", 212 | help="ignore case when sorting", 213 | action="store_true", 214 | ) 215 | sort.add_argument( 216 | "-a", 217 | "--all", 218 | help=( 219 | "sort ALL keys. This implies sort table-keys, inline-tables and " 220 | "inline arrays. (default: only sort non-inline 'tables and arrays " 221 | "of tables')" 222 | ), 223 | action="store_true", 224 | ) 225 | sort.add_argument( 226 | "--no-sort-tables", 227 | help=( 228 | "Disables the default behavior of sorting tables and arrays of " 229 | "tables by their header value. Setting this option will keep the " 230 | "order of tables in the toml file the same." 231 | ), 232 | action="store_true", 233 | ) 234 | sort.add_argument( 235 | "--sort-table-keys", 236 | help=( 237 | "Sort the keys in tables and arrays of tables (excluding inline " 238 | "tables and arrays)." 239 | ), 240 | action="store_true", 241 | ) 242 | sort.add_argument( 243 | "--sort-inline-tables", 244 | help=("Sort inline tables."), 245 | action="store_true", 246 | ) 247 | sort.add_argument( 248 | "--sort-inline-arrays", 249 | help=("Sort inline arrays."), 250 | action="store_true", 251 | ) 252 | sort.add_argument( 253 | "--sort-first", 254 | help=( 255 | "Table keys that will be sorted first in the output. Multiple " 256 | "keys can be given separated by a comma." 257 | ), 258 | metavar="KEYS", 259 | type=str, 260 | default="", 261 | ) 262 | comments = parser.add_argument_group("comments", "exclude comments from output") 263 | comments.add_argument( 264 | "--no-header", 265 | help="Deprecated. See --no-header-comments", 266 | action="store_true", 267 | ) 268 | comments.add_argument( 269 | "--no-comments", 270 | help=( 271 | "remove all comments. Implies no header, footer, inline, or block comments" 272 | ), 273 | action="store_true", 274 | ) 275 | comments.add_argument( 276 | "--no-header-comments", 277 | help="remove a document's leading comments", 278 | action="store_true", 279 | ) 280 | comments.add_argument( 281 | "--no-footer-comments", 282 | help="remove a document's trailing comments", 283 | action="store_true", 284 | ) 285 | comments.add_argument( 286 | "--no-inline-comments", 287 | help="remove a document's inline comments", 288 | action="store_true", 289 | ) 290 | comments.add_argument( 291 | "--no-block-comments", 292 | help="remove a document's block comments", 293 | action="store_true", 294 | ) 295 | formatting = parser.add_argument_group( 296 | "formatting", "options to change output formatting" 297 | ) 298 | formatting.add_argument( 299 | "--spaces-before-inline-comment", 300 | help=("the number of spaces before an inline comment (default: 1)"), 301 | type=int, 302 | choices=range(1, 5), 303 | default=1, 304 | ) 305 | formatting.add_argument( 306 | "--spaces-indent-inline-array", 307 | help=("the number of spaces to indent a multiline inline array (default: 2)"), 308 | type=int, 309 | choices=[2, 4, 6, 8], 310 | default=2, 311 | ) 312 | formatting.add_argument( 313 | "--trailing-comma-inline-array", 314 | help="add trailing comma to the last item in a multiline inline array", 315 | action="store_true", 316 | ) 317 | parser.add_argument( 318 | "--check", 319 | help=("silently check if an original file would be changed by the formatter"), 320 | action="store_true", 321 | ) 322 | parser.add_argument( 323 | "filenames", 324 | metavar="F", 325 | help=(f"filename(s) to be processed by toml-sort (default: {STD_STREAM})"), 326 | type=str, 327 | nargs="*", 328 | ) 329 | parser.set_defaults(**defaults) 330 | return parser 331 | 332 | 333 | def cli( # pylint: disable=too-many-branches,too-many-locals 334 | arguments: Optional[List[str]] = None, 335 | ) -> None: 336 | """Toml sort cli implementation.""" 337 | settings = load_pyproject() 338 | configuration = parse_config(settings) 339 | configuration_overrides = parse_config_overrides(settings) 340 | args = get_parser(configuration).parse_args(args=arguments) # strip command itself 341 | if args.version: 342 | print(get_version()) 343 | sys.exit(0) 344 | sort_first, configuration_overrides = parse_sort_first( 345 | args.sort_first, configuration_overrides 346 | ) 347 | 348 | filenames_clean = args.filenames if args.filenames else (STD_STREAM,) 349 | usage_errors = [] 350 | 351 | if len(filenames_clean) > 1: 352 | if not (args.in_place or args.check): 353 | usage_errors.append( 354 | "'--check' or '--in-place' required if using 2+ FILENAME args" 355 | ) 356 | if args.output is not None: 357 | usage_errors.append("'--output' not allowed with 2+ FILENAME args") 358 | if args.in_place and STD_STREAM in filenames_clean: 359 | usage_errors.append( 360 | f"'--in-place' not allowed with stdin FILENAME '{STD_STREAM}'" 361 | ) 362 | if args.in_place and args.output is not None: 363 | usage_errors.append("'--output' and '--in-place' cannot be used together") 364 | 365 | if usage_errors: 366 | printerr("Usage error(s):") 367 | for errno, usage_error in enumerate(usage_errors): 368 | printerr(f"{errno + 1}. {usage_error}") 369 | sys.exit(1) 370 | 371 | output_clean = args.output if args.output is not None else STD_STREAM 372 | check_failures = [] 373 | 374 | for filename in filenames_clean: 375 | original_toml = read_file(filename) 376 | sorted_toml = TomlSort( 377 | input_toml=original_toml, 378 | comment_config=CommentConfiguration( 379 | header=not bool( 380 | args.no_header or args.no_header_comments or args.no_comments 381 | ), 382 | footer=not bool(args.no_footer_comments or args.no_comments), 383 | block=not bool(args.no_block_comments or args.no_comments), 384 | inline=not bool(args.no_inline_comments or args.no_comments), 385 | ), 386 | sort_config=SortConfiguration( 387 | ignore_case=args.ignore_case, 388 | tables=not bool(args.no_sort_tables), 389 | table_keys=bool(args.sort_table_keys or args.all), 390 | inline_tables=bool(args.sort_inline_tables or args.all), 391 | inline_arrays=bool(args.sort_inline_arrays or args.all), 392 | first=sort_first, 393 | ), 394 | format_config=FormattingConfiguration( 395 | spaces_before_inline_comment=args.spaces_before_inline_comment, 396 | spaces_indent_inline_array=args.spaces_indent_inline_array, 397 | trailing_comma_inline_array=args.trailing_comma_inline_array, 398 | ), 399 | sort_config_overrides=configuration_overrides, 400 | ).sorted() 401 | if args.check: 402 | if original_toml != sorted_toml: 403 | check_failures.append(filename) 404 | elif args.in_place: 405 | if original_toml != sorted_toml: 406 | write_file(filename, sorted_toml) 407 | elif len(filenames_clean) == 1: 408 | write_file(output_clean, sorted_toml) 409 | else: 410 | printerr("Uncaught error. Please submit GitHub issue:") 411 | printerr("") 412 | sys.exit(1) 413 | 414 | if args.check and check_failures: 415 | printerr(f"{len(check_failures)} check failure(s):") 416 | for check_failure in check_failures: 417 | printerr(f" - {check_failure}") 418 | sys.exit(1) 419 | -------------------------------------------------------------------------------- /toml_sort/tomlsort.py: -------------------------------------------------------------------------------- 1 | """Utility functions and classes to sort toml text.""" 2 | 3 | from __future__ import annotations 4 | 5 | import fnmatch 6 | import itertools 7 | import re 8 | from dataclasses import asdict, dataclass, field 9 | from typing import ( 10 | Any, 11 | Dict, 12 | Iterable, 13 | List, 14 | Optional, 15 | Tuple, 16 | TypeVar, 17 | Union, 18 | cast, 19 | ) 20 | 21 | import tomlkit 22 | from tomlkit.api import ws 23 | from tomlkit.container import Container 24 | from tomlkit.items import ( 25 | AoT, 26 | Array, 27 | Comment, 28 | InlineTable, 29 | Item, 30 | Key, 31 | Null, 32 | Table, 33 | Trivia, 34 | Whitespace, 35 | _ArrayItemGroup, 36 | ) 37 | from tomlkit.items import item as tomlkit_item 38 | from tomlkit.toml_document import TOMLDocument 39 | 40 | __all__ = ["TomlSort"] 41 | 42 | 43 | def clean_toml_text(input_toml: str) -> str: 44 | """Clean input toml, increasing the chance for beautiful output.""" 45 | cleaned = re.sub(r"[\r\n][\r\n]{2,}", "\n\n", input_toml) 46 | return "\n" + cleaned.strip() + "\n" 47 | 48 | 49 | def convert_tomlkit_buggy_types(in_value: Any, parent: Any, key: str) -> Item: 50 | """Fix buggy items while iterating through tomlkit items. 51 | 52 | Iterating through tomlkit items can often be buggy; this function 53 | fixes it 54 | """ 55 | if isinstance(in_value, bool): 56 | # Bool items don't have trivia and are resolved to Python's `bool` 57 | # https://github.com/sdispater/tomlkit/issues/119#issuecomment-955840942 58 | try: 59 | return cast(Item, parent.value.item(key)) 60 | except AttributeError: 61 | return cast(Item, tomlkit_item(parent.value[key], parent)) 62 | if isinstance(in_value, Item): 63 | return in_value 64 | if isinstance(in_value, Container): 65 | return Table(in_value, trivia=Trivia(), is_aot_element=False) 66 | return cast(Item, tomlkit_item(in_value, parent)) 67 | 68 | 69 | def attach_comments(item: TomlSortItem, previous_item: Table | TOMLDocument) -> None: 70 | """Attach comments to previous item and formatting tables.""" 71 | if item.attached_comments and isinstance(item.value, Table): 72 | previous_item.add(Whitespace("\n")) 73 | item.value.trivia.indent = "" 74 | for comment in item.attached_comments: 75 | previous_item.add(comment) 76 | 77 | 78 | T = TypeVar("T", bound=Item) 79 | 80 | 81 | def format_comment(comment: str) -> str: 82 | """Reformats a comment string removing extra whitespace.""" 83 | return f"# {comment[1:].strip()}".strip() 84 | 85 | 86 | def normalize_trivia( 87 | item: T, include_comments: bool = True, comment_spaces: int = 2 88 | ) -> T: 89 | """Normalize trivia, de-indenting. 90 | 91 | If it has a comment, the comment is correctly formatted. 92 | """ 93 | if not isinstance(item, Whitespace): 94 | trivia = item.trivia 95 | trivia.indent = "" 96 | trivia.trail = "\n" 97 | if trivia.comment != "": 98 | if include_comments: 99 | trivia.comment_ws = " " * comment_spaces 100 | trivia.comment = format_comment(trivia.comment) 101 | else: 102 | trivia.comment = "" 103 | trivia.comment_ws = "" 104 | return item 105 | 106 | 107 | def coalesce_tables( 108 | tables: Iterable[TomlSortItem], 109 | ) -> Iterable[TomlSortItem]: 110 | """Merge any duplicate keys that exist in an iterable of TomlSortItem.""" 111 | coalesced = {} 112 | for table in tables: 113 | if table.keys.base not in coalesced: 114 | coalesced[table.keys.base] = table 115 | else: 116 | existing = coalesced[table.keys.base] 117 | existing.children.extend(table.children) 118 | existing.attached_comments.extend(table.attached_comments) 119 | if isinstance(existing.value, Table): 120 | if existing.value.is_super_table(): 121 | existing.value = table.value 122 | return coalesced.values() 123 | 124 | 125 | class TomlSortKeys: 126 | """Keeps track of the Keys for a particular TomlSortItem. 127 | 128 | We use this to keep track of the full path of an item so that we can 129 | find the configuration overrides that apply to it. 130 | """ 131 | 132 | keys: List[Key] 133 | 134 | def __init__(self, keys: Union[List[Key], Key]): 135 | if isinstance(keys, Key): 136 | self.keys = [keys] 137 | else: 138 | self.keys = keys 139 | 140 | @property 141 | def base(self) -> Key: 142 | """Returns the last key segment. 143 | 144 | For example: would return test for this.is.a.test 145 | """ 146 | return self.keys[-1] 147 | 148 | @base.setter 149 | def base(self, value: Key) -> None: 150 | """Setter for the base property.""" 151 | self.keys[-1] = value 152 | 153 | def as_string(self) -> str: 154 | """Returns the full set of keys as a string.""" 155 | return ".".join(k.key for k in self.keys) 156 | 157 | def __add__(self, other: Union[TomlSortKeys, Key]) -> TomlSortKeys: 158 | """Combine TomlSortKeys object with either Key or TomlSortKeys.""" 159 | if isinstance(other, Key): 160 | keys = [other] 161 | else: 162 | keys = other.keys 163 | return TomlSortKeys(self.keys + keys) 164 | 165 | def __repr__(self) -> str: 166 | """Representation of TomlSortKeys.""" 167 | return f"<{self.__class__.__name__}: {self.as_string()}>" 168 | 169 | 170 | @dataclass 171 | class TomlSortItem: 172 | """Track comments attached to sorted Toml Items.""" 173 | 174 | keys: TomlSortKeys 175 | value: Item 176 | attached_comments: List[Comment] = field(default_factory=list) 177 | children: List[TomlSortItem] = field(default_factory=list) 178 | 179 | @property 180 | def is_table(self) -> bool: 181 | """Returns true if the Item inside this TomlSortItem is a Table.""" 182 | return isinstance(self.value, Table) 183 | 184 | @property 185 | def is_super_table(self) -> bool: 186 | """True if Item in TomlSortItem is a Super Table.""" 187 | return ( # pylint: disable=no-member 188 | self.is_table and cast(Table, self.value).is_super_table() 189 | ) 190 | 191 | @property 192 | def table(self) -> Table: 193 | """TomlSortItem.value if Table, otherwise TypeError.""" 194 | if not self.is_table: 195 | raise TypeError(f"self.value is {str(type(self.value))} not a Table") 196 | return cast(Table, self.value) 197 | 198 | @property 199 | def is_aot(self) -> bool: 200 | """Returns true if the Item inside this TomlSortItem is an AoT.""" 201 | return isinstance(self.value, AoT) 202 | 203 | @property 204 | def aot(self) -> AoT: 205 | """TomlSortItem.value if AoT, otherwise TypeError.""" 206 | if not self.is_aot: 207 | raise TypeError(f"self.value is {str(type(self.value))} not an AoT") 208 | return cast(AoT, self.value) 209 | 210 | 211 | @dataclass 212 | class CommentConfiguration: 213 | """Configures how TomlSort handles comments.""" 214 | 215 | header: bool = True 216 | footer: bool = True 217 | inline: bool = True 218 | block: bool = True 219 | 220 | 221 | @dataclass 222 | class SortConfiguration: 223 | """Configures how TomlSort sorts the input toml.""" 224 | 225 | tables: bool = True 226 | table_keys: bool = True 227 | inline_tables: bool = False 228 | inline_arrays: bool = False 229 | ignore_case: bool = False 230 | first: List[str] = field(default_factory=list) 231 | 232 | 233 | @dataclass 234 | class FormattingConfiguration: 235 | """Configures how TomlSort formats its output.""" 236 | 237 | spaces_before_inline_comment: int = 2 238 | spaces_indent_inline_array: int = 2 239 | trailing_comma_inline_array: bool = False 240 | 241 | 242 | @dataclass 243 | class SortOverrideConfiguration: 244 | """Configures overrides to sort configuration for a particular key.""" 245 | 246 | table_keys: Optional[bool] = None 247 | inline_tables: Optional[bool] = None 248 | inline_arrays: Optional[bool] = None 249 | first: List[str] = field(default_factory=list) 250 | 251 | 252 | class TomlSort: 253 | """API to manage sorting toml files.""" 254 | 255 | def __init__( # pylint: disable=too-many-arguments 256 | self, 257 | input_toml: str, 258 | comment_config: Optional[CommentConfiguration] = None, 259 | sort_config: Optional[SortConfiguration] = None, 260 | format_config: Optional[FormattingConfiguration] = None, 261 | sort_config_overrides: Optional[Dict[str, SortOverrideConfiguration]] = None, 262 | ) -> None: 263 | """Initializer.""" 264 | self.input_toml = input_toml 265 | 266 | if comment_config is None: 267 | comment_config = CommentConfiguration() 268 | self.comment_config = comment_config 269 | 270 | if sort_config is None: 271 | sort_config = SortConfiguration() 272 | self._sort_config = sort_config 273 | 274 | if format_config is None: 275 | format_config = FormattingConfiguration() 276 | self.format_config = format_config 277 | 278 | if sort_config_overrides is None: 279 | sort_config_overrides = {} 280 | self.sort_config_overrides = sort_config_overrides 281 | 282 | def _find_config_override( 283 | self, keys: Optional[TomlSortKeys] 284 | ) -> Optional[SortOverrideConfiguration]: 285 | """SortOverrideConfiguration a particular TomlSortKeys. 286 | 287 | If None exists returns None. 288 | 289 | Override matches are evaluated as glob patterns by the python 290 | fnmatch function. If there are multiple matches, return the 291 | exact match first otherwise return the first match. 292 | """ 293 | if keys is None: 294 | return None 295 | 296 | if keys.as_string() in self.sort_config_overrides: 297 | return self.sort_config_overrides.get(keys.as_string()) 298 | 299 | matches = [ 300 | config 301 | for pattern, config in self.sort_config_overrides.items() 302 | if fnmatch.fnmatch(keys.as_string(), pattern) 303 | ] 304 | 305 | if len(matches) > 0: 306 | return matches[0] 307 | 308 | return None 309 | 310 | def sort_config(self, keys: Optional[TomlSortKeys] = None) -> SortConfiguration: 311 | """Returns the SortConfiguration to use for particular TomlSortKeys. 312 | 313 | This merges the global SortConfiguration with any matching 314 | SortOverrideConfiguration to give the full SortConfiguration 315 | that applies to this Key. 316 | """ 317 | override = self._find_config_override(keys) 318 | if override is None: 319 | return self._sort_config 320 | 321 | main_config = asdict(self._sort_config) 322 | override_config = asdict(override) 323 | merged_config = {} 324 | 325 | for key, value in main_config.items(): 326 | if key in override_config and override_config[key] is not None: 327 | merged_config[key] = override_config[key] 328 | else: 329 | merged_config[key] = value 330 | 331 | return SortConfiguration(**merged_config) 332 | 333 | def sort_array( 334 | self, keys: TomlSortKeys, array: Array, indent_depth: int = 0 335 | ) -> Array: 336 | """Sort and format an inline array item while preserving comments.""" 337 | multiline = "\n" in array.as_string() 338 | indent_size = self.format_config.spaces_indent_inline_array 339 | indent = "\n" + " " * indent_size * (indent_depth + 1) if multiline else "" 340 | comma = "," if multiline else ", " 341 | 342 | comments: List[_ArrayItemGroup] = [] 343 | new_array_items = [] 344 | for array_item in array._value: # pylint: disable=protected-access 345 | if isinstance(array_item.value, Null) and isinstance( 346 | array_item.comment, Comment 347 | ): 348 | # Previous comments are orphaned if there is whitespace 349 | if ( 350 | array_item.indent is not None 351 | and "\n\n" in array_item.indent.as_string() 352 | ): 353 | comments = [] 354 | 355 | # Comment on its own line within the array 356 | array_item.indent = Whitespace(indent) 357 | array_item.comma = Whitespace("") 358 | array_item.comment.trivia.comment = format_comment( 359 | array_item.comment.trivia.comment 360 | ) 361 | comments.append(array_item) 362 | elif array_item.value is not None and not isinstance( 363 | array_item.value, Null 364 | ): 365 | # Actual array item 366 | array_item.indent = Whitespace(indent) 367 | array_item.comma = Whitespace(comma) 368 | if array_item.comment is not None: 369 | if self.comment_config.inline: 370 | array_item.comment.trivia.comment = format_comment( 371 | array_item.comment.trivia.comment 372 | ) 373 | array_item.comment.trivia.indent = ( 374 | " " * self.format_config.spaces_before_inline_comment 375 | ) 376 | else: 377 | array_item.comment = None 378 | new_array_items.append((array_item, comments)) 379 | comments = [] 380 | array_item.value = self.sort_item( 381 | keys, 382 | array_item.value, 383 | indent_depth=indent_depth + 1 if multiline else indent_depth, 384 | ) 385 | 386 | if self.sort_config(keys).inline_arrays: 387 | new_array_items = sorted(new_array_items, key=self.array_sort_func) 388 | new_array_value = [] 389 | for array_item, comments in new_array_items: 390 | if comments and self.comment_config.block: 391 | new_array_value.extend(comments) 392 | new_array_value.append(array_item) 393 | 394 | if len(new_array_value) != 0 and not ( 395 | multiline and self.format_config.trailing_comma_inline_array 396 | ): 397 | new_array_value[-1].comma = Whitespace("") 398 | 399 | if multiline: 400 | array_item = _ArrayItemGroup() 401 | array_item.value = Whitespace("\n" + " " * indent_size * indent_depth) 402 | new_array_value.append(array_item) 403 | 404 | array._value = new_array_value # pylint: disable=protected-access 405 | array._reindex() # pylint: disable=protected-access 406 | array = normalize_trivia( 407 | array, 408 | include_comments=self.comment_config.inline, 409 | comment_spaces=self.format_config.spaces_before_inline_comment, 410 | ) 411 | return array 412 | 413 | def sort_item(self, keys: TomlSortKeys, item: Item, indent_depth: int = 0) -> Item: 414 | """Sort item, recursing down for inline tables and arrays.""" 415 | if isinstance(item, Array): 416 | return self.sort_array(keys, item, indent_depth=indent_depth) 417 | 418 | if isinstance(item, InlineTable): 419 | return self.sort_inline_table(keys, item, indent_depth=indent_depth) 420 | 421 | return item 422 | 423 | def sort_keys( 424 | self, items: Iterable[TomlSortItem], sort_config: SortConfiguration 425 | ) -> List[TomlSortItem]: 426 | """Sorts Iterable of Tomlsort item based on keys. 427 | 428 | The sort respects the sort_config.first setting which allows 429 | overriding the sorted order of keys. 430 | """ 431 | 432 | def sort_first(item: TomlSortItem) -> int: 433 | for index, value in enumerate(sort_config.first): 434 | if value == item.keys.base.key: 435 | return index 436 | return len(sort_config.first) 437 | 438 | items = sorted(items, key=self.key_sort_func) 439 | items = sorted(items, key=sort_first) 440 | return items 441 | 442 | def sort_inline_table( 443 | self, keys: TomlSortKeys, item: Item, indent_depth: int = 0 444 | ) -> InlineTable: 445 | """Sort an inline table, recursing into its items.""" 446 | tomlsort_items = [ 447 | TomlSortItem( 448 | keys=keys + k, 449 | value=self.sort_item(keys + k, v, indent_depth=indent_depth), 450 | ) 451 | for k, v in item.value.body 452 | if not isinstance(v, Whitespace) and k is not None 453 | ] 454 | sort_config = self.sort_config(keys) 455 | if sort_config.inline_tables: 456 | tomlsort_items = self.sort_keys(tomlsort_items, sort_config) 457 | new_table = InlineTable(Container(parsed=True), trivia=item.trivia, new=True) 458 | for tomlsort_item in tomlsort_items: 459 | normalize_trivia(tomlsort_item.value, include_comments=False) 460 | new_table.append( 461 | self.format_key(tomlsort_item.keys.base), tomlsort_item.value 462 | ) 463 | new_table = normalize_trivia( 464 | new_table, 465 | include_comments=self.comment_config.inline, 466 | comment_spaces=self.format_config.spaces_before_inline_comment, 467 | ) 468 | return new_table 469 | 470 | @staticmethod 471 | def format_key(key: Key) -> Key: 472 | """Format a key, removing any extra whitespace. 473 | 474 | Make sure that it will be formatted like: key = value with one space on 475 | either side of the equal sign. For dotted keys, preserve the dotted format. 476 | """ 477 | # Only set separator for non-dotted keys 478 | if not key.is_dotted(): 479 | key.sep = " = " 480 | key._original = ( # pylint: disable=protected-access 481 | key.as_string().strip() 482 | ) 483 | return key 484 | 485 | def sort_items(self, items: Iterable[TomlSortItem]) -> Iterable[TomlSortItem]: 486 | """Sort an iterable full of TomlSortItem. 487 | 488 | Make sure the key is correctly formatted and recursing into any 489 | sub-items. 490 | """ 491 | for item in items: 492 | item.keys.base = self.format_key(item.keys.base) 493 | item.value = self.sort_item(item.keys, item.value) 494 | return items 495 | 496 | def key_sort_func(self, value: TomlSortItem) -> str: 497 | """Sort function that looks at TomlSortItems keys. 498 | 499 | Respects the configured value for ignore_case. 500 | """ 501 | key = value.keys.base.key 502 | if self.sort_config().ignore_case: 503 | key = key.lower() 504 | return key 505 | 506 | def array_sort_func(self, value: Tuple[_ArrayItemGroup, Any]) -> str: 507 | """Sorts on the .value member of an ArrayItemGroup. 508 | 509 | Respects the class setting for ignore_case. 510 | """ 511 | if value[0].value is None: 512 | return "" 513 | ret = value[0].value.as_string() 514 | if self.sort_config().ignore_case: 515 | ret = ret.lower() 516 | return ret 517 | 518 | def sorted_children_table( 519 | self, parent_keys: Optional[TomlSortKeys], parent: List[TomlSortItem] 520 | ) -> Iterable[TomlSortItem]: 521 | """Get the sorted children of a table.""" 522 | sort_config = self.sort_config(parent_keys) 523 | tables = coalesce_tables( 524 | item for item in parent if isinstance(item.value, (Table, AoT)) 525 | ) 526 | non_tables = self.sort_items( 527 | [item for item in parent if not isinstance(item.value, (Table, AoT))] 528 | ) 529 | non_tables_final = ( 530 | self.sort_keys(non_tables, sort_config) 531 | if sort_config.table_keys 532 | else non_tables 533 | ) 534 | tables_final = ( 535 | self.sort_keys(tables, sort_config) 536 | if self.sort_config(parent_keys).tables 537 | else tables 538 | ) 539 | return itertools.chain(non_tables_final, tables_final) 540 | 541 | def write_header_comment( 542 | self, 543 | from_doc_body: List[Tuple[Optional[Key], Item]], 544 | to_doc: TOMLDocument, 545 | ) -> List[Tuple[Optional[Key], Item]]: 546 | """Write header comment from the FROM doc to the TO doc. 547 | 548 | Only writes comments / whitespace from the beginning of a TOML 549 | document. 550 | """ 551 | # Discard leading whitespace 552 | while len(from_doc_body) > 0 and isinstance(from_doc_body[0][1], Whitespace): 553 | from_doc_body.pop(0) 554 | 555 | # Remove the header comment from the input document, adding it to 556 | # the output document, followed by a newline. 557 | spaces = self.format_config.spaces_before_inline_comment 558 | while len(from_doc_body) > 0 and isinstance(from_doc_body[0][1], Comment): 559 | _, value = from_doc_body.pop(0) 560 | value = normalize_trivia( 561 | value, 562 | comment_spaces=spaces, 563 | ) 564 | to_doc.add(value) 565 | 566 | to_doc.add(ws("\n")) 567 | return from_doc_body 568 | 569 | def toml_elements_sorted( 570 | self, original: TomlSortItem, parent: Table | TOMLDocument 571 | ) -> Item: 572 | """Returns a sorted item, recursing collections to their base.""" 573 | if original.is_table: 574 | new_table = original.table 575 | 576 | for item in self.sorted_children_table(original.keys, original.children): 577 | previous_item = self.table_previous_item(new_table, parent) 578 | attach_comments(item, previous_item) 579 | new_table.add( 580 | item.keys.base, 581 | self.toml_elements_sorted(item, previous_item), 582 | ) 583 | return new_table 584 | 585 | if original.is_aot: 586 | new_aot = normalize_trivia( 587 | original.aot, 588 | self.comment_config.inline, 589 | self.format_config.spaces_before_inline_comment, 590 | ) 591 | for table in original.children: 592 | previous_item = next(iter(new_aot), parent) 593 | attach_comments(table, previous_item) 594 | new_aot.append( 595 | self.toml_elements_sorted(table, next(iter(new_aot), previous_item)) 596 | ) 597 | 598 | return new_aot 599 | 600 | return original.value 601 | 602 | @staticmethod 603 | def table_previous_item( 604 | parent_table: Table, 605 | grandparent: Table | TOMLDocument, 606 | ) -> Table | TOMLDocument: 607 | """Finds the previous item that we should attach a comment to. 608 | 609 | Specifically, in the case where the previous item is a table. 610 | 611 | This take into account that a table may be a super table. 612 | """ 613 | if parent_table.is_super_table(): 614 | if len(parent_table) == 0: 615 | return grandparent 616 | last_item = parent_table.value.last_item() 617 | if isinstance(last_item, Table): 618 | return last_item 619 | return parent_table 620 | 621 | def body_to_tomlsortitems( 622 | self, 623 | parent: List[Tuple[Optional[Key], Item]], 624 | parent_key: Optional[TomlSortKeys] = None, 625 | ) -> Tuple[List[TomlSortItem], List[Comment]]: 626 | """Iterate over Container.body, recursing down into sub-containers. 627 | 628 | Attaches the comments that are found to the correct TomlSortItem. We 629 | need to do this iteration because TomlKit puts comments into end of the 630 | collection they appear in, instead of the start of the next collection. 631 | 632 | For example 633 | 634 | ```toml 635 | [xyz] 636 | 637 | # Comment 638 | [abc] 639 | ``` 640 | 641 | TomlKit would place the comment from the example into the [xyz] 642 | collection, when we would like it to be attached to the [abc] 643 | collection. 644 | 645 | So before sorting we have to iterate over the container, correctly 646 | attaching the comments, then undo this process once everything is 647 | sorted. 648 | """ 649 | items: List[TomlSortItem] = [] 650 | comments: List[Comment] = [] 651 | for key, value in parent: 652 | if key is None: 653 | if isinstance(value, Whitespace): 654 | comments = [] 655 | elif isinstance(value, Comment) and self.comment_config.block: 656 | comment_spaces = self.format_config.spaces_before_inline_comment 657 | value = normalize_trivia( 658 | value, 659 | comment_spaces=comment_spaces, 660 | ) 661 | comments.append(value) 662 | continue 663 | 664 | value = convert_tomlkit_buggy_types(value, parent, key.key) 665 | value = normalize_trivia( 666 | value, 667 | self.comment_config.inline, 668 | comment_spaces=self.format_config.spaces_before_inline_comment, 669 | ) 670 | full_key = parent_key + key if parent_key else TomlSortKeys(key) 671 | 672 | if isinstance(value, Table): 673 | comments, item = self.table_to_tomlsortitem(comments, full_key, value) 674 | 675 | elif isinstance(value, AoT): 676 | comments, item = self.aot_to_tomlsortitem(comments, full_key, value) 677 | 678 | elif isinstance(value, Item): 679 | item = TomlSortItem(full_key, value, comments) 680 | comments = [] 681 | 682 | else: 683 | raise TypeError( 684 | "Invalid TOML; " + str(type(value)) + " is not an Item." 685 | ) 686 | 687 | items.append(item) 688 | 689 | return items, comments 690 | 691 | def aot_to_tomlsortitem( 692 | self, comments: List[Comment], keys: TomlSortKeys, value: AoT 693 | ) -> Tuple[List[Comment], TomlSortItem]: 694 | """Turn an AoT into a TomlSortItem. 695 | 696 | Recurses down through its collections and attaching all the comments to 697 | the correct items. 698 | """ 699 | new_aot = AoT([], parsed=True) 700 | children = [] 701 | for table in value.body: 702 | [first_child], trailing_comments = self.body_to_tomlsortitems( 703 | [(keys.base, table)] 704 | ) 705 | first_child.attached_comments = comments 706 | comments = trailing_comments 707 | children.append(first_child) 708 | item = TomlSortItem(keys, new_aot, children=children) 709 | return comments, item 710 | 711 | def table_to_tomlsortitem( 712 | self, comments: List[Comment], keys: TomlSortKeys, value: Table 713 | ) -> Tuple[List[Comment], TomlSortItem]: 714 | """Turn a table into a TomlSortItem. 715 | 716 | Recurses down through its collections and attaching all the comments to 717 | the correct items. 718 | """ 719 | children, trailing_comments = self.body_to_tomlsortitems( 720 | value.value.body, parent_key=keys 721 | ) 722 | new_table = Table( 723 | Container(parsed=True), 724 | trivia=value.trivia, 725 | is_aot_element=value.is_aot_element(), 726 | is_super_table=value.is_super_table(), 727 | ) 728 | if not value.is_super_table(): 729 | new_table.trivia.indent = "\n" 730 | 731 | first_child = next(iter(children), None) 732 | 733 | # If the first child of this item is an AoT, we want the 734 | # comment to be attached to the first table within the AoT, 735 | # rather than the parent AoT object 736 | if first_child and first_child.is_aot: 737 | first_child.children[0].attached_comments = comments 738 | comments = [] 739 | 740 | # If this item is a super table we want to walk down 741 | # the tree and attach the comment to the first non-super table. 742 | if value.is_super_table(): 743 | child_table = children[0] 744 | while child_table.is_super_table: 745 | child_table = child_table.children[0] 746 | 747 | child_table.attached_comments = comments 748 | comments = [] 749 | 750 | item = TomlSortItem(keys, new_table, comments, children) 751 | comments = trailing_comments 752 | return comments, item 753 | 754 | def toml_doc_sorted(self, original: TOMLDocument) -> TOMLDocument: 755 | """Sort a TOMLDocument.""" 756 | sorted_document = TOMLDocument(parsed=True) 757 | 758 | original_body = original.body 759 | if self.comment_config.header: 760 | original_body = self.write_header_comment(original_body, sorted_document) 761 | 762 | items, footer_comment = self.body_to_tomlsortitems(original_body) 763 | 764 | for item in self.sorted_children_table(None, items): 765 | attach_comments(item, sorted_document) 766 | sorted_document.add( 767 | item.keys.base, 768 | self.toml_elements_sorted(item, sorted_document), 769 | ) 770 | 771 | if self.comment_config.footer and footer_comment: 772 | sorted_document.add(Whitespace("\n")) 773 | for comment in footer_comment: 774 | sorted_document.add(comment) 775 | 776 | return sorted_document 777 | 778 | def sorted(self) -> str: 779 | """Sort a TOML string.""" 780 | clean_toml = clean_toml_text(self.input_toml) 781 | toml_doc = tomlkit.parse(clean_toml) 782 | sorted_toml = self.toml_doc_sorted(toml_doc) 783 | return clean_toml_text(tomlkit.dumps(sorted_toml)).strip() + "\n" 784 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "argcomplete" 5 | version = "3.6.2" 6 | description = "Bash tab completion for argparse" 7 | optional = false 8 | python-versions = ">=3.8" 9 | groups = ["dev"] 10 | files = [ 11 | {file = "argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591"}, 12 | {file = "argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf"}, 13 | ] 14 | 15 | [package.extras] 16 | test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] 17 | 18 | [[package]] 19 | name = "attrs" 20 | version = "25.3.0" 21 | description = "Classes Without Boilerplate" 22 | optional = false 23 | python-versions = ">=3.8" 24 | groups = ["dev"] 25 | files = [ 26 | {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, 27 | {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, 28 | ] 29 | 30 | [package.extras] 31 | benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 32 | cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 33 | dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 34 | docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] 35 | tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 36 | tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] 37 | 38 | [[package]] 39 | name = "colorama" 40 | version = "0.4.6" 41 | description = "Cross-platform colored terminal text." 42 | optional = false 43 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 44 | groups = ["dev"] 45 | markers = "sys_platform == \"win32\"" 46 | files = [ 47 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 48 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 49 | ] 50 | 51 | [[package]] 52 | name = "colorlog" 53 | version = "6.9.0" 54 | description = "Add colours to the output of Python's logging module." 55 | optional = false 56 | python-versions = ">=3.6" 57 | groups = ["dev"] 58 | files = [ 59 | {file = "colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff"}, 60 | {file = "colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2"}, 61 | ] 62 | 63 | [package.dependencies] 64 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 65 | 66 | [package.extras] 67 | development = ["black", "flake8", "mypy", "pytest", "types-colorama"] 68 | 69 | [[package]] 70 | name = "coverage" 71 | version = "7.10.6" 72 | description = "Code coverage measurement for Python" 73 | optional = false 74 | python-versions = ">=3.9" 75 | groups = ["dev"] 76 | files = [ 77 | {file = "coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356"}, 78 | {file = "coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301"}, 79 | {file = "coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460"}, 80 | {file = "coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd"}, 81 | {file = "coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb"}, 82 | {file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6"}, 83 | {file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945"}, 84 | {file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e"}, 85 | {file = "coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1"}, 86 | {file = "coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528"}, 87 | {file = "coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f"}, 88 | {file = "coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc"}, 89 | {file = "coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a"}, 90 | {file = "coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a"}, 91 | {file = "coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62"}, 92 | {file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153"}, 93 | {file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5"}, 94 | {file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619"}, 95 | {file = "coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba"}, 96 | {file = "coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e"}, 97 | {file = "coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c"}, 98 | {file = "coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea"}, 99 | {file = "coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634"}, 100 | {file = "coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6"}, 101 | {file = "coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9"}, 102 | {file = "coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c"}, 103 | {file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a"}, 104 | {file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5"}, 105 | {file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972"}, 106 | {file = "coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d"}, 107 | {file = "coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629"}, 108 | {file = "coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80"}, 109 | {file = "coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6"}, 110 | {file = "coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80"}, 111 | {file = "coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003"}, 112 | {file = "coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27"}, 113 | {file = "coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4"}, 114 | {file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d"}, 115 | {file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc"}, 116 | {file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc"}, 117 | {file = "coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e"}, 118 | {file = "coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32"}, 119 | {file = "coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2"}, 120 | {file = "coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b"}, 121 | {file = "coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393"}, 122 | {file = "coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27"}, 123 | {file = "coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df"}, 124 | {file = "coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb"}, 125 | {file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282"}, 126 | {file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4"}, 127 | {file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21"}, 128 | {file = "coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0"}, 129 | {file = "coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5"}, 130 | {file = "coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b"}, 131 | {file = "coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e"}, 132 | {file = "coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb"}, 133 | {file = "coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034"}, 134 | {file = "coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1"}, 135 | {file = "coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a"}, 136 | {file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb"}, 137 | {file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d"}, 138 | {file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747"}, 139 | {file = "coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5"}, 140 | {file = "coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713"}, 141 | {file = "coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32"}, 142 | {file = "coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65"}, 143 | {file = "coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6"}, 144 | {file = "coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0"}, 145 | {file = "coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e"}, 146 | {file = "coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5"}, 147 | {file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7"}, 148 | {file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5"}, 149 | {file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0"}, 150 | {file = "coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7"}, 151 | {file = "coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930"}, 152 | {file = "coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b"}, 153 | {file = "coverage-7.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90558c35af64971d65fbd935c32010f9a2f52776103a259f1dee865fe8259352"}, 154 | {file = "coverage-7.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8953746d371e5695405806c46d705a3cd170b9cc2b9f93953ad838f6c1e58612"}, 155 | {file = "coverage-7.10.6-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c83f6afb480eae0313114297d29d7c295670a41c11b274e6bca0c64540c1ce7b"}, 156 | {file = "coverage-7.10.6-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7eb68d356ba0cc158ca535ce1381dbf2037fa8cb5b1ae5ddfc302e7317d04144"}, 157 | {file = "coverage-7.10.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b15a87265e96307482746d86995f4bff282f14b027db75469c446da6127433b"}, 158 | {file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fc53ba868875bfbb66ee447d64d6413c2db91fddcfca57025a0e7ab5b07d5862"}, 159 | {file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efeda443000aa23f276f4df973cb82beca682fd800bb119d19e80504ffe53ec2"}, 160 | {file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9702b59d582ff1e184945d8b501ffdd08d2cee38d93a2206aa5f1365ce0b8d78"}, 161 | {file = "coverage-7.10.6-cp39-cp39-win32.whl", hash = "sha256:2195f8e16ba1a44651ca684db2ea2b2d4b5345da12f07d9c22a395202a05b23c"}, 162 | {file = "coverage-7.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:f32ff80e7ef6a5b5b606ea69a36e97b219cd9dc799bcf2963018a4d8f788cfbf"}, 163 | {file = "coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3"}, 164 | {file = "coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90"}, 165 | ] 166 | 167 | [package.dependencies] 168 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 169 | 170 | [package.extras] 171 | toml = ["tomli ; python_full_version <= \"3.11.0a6\""] 172 | 173 | [[package]] 174 | name = "dependency-groups" 175 | version = "1.3.1" 176 | description = "A tool for resolving PEP 735 Dependency Group data" 177 | optional = false 178 | python-versions = ">=3.8" 179 | groups = ["dev"] 180 | files = [ 181 | {file = "dependency_groups-1.3.1-py3-none-any.whl", hash = "sha256:51aeaa0dfad72430fcfb7bcdbefbd75f3792e5919563077f30bc0d73f4493030"}, 182 | {file = "dependency_groups-1.3.1.tar.gz", hash = "sha256:78078301090517fd938c19f64a53ce98c32834dfe0dee6b88004a569a6adfefd"}, 183 | ] 184 | 185 | [package.dependencies] 186 | packaging = "*" 187 | tomli = {version = "*", markers = "python_version < \"3.11\""} 188 | 189 | [package.extras] 190 | cli = ["tomli ; python_version < \"3.11\""] 191 | 192 | [[package]] 193 | name = "distlib" 194 | version = "0.4.0" 195 | description = "Distribution utilities" 196 | optional = false 197 | python-versions = "*" 198 | groups = ["dev"] 199 | files = [ 200 | {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, 201 | {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, 202 | ] 203 | 204 | [[package]] 205 | name = "exceptiongroup" 206 | version = "1.3.0" 207 | description = "Backport of PEP 654 (exception groups)" 208 | optional = false 209 | python-versions = ">=3.7" 210 | groups = ["dev"] 211 | markers = "python_version < \"3.11\"" 212 | files = [ 213 | {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, 214 | {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, 215 | ] 216 | 217 | [package.dependencies] 218 | typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} 219 | 220 | [package.extras] 221 | test = ["pytest (>=6)"] 222 | 223 | [[package]] 224 | name = "filelock" 225 | version = "3.19.1" 226 | description = "A platform independent file lock." 227 | optional = false 228 | python-versions = ">=3.9" 229 | groups = ["dev"] 230 | files = [ 231 | {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, 232 | {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, 233 | ] 234 | 235 | [[package]] 236 | name = "iniconfig" 237 | version = "2.1.0" 238 | description = "brain-dead simple config-ini parsing" 239 | optional = false 240 | python-versions = ">=3.8" 241 | groups = ["dev"] 242 | files = [ 243 | {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, 244 | {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, 245 | ] 246 | 247 | [[package]] 248 | name = "mypy" 249 | version = "1.17.1" 250 | description = "Optional static typing for Python" 251 | optional = false 252 | python-versions = ">=3.9" 253 | groups = ["dev"] 254 | files = [ 255 | {file = "mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972"}, 256 | {file = "mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7"}, 257 | {file = "mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df"}, 258 | {file = "mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390"}, 259 | {file = "mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94"}, 260 | {file = "mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b"}, 261 | {file = "mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58"}, 262 | {file = "mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5"}, 263 | {file = "mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd"}, 264 | {file = "mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b"}, 265 | {file = "mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5"}, 266 | {file = "mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b"}, 267 | {file = "mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb"}, 268 | {file = "mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403"}, 269 | {file = "mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056"}, 270 | {file = "mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341"}, 271 | {file = "mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb"}, 272 | {file = "mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19"}, 273 | {file = "mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7"}, 274 | {file = "mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81"}, 275 | {file = "mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6"}, 276 | {file = "mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849"}, 277 | {file = "mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14"}, 278 | {file = "mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a"}, 279 | {file = "mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733"}, 280 | {file = "mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd"}, 281 | {file = "mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0"}, 282 | {file = "mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a"}, 283 | {file = "mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91"}, 284 | {file = "mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed"}, 285 | {file = "mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9"}, 286 | {file = "mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99"}, 287 | {file = "mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8"}, 288 | {file = "mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8"}, 289 | {file = "mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259"}, 290 | {file = "mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d"}, 291 | {file = "mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9"}, 292 | {file = "mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01"}, 293 | ] 294 | 295 | [package.dependencies] 296 | mypy_extensions = ">=1.0.0" 297 | pathspec = ">=0.9.0" 298 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 299 | typing_extensions = ">=4.6.0" 300 | 301 | [package.extras] 302 | dmypy = ["psutil (>=4.0)"] 303 | faster-cache = ["orjson"] 304 | install-types = ["pip"] 305 | mypyc = ["setuptools (>=50)"] 306 | reports = ["lxml"] 307 | 308 | [[package]] 309 | name = "mypy-extensions" 310 | version = "1.1.0" 311 | description = "Type system extensions for programs checked with the mypy type checker." 312 | optional = false 313 | python-versions = ">=3.8" 314 | groups = ["dev"] 315 | files = [ 316 | {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, 317 | {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, 318 | ] 319 | 320 | [[package]] 321 | name = "nox" 322 | version = "2025.5.1" 323 | description = "Flexible test automation." 324 | optional = false 325 | python-versions = ">=3.8" 326 | groups = ["dev"] 327 | files = [ 328 | {file = "nox-2025.5.1-py3-none-any.whl", hash = "sha256:56abd55cf37ff523c254fcec4d152ed51e5fe80e2ab8317221d8b828ac970a31"}, 329 | {file = "nox-2025.5.1.tar.gz", hash = "sha256:2a571dfa7a58acc726521ac3cd8184455ebcdcbf26401c7b737b5bc6701427b2"}, 330 | ] 331 | 332 | [package.dependencies] 333 | argcomplete = ">=1.9.4,<4" 334 | attrs = ">=23.1" 335 | colorlog = ">=2.6.1,<7" 336 | dependency-groups = ">=1.1" 337 | packaging = ">=20.9" 338 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 339 | virtualenv = ">=20.14.1" 340 | 341 | [package.extras] 342 | tox-to-nox = ["importlib-resources ; python_version < \"3.9\"", "jinja2", "tox (>=4)"] 343 | uv = ["uv (>=0.1.6)"] 344 | 345 | [[package]] 346 | name = "packaging" 347 | version = "25.0" 348 | description = "Core utilities for Python packages" 349 | optional = false 350 | python-versions = ">=3.8" 351 | groups = ["dev"] 352 | files = [ 353 | {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, 354 | {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, 355 | ] 356 | 357 | [[package]] 358 | name = "pathspec" 359 | version = "0.12.1" 360 | description = "Utility library for gitignore style pattern matching of file paths." 361 | optional = false 362 | python-versions = ">=3.8" 363 | groups = ["dev"] 364 | files = [ 365 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 366 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 367 | ] 368 | 369 | [[package]] 370 | name = "platformdirs" 371 | version = "4.4.0" 372 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 373 | optional = false 374 | python-versions = ">=3.9" 375 | groups = ["dev"] 376 | files = [ 377 | {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, 378 | {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, 379 | ] 380 | 381 | [package.extras] 382 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] 383 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] 384 | type = ["mypy (>=1.14.1)"] 385 | 386 | [[package]] 387 | name = "pluggy" 388 | version = "1.6.0" 389 | description = "plugin and hook calling mechanisms for python" 390 | optional = false 391 | python-versions = ">=3.9" 392 | groups = ["dev"] 393 | files = [ 394 | {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, 395 | {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, 396 | ] 397 | 398 | [package.extras] 399 | dev = ["pre-commit", "tox"] 400 | testing = ["coverage", "pytest", "pytest-benchmark"] 401 | 402 | [[package]] 403 | name = "pygments" 404 | version = "2.19.2" 405 | description = "Pygments is a syntax highlighting package written in Python." 406 | optional = false 407 | python-versions = ">=3.8" 408 | groups = ["dev"] 409 | files = [ 410 | {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, 411 | {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, 412 | ] 413 | 414 | [package.extras] 415 | windows-terminal = ["colorama (>=0.4.6)"] 416 | 417 | [[package]] 418 | name = "pytest" 419 | version = "8.4.2" 420 | description = "pytest: simple powerful testing with Python" 421 | optional = false 422 | python-versions = ">=3.9" 423 | groups = ["dev"] 424 | files = [ 425 | {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, 426 | {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, 427 | ] 428 | 429 | [package.dependencies] 430 | colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} 431 | exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} 432 | iniconfig = ">=1" 433 | packaging = ">=20" 434 | pluggy = ">=1.5,<2" 435 | pygments = ">=2.7.2" 436 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 437 | 438 | [package.extras] 439 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] 440 | 441 | [[package]] 442 | name = "pytest-cov" 443 | version = "7.0.0" 444 | description = "Pytest plugin for measuring coverage." 445 | optional = false 446 | python-versions = ">=3.9" 447 | groups = ["dev"] 448 | files = [ 449 | {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, 450 | {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, 451 | ] 452 | 453 | [package.dependencies] 454 | coverage = {version = ">=7.10.6", extras = ["toml"]} 455 | pluggy = ">=1.2" 456 | pytest = ">=7" 457 | 458 | [package.extras] 459 | testing = ["process-tests", "pytest-xdist", "virtualenv"] 460 | 461 | [[package]] 462 | name = "ruff" 463 | version = "0.12.12" 464 | description = "An extremely fast Python linter and code formatter, written in Rust." 465 | optional = false 466 | python-versions = ">=3.7" 467 | groups = ["dev"] 468 | files = [ 469 | {file = "ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc"}, 470 | {file = "ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727"}, 471 | {file = "ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb"}, 472 | {file = "ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577"}, 473 | {file = "ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e"}, 474 | {file = "ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e"}, 475 | {file = "ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8"}, 476 | {file = "ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5"}, 477 | {file = "ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92"}, 478 | {file = "ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45"}, 479 | {file = "ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5"}, 480 | {file = "ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4"}, 481 | {file = "ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23"}, 482 | {file = "ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489"}, 483 | {file = "ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee"}, 484 | {file = "ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1"}, 485 | {file = "ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d"}, 486 | {file = "ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093"}, 487 | {file = "ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6"}, 488 | ] 489 | 490 | [[package]] 491 | name = "tomli" 492 | version = "2.2.1" 493 | description = "A lil' TOML parser" 494 | optional = false 495 | python-versions = ">=3.8" 496 | groups = ["dev"] 497 | markers = "python_full_version <= \"3.11.0a6\"" 498 | files = [ 499 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 500 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 501 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 502 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 503 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 504 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 505 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 506 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 507 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 508 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 509 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 510 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 511 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 512 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 513 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 514 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 515 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 516 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 517 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 518 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 519 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 520 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 521 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 522 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 523 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 524 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 525 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 526 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 527 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 528 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 529 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 530 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 531 | ] 532 | 533 | [[package]] 534 | name = "tomlkit" 535 | version = "0.13.3" 536 | description = "Style preserving TOML library" 537 | optional = false 538 | python-versions = ">=3.8" 539 | groups = ["main"] 540 | files = [ 541 | {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, 542 | {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, 543 | ] 544 | 545 | [[package]] 546 | name = "typing-extensions" 547 | version = "4.15.0" 548 | description = "Backported and Experimental Type Hints for Python 3.9+" 549 | optional = false 550 | python-versions = ">=3.9" 551 | groups = ["dev"] 552 | files = [ 553 | {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, 554 | {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, 555 | ] 556 | 557 | [[package]] 558 | name = "virtualenv" 559 | version = "20.34.0" 560 | description = "Virtual Python Environment builder" 561 | optional = false 562 | python-versions = ">=3.8" 563 | groups = ["dev"] 564 | files = [ 565 | {file = "virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026"}, 566 | {file = "virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a"}, 567 | ] 568 | 569 | [package.dependencies] 570 | distlib = ">=0.3.7,<1" 571 | filelock = ">=3.12.2,<4" 572 | platformdirs = ">=3.9.1,<5" 573 | typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} 574 | 575 | [package.extras] 576 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 577 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] 578 | 579 | [metadata] 580 | lock-version = "2.1" 581 | python-versions = ">=3.9" 582 | content-hash = "08973ace2e79b407209d28c433414c74313b11b188a90845fb8911d8714a0c50" 583 | --------------------------------------------------------------------------------