diff --git a/CHANGES.rst b/CHANGES.rst index 7bf685705..9f6621314 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -52,6 +52,10 @@ Fixed - Linting fixes: Use ``stacklevel=2`` in ``warnings.warn()`` calls as suggested by Flake8; skip Bandit check for virtualenv creation in the GitHub Action; use ``ignore[method-assign]`` as suggested by Mypy. +- Configuration options spelled with hyphens in ``pyproject.toml`` + (e.g. ``line-length = 88``) are now supported. +- In debug log output mode, configuration options are now always spelled with hyphens + instead of underscores. 1.7.0_ - 2023-02-11 diff --git a/src/darker/config.py b/src/darker/config.py index 940a5d909..702c1f0a3 100644 --- a/src/darker/config.py +++ b/src/darker/config.py @@ -6,7 +6,7 @@ from argparse import ArgumentParser, Namespace from dataclasses import dataclass, field from pathlib import Path -from typing import Iterable, List, Optional, Set, cast +from typing import Dict, Iterable, List, Optional, Set, Union, cast import toml @@ -26,6 +26,9 @@ def dump_list(self, v: Iterable[object]) -> str: return "[{}\n]".format("".join(f"\n {self.dump_value(item)}," for item in v)) +UnvalidatedConfig = Dict[str, Union[List[str], str, bool, int]] + + class DarkerConfig(TypedDict, total=False): """Dictionary representing ``[tool.darker]`` from ``pyproject.toml``""" @@ -92,6 +95,40 @@ class ConfigurationError(Exception): """Exception class for invalid configuration values""" +def convert_config_characters( + config: UnvalidatedConfig, pattern: str, replacement: str +) -> UnvalidatedConfig: + """Convert a character in config keys to a different character""" + return {key.replace(pattern, replacement): value for key, value in config.items()} + + +def convert_hyphens_to_underscores(config: UnvalidatedConfig) -> UnvalidatedConfig: + """Convert hyphenated config keys to underscored keys""" + return convert_config_characters(config, "-", "_") + + +def convert_underscores_to_hyphens(config: DarkerConfig) -> UnvalidatedConfig: + """Convert underscores in config keys to hyphens""" + return convert_config_characters(cast(UnvalidatedConfig, config), "_", "-") + + +def validate_config_keys(config: UnvalidatedConfig) -> None: + """Raise an exception if any keys in the configuration are invalid. + + :param config: The configuration read from ``pyproject.toml`` + :raises ConfigurationError: Raised if unknown options are present + + """ + if set(config).issubset(DarkerConfig.__annotations__): + return + unknown_keys = ", ".join( + sorted(set(config).difference(DarkerConfig.__annotations__)) + ) + raise ConfigurationError( + f"Invalid [tool.darker] keys in pyproject.toml: {unknown_keys}" + ) + + def replace_log_level_name(config: DarkerConfig) -> None: """Replace numeric log level in configuration with the name of the log level""" if "log_level" in config: @@ -165,7 +202,11 @@ def load_config(path: Optional[str], srcs: Iterable[str]) -> DarkerConfig: if not config_path.is_file(): return {} pyproject_toml = toml.load(config_path) - config = cast(DarkerConfig, pyproject_toml.get("tool", {}).get("darker", {}) or {}) + tool_darker_config = convert_hyphens_to_underscores( + pyproject_toml.get("tool", {}).get("darker", {}) or {} + ) + validate_config_keys(tool_darker_config) + config = cast(DarkerConfig, tool_darker_config) replace_log_level_name(config) validate_config_output_mode(config) return config @@ -195,7 +236,9 @@ def get_modified_config(parser: ArgumentParser, args: Namespace) -> DarkerConfig def dump_config(config: DarkerConfig) -> str: """Return the configuration in TOML format""" - dump = toml.dumps(config, encoder=TomlArrayLinesEncoder()) + dump = toml.dumps( + convert_underscores_to_hyphens(config), encoder=TomlArrayLinesEncoder() + ) return f"[tool.darker]\n{dump}" diff --git a/src/darker/tests/test_config.py b/src/darker/tests/test_config.py index 577e4b32c..0713ef42b 100644 --- a/src/darker/tests/test_config.py +++ b/src/darker/tests/test_config.py @@ -206,8 +206,8 @@ def test_output_mode_from_args(diff, stdout, expect): dict(cwd="lvl1/lvl2"), dict(cwd="has_git", expect={}), dict(cwd="has_git/lvl1", expect={}), - dict(cwd="has_pyp", expect={"CONFIG_PATH": "has_pyp"}), - dict(cwd="has_pyp/lvl1", expect={"CONFIG_PATH": "has_pyp"}), + dict(cwd="has_pyp", expect={"config": "has_pyp"}), + dict(cwd="has_pyp/lvl1", expect={"config": "has_pyp"}), dict(srcs=["root.py"]), dict(srcs=["../root.py"], cwd="lvl1"), dict(srcs=["../root.py"], cwd="has_git"), @@ -222,22 +222,22 @@ def test_output_mode_from_args(diff, stdout, expect): dict(srcs=["pyp.py", "../lvl1/lvl1.py"], cwd="has_pyp"), dict( srcs=["has_pyp/lvl1/l1.py", "has_pyp/lvl1b/l1b.py"], - expect={"CONFIG_PATH": "has_pyp"}, + expect={"config": "has_pyp"}, ), dict( srcs=["../has_pyp/lvl1/l1.py", "../has_pyp/lvl1b/l1b.py"], cwd="lvl1", - expect={"CONFIG_PATH": "has_pyp"}, + expect={"config": "has_pyp"}, ), dict( srcs=["../has_pyp/lvl1/l1.py", "../has_pyp/lvl1b/l1b.py"], cwd="has_git", - expect={"CONFIG_PATH": "has_pyp"}, + expect={"config": "has_pyp"}, ), dict( srcs=["lvl1/l1.py", "lvl1b/l1b.py"], cwd="has_pyp", - expect={"CONFIG_PATH": "has_pyp"}, + expect={"config": "has_pyp"}, ), dict( srcs=["full_example/full.py"], @@ -252,202 +252,230 @@ def test_output_mode_from_args(diff, stdout, expect): }, ), dict(srcs=["stdout_example/dummy.py"], expect={"stdout": True}), - dict(confpath="c", expect={"PYP_TOML": 1}), - dict(confpath="c/pyproject.toml", expect={"PYP_TOML": 1}), - dict(cwd="lvl1", confpath="../c", expect={"PYP_TOML": 1}), - dict(cwd="lvl1", confpath="../c/pyproject.toml", expect={"PYP_TOML": 1}), - dict(cwd="lvl1/lvl2", confpath="../../c", expect={"PYP_TOML": 1}), - dict(cwd="lvl1/lvl2", confpath="../../c/pyproject.toml", expect={"PYP_TOML": 1}), - dict(cwd="has_git", confpath="../c", expect={"PYP_TOML": 1}), - dict(cwd="has_git", confpath="../c/pyproject.toml", expect={"PYP_TOML": 1}), - dict(cwd="has_git/lvl1", confpath="../../c", expect={"PYP_TOML": 1}), - dict(cwd="has_git/lvl1", confpath="../../c/pyproject.toml", expect={"PYP_TOML": 1}), - dict(cwd="has_pyp", confpath="../c", expect={"PYP_TOML": 1}), - dict(cwd="has_pyp", confpath="../c/pyproject.toml", expect={"PYP_TOML": 1}), - dict(cwd="has_pyp/lvl1", confpath="../../c", expect={"PYP_TOML": 1}), - dict(cwd="has_pyp/lvl1", confpath="../../c/pyproject.toml", expect={"PYP_TOML": 1}), - dict(srcs=["root.py"], confpath="c", expect={"PYP_TOML": 1}), - dict(srcs=["root.py"], confpath="c/pyproject.toml", expect={"PYP_TOML": 1}), - dict(srcs=["../root.py"], cwd="lvl1", confpath="../c", expect={"PYP_TOML": 1}), + dict(confpath="c", expect={"lint": ["PYP_TOML"]}), + dict(confpath="c/pyproject.toml", expect={"lint": ["PYP_TOML"]}), + dict(cwd="lvl1", confpath="../c", expect={"lint": ["PYP_TOML"]}), + dict(cwd="lvl1", confpath="../c/pyproject.toml", expect={"lint": ["PYP_TOML"]}), + dict(cwd="lvl1/lvl2", confpath="../../c", expect={"lint": ["PYP_TOML"]}), + dict( + cwd="lvl1/lvl2", + confpath="../../c/pyproject.toml", + expect={"lint": ["PYP_TOML"]}, + ), + dict(cwd="has_git", confpath="../c", expect={"lint": ["PYP_TOML"]}), + dict(cwd="has_git", confpath="../c/pyproject.toml", expect={"lint": ["PYP_TOML"]}), + dict(cwd="has_git/lvl1", confpath="../../c", expect={"lint": ["PYP_TOML"]}), + dict( + cwd="has_git/lvl1", + confpath="../../c/pyproject.toml", + expect={"lint": ["PYP_TOML"]}, + ), + dict(cwd="has_pyp", confpath="../c", expect={"lint": ["PYP_TOML"]}), + dict(cwd="has_pyp", confpath="../c/pyproject.toml", expect={"lint": ["PYP_TOML"]}), + dict(cwd="has_pyp/lvl1", confpath="../../c", expect={"lint": ["PYP_TOML"]}), + dict( + cwd="has_pyp/lvl1", + confpath="../../c/pyproject.toml", + expect={"lint": ["PYP_TOML"]}, + ), + dict(srcs=["root.py"], confpath="c", expect={"lint": ["PYP_TOML"]}), + dict(srcs=["root.py"], confpath="c/pyproject.toml", expect={"lint": ["PYP_TOML"]}), + dict( + srcs=["../root.py"], cwd="lvl1", confpath="../c", expect={"lint": ["PYP_TOML"]} + ), dict( srcs=["../root.py"], cwd="lvl1", confpath="../c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, + ), + dict( + srcs=["../root.py"], + cwd="has_git", + confpath="../c", + expect={"lint": ["PYP_TOML"]}, ), - dict(srcs=["../root.py"], cwd="has_git", confpath="../c", expect={"PYP_TOML": 1}), dict( srcs=["../root.py"], cwd="has_git", confpath="../c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, + ), + dict( + srcs=["../root.py"], + cwd="has_pyp", + confpath="../c", + expect={"lint": ["PYP_TOML"]}, ), - dict(srcs=["../root.py"], cwd="has_pyp", confpath="../c", expect={"PYP_TOML": 1}), dict( srcs=["../root.py"], cwd="has_pyp", confpath="../c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), - dict(srcs=["root.py", "lvl1/lvl1.py"], confpath="c", expect={"PYP_TOML": 1}), + dict(srcs=["root.py", "lvl1/lvl1.py"], confpath="c", expect={"lint": ["PYP_TOML"]}), dict( srcs=["root.py", "lvl1/lvl1.py"], confpath="c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["../root.py", "lvl1.py"], cwd="lvl1", confpath="../c", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["../root.py", "lvl1.py"], cwd="lvl1", confpath="../c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["../root.py", "../lvl1/lvl1.py"], cwd="has_git", confpath="../c", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["../root.py", "../lvl1/lvl1.py"], cwd="has_git", confpath="../c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["../root.py", "../lvl1/lvl1.py"], cwd="has_pyp", confpath="../c", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["../root.py", "../lvl1/lvl1.py"], cwd="has_pyp", confpath="../c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, + ), + dict( + srcs=["has_pyp/pyp.py", "lvl1/lvl1.py"], + confpath="c", + expect={"lint": ["PYP_TOML"]}, ), - dict(srcs=["has_pyp/pyp.py", "lvl1/lvl1.py"], confpath="c", expect={"PYP_TOML": 1}), dict( srcs=["has_pyp/pyp.py", "lvl1/lvl1.py"], confpath="c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["../has_pyp/pyp.py", "lvl1.py"], cwd="lvl1", confpath="../c", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["../has_pyp/pyp.py", "lvl1.py"], cwd="lvl1", confpath="../c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["../has_pyp/pyp.py", "../lvl1/lvl1.py"], cwd="has_git", confpath="../c", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["../has_pyp/pyp.py", "../lvl1/lvl1.py"], cwd="has_git", confpath="../c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["pyp.py", "../lvl1/lvl1.py"], cwd="has_pyp", confpath="../c", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["pyp.py", "../lvl1/lvl1.py"], cwd="has_pyp", confpath="../c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["has_pyp/lvl1/l1.py", "has_pyp/lvl1b/l1b.py"], confpath="c", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["has_pyp/lvl1/l1.py", "has_pyp/lvl1b/l1b.py"], confpath="c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["../has_pyp/lvl1/l1.py", "../has_pyp/lvl1b/l1b.py"], cwd="lvl1", confpath="../c", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["../has_pyp/lvl1/l1.py", "../has_pyp/lvl1b/l1b.py"], cwd="lvl1", confpath="../c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["../has_pyp/lvl1/l1.py", "../has_pyp/lvl1b/l1b.py"], cwd="has_git", confpath="../c", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["../has_pyp/lvl1/l1.py", "../has_pyp/lvl1b/l1b.py"], cwd="has_git", confpath="../c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["lvl1/l1.py", "lvl1b/l1b.py"], cwd="has_pyp", confpath="../c", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), dict( srcs=["lvl1/l1.py", "lvl1b/l1b.py"], cwd="has_pyp", confpath="../c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), - dict(srcs=["full_example/full.py"], confpath="c", expect={"PYP_TOML": 1}), + dict(srcs=["full_example/full.py"], confpath="c", expect={"lint": ["PYP_TOML"]}), dict( srcs=["full_example/full.py"], confpath="c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), - dict(srcs=["stdout_example/dummy.py"], confpath="c", expect={"PYP_TOML": 1}), + dict(srcs=["stdout_example/dummy.py"], confpath="c", expect={"lint": ["PYP_TOML"]}), dict( srcs=["stdout_example/dummy.py"], confpath="c/pyproject.toml", - expect={"PYP_TOML": 1}, + expect={"lint": ["PYP_TOML"]}, ), srcs=[], cwd=".", confpath=None, - expect={"CONFIG_PATH": "."}, + expect={"config": "no_pyp"}, ) def test_load_config( # pylint: disable=too-many-arguments find_project_root_cache_clear, tmp_path, monkeypatch, srcs, cwd, confpath, expect ): """``load_config()`` finds and loads configuration based on source file paths""" (tmp_path / ".git").mkdir() - (tmp_path / "pyproject.toml").write_text('[tool.darker]\nCONFIG_PATH = "."\n') + (tmp_path / "pyproject.toml").write_text('[tool.darker]\nconfig = "no_pyp"\n') (tmp_path / "lvl1/lvl2").mkdir(parents=True) (tmp_path / "has_git/.git").mkdir(parents=True) (tmp_path / "has_git/lvl1").mkdir() (tmp_path / "has_pyp/lvl1").mkdir(parents=True) (tmp_path / "has_pyp/pyproject.toml").write_text( - '[tool.darker]\nCONFIG_PATH = "has_pyp"\n' + '[tool.darker]\nconfig = "has_pyp"\n' ) (tmp_path / "full_example").mkdir() (tmp_path / "full_example/pyproject.toml").write_text( @@ -476,7 +504,9 @@ def test_load_config( # pylint: disable=too-many-arguments "[tool.darker]\nstdout = true\n" ) (tmp_path / "c").mkdir() - (tmp_path / "c" / "pyproject.toml").write_text("[tool.darker]\nPYP_TOML = 1\n") + (tmp_path / "c" / "pyproject.toml").write_text( + "[tool.darker]\nlint = ['PYP_TOML']\n" + ) monkeypatch.chdir(tmp_path / cwd) result = load_config(confpath, srcs) @@ -608,7 +638,7 @@ def test_get_modified_config(args, expect): isort = false lint = [ ] - log_level = "DEBUG" + log-level = "DEBUG" """ ), ),