Skip to content

Commit

Permalink
Merge pull request #52 from jonathangreen/feature/sort-overrides
Browse files Browse the repository at this point in the history
Override sort configuration per key in configuration
  • Loading branch information
pappasam authored Mar 16, 2023
2 parents 9c79248 + c30f8fe commit 26df965
Show file tree
Hide file tree
Showing 6 changed files with 423 additions and 51 deletions.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,45 @@ check = true
ignore_case = true
```

### Configuration Overrides
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.

Only the following options can be included in an override:

```toml
[tool.tomlsort.overrides."path.to.key"]
table_keys = true
inline_tables = true
inline_arrays = true
```

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.

For instance, to disable sorting the table in the following TOML file:

```toml
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
country = "中国"
```

You can use any of the following overrides:

```toml
# Overrides in own table
[tool.tomlsort.overrides."servers.beta"]
table_keys = false

# Overrides in the tomlsort table
[tool.tomlsort]
overrides."servers.beta".table_keys = false

# Override using a wildcard if config should be applied to all servers keys
[tool.tomlsort]
overrides."servers.*".table_keys = false
```

## Comments

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.
Expand Down
44 changes: 44 additions & 0 deletions tests/examples/sorted/from-toml-lang-overrides.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# This is a TOML document. Boom.

title = "TOML Example"

[clients]
data = [["gamma", "delta"], [1, 2]] # just an update to make sure parsers support it
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]

[database]
connection_max = 5000
enabled = true # Comment after a boolean
ports = [8001, 8001, 8002]
server = "192.168.1.1"

[owner]
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
name = "Tom Preston-Werner"
organization = "GitHub"

[[products]]
name = "Hammer"
sku = 738594937

[[products]]
color = "gray"
name = "Nail"
sku = 284758393

[servers]

# You can indent as you please. Tabs or spaces. TOML don't care.
[servers.alpha]
dc = "eqdc10"
ip = "10.0.0.1"

[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
country = "中国" # This should be parsed as UTF-8
94 changes: 91 additions & 3 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import pytest

from toml_sort import cli
from toml_sort.tomlsort import SortOverrideConfiguration

PATH_EXAMPLES = "tests/examples"

Expand Down Expand Up @@ -254,7 +255,8 @@ def test_multiple_files_and_errors(options):
def test_load_config_file_read():
"""Test no error if pyproject.toml cannot be read."""
with mock.patch("toml_sort.cli.open", side_effect=OSError):
assert not cli.load_config_file()
section = cli.load_pyproject()
assert not cli.parse_config(section)


@pytest.mark.parametrize(
Expand All @@ -264,6 +266,15 @@ def test_load_config_file_read():
("[tool.other]\nfoo=2", {}),
("[tool.tomlsort]", {}),
("[tool.tomlsort]\nall=true", {"all": True}),
(
"""
[tool.tomlsort]
all=true
[tool.tomlsort.overrides]
"a.b.c".inline_array = false
""",
{"all": True},
),
(
"[tool.tomlsort]\nspaces_before_inline_comment=4",
{"spaces_before_inline_comment": 4},
Expand All @@ -274,7 +285,8 @@ def test_load_config_file(toml, expected):
"""Test load_config_file."""
open_mock = mock.mock_open(read_data=toml)
with mock.patch("toml_sort.cli.open", open_mock):
assert cli.load_config_file() == expected
section = cli.load_pyproject()
assert cli.parse_config(section) == expected


@pytest.mark.parametrize(
Expand All @@ -285,4 +297,80 @@ def test_load_config_file_invalid(toml):
open_mock = mock.mock_open(read_data=toml)
with mock.patch("toml_sort.cli.open", open_mock):
with pytest.raises(SystemExit):
cli.load_config_file()
section = cli.load_pyproject()
cli.parse_config(section)


@pytest.mark.parametrize(
"toml,expected",
[
(
"""
[tool.tomlsort.overrides."a.b.c"]
table_keys = false
""",
{"a.b.c": SortOverrideConfiguration(table_keys=False)},
),
(
"""
[tool.tomlsort.overrides."test.123"]
table_keys = false
[tool.tomlsort.overrides."test.456"]
inline_tables = false
[tool.tomlsort.overrides."test.789"]
inline_arrays = false
""",
{
"test.123": SortOverrideConfiguration(table_keys=False),
"test.456": SortOverrideConfiguration(inline_tables=False),
"test.789": SortOverrideConfiguration(inline_arrays=False),
},
),
(
"""
[tool.tomlsort.overrides]
"test.123".table_keys = false
"test.456".inline_tables = false
"test.789".inline_arrays = false
""",
{
"test.123": SortOverrideConfiguration(table_keys=False),
"test.456": SortOverrideConfiguration(inline_tables=False),
"test.789": SortOverrideConfiguration(inline_arrays=False),
},
),
],
)
def test_load_config_overrides(toml, expected):
"""Test that we correctly turn settings in tomldocument into a
SortOverrideConfiguration dataclass."""
open_mock = mock.mock_open(read_data=toml)
with mock.patch("toml_sort.cli.open", open_mock):
section = cli.load_pyproject()
assert expected == cli.parse_config_overrides(section)


@pytest.mark.parametrize(
"toml",
[
"""
[tool.tomlsort.overrides."a.b.c"]
unknown = false
""",
"""
[tool.tomlsort.overrides."a.b.c"]
table_keys = false
inline_tables = false
inline_arrays = false
foo = "bar"
""",
],
)
def test_load_config_overrides_fail(toml):
"""Test that parse_config_overrides exits if the config contains an
unexpected key."""
open_mock = mock.mock_open(read_data=toml)
with mock.patch("toml_sort.cli.open", open_mock):
with pytest.raises(SystemExit):
section = cli.load_pyproject()
cli.parse_config_overrides(section)
21 changes: 21 additions & 0 deletions tests/test_toml_sort.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
CommentConfiguration,
FormattingConfiguration,
SortConfiguration,
SortOverrideConfiguration,
)


Expand Down Expand Up @@ -82,6 +83,26 @@ def test_sort_toml_is_str() -> None:
),
},
),
(
"from-toml-lang",
"from-toml-lang-overrides",
{
"sort_config": SortConfiguration(
inline_arrays=True, inline_tables=True
),
"format_config": FormattingConfiguration(
spaces_before_inline_comment=1
),
"sort_config_overrides": {
"servers.beta": SortOverrideConfiguration(
table_keys=False
),
"clients.data": SortOverrideConfiguration(
inline_arrays=False
),
},
},
),
(
"pyproject-weird-order",
"pyproject-weird-order",
Expand Down
56 changes: 48 additions & 8 deletions toml_sort/cli.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
"""Toml Sort command line interface."""

import argparse
import dataclasses
import sys
from argparse import ArgumentParser
from typing import Any, Dict, List, Optional, Type

import tomlkit
from tomlkit import TOMLDocument

from .tomlsort import (
CommentConfiguration,
FormattingConfiguration,
SortConfiguration,
SortOverrideConfiguration,
TomlSort,
)

Expand Down Expand Up @@ -78,18 +81,26 @@ def validate_and_copy(
target[key] = data.pop(key)


def load_config_file() -> Dict[str, Any]:
"""Load the configuration from pyproject.toml."""
def load_pyproject() -> TOMLDocument:
"""Load pyproject file, and return tool.tomlsort section."""
try:
with open("pyproject.toml", encoding="utf-8") as file:
content = file.read()
except OSError:
return {}
return tomlkit.document()

document = tomlkit.parse(content)
tool_section = document.get("tool", tomlkit.document())
toml_sort_section = tool_section.get("tomlsort", tomlkit.document())
config = dict(toml_sort_section)
return tool_section.get("tomlsort", tomlkit.document())


def parse_config(tomlsort_section: TOMLDocument) -> Dict[str, Any]:
"""Load the toml_sort configuration from a TOMLDocument."""
config = dict(tomlsort_section)

# remove the overrides key, since it is parsed separately
# in parse_config_overrides.
config.pop("overrides", None)

clean_config: Dict[str, Any] = {}
validate_and_copy(config, clean_config, "all", bool)
Expand Down Expand Up @@ -121,7 +132,30 @@ def load_config_file() -> Dict[str, Any]:
return clean_config


def get_parser() -> ArgumentParser:
def parse_config_overrides(
tomlsort_section: TOMLDocument,
) -> Dict[str, SortOverrideConfiguration]:
"""Parse the tool.tomlsort.overrides section of the config."""
fields = dataclasses.fields(SortOverrideConfiguration)
settings_definition = {field.name: field.type for field in fields}
override_settings = dict(
tomlsort_section.get("overrides", tomlkit.document())
)
overrides = {}
for path, settings in override_settings.items():
if not settings.keys() <= settings_definition.keys():
unknown_settings = settings.keys() - settings_definition.keys()
printerr("Unexpected configuration override settings:")
for unknown_setting in unknown_settings:
printerr(f' "{path}".{unknown_setting}')
sys.exit(1)

overrides[path] = SortOverrideConfiguration(**settings)

return overrides


def get_parser(defaults: Dict[str, Any]) -> ArgumentParser:
"""Get the argument parser."""
parser = ArgumentParser(
prog="toml-sort",
Expand Down Expand Up @@ -286,15 +320,20 @@ def get_parser() -> ArgumentParser:
type=str,
nargs="*",
)
parser.set_defaults(**load_config_file())
parser.set_defaults(**defaults)
return parser


def cli( # pylint: disable=too-many-branches
arguments: Optional[List[str]] = None,
) -> None:
"""Toml sort cli implementation."""
args = get_parser().parse_args(args=arguments) # strip command itself
settings = load_pyproject()
configuration = parse_config(settings)
configuration_overrides = parse_config_overrides(settings)
args = get_parser(configuration).parse_args(
args=arguments
) # strip command itself
if args.version:
print(get_version())
sys.exit(0)
Expand Down Expand Up @@ -353,6 +392,7 @@ def cli( # pylint: disable=too-many-branches
spaces_indent_inline_array=args.spaces_indent_inline_array,
trailing_comma_inline_array=args.trailing_comma_inline_array,
),
sort_config_overrides=configuration_overrides,
).sorted()
if args.check:
if original_toml != sorted_toml:
Expand Down
Loading

0 comments on commit 26df965

Please sign in to comment.