-
-
Notifications
You must be signed in to change notification settings - Fork 520
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
- Loading branch information
1 parent
f5eba31
commit 82387e7
Showing
9 changed files
with
502 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Native TOML configuration support - by :user:`gaborbernat`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
from __future__ import annotations | ||
|
||
import sys | ||
from inspect import isclass | ||
from pathlib import Path | ||
from typing import ( | ||
TYPE_CHECKING, | ||
Any, | ||
Dict, | ||
Iterator, | ||
List, | ||
Literal, | ||
Mapping, | ||
Set, | ||
TypeVar, | ||
Union, | ||
cast, | ||
) | ||
|
||
from tox.config.loader.api import Loader, Override | ||
from tox.config.types import Command, EnvList | ||
from tox.report import HandledError | ||
|
||
if TYPE_CHECKING: | ||
from tox.config.loader.section import Section | ||
from tox.config.main import Config | ||
|
||
if sys.version_info >= (3, 11): # pragma: no cover (py311+) | ||
from typing import TypeGuard | ||
else: # pragma: no cover (py311+) | ||
from typing_extensions import TypeGuard | ||
if sys.version_info >= (3, 10): # pragma: no cover (py310+) | ||
from typing import TypeAlias | ||
else: # pragma: no cover (py310+) | ||
from typing_extensions import TypeAlias | ||
|
||
TomlTypes: TypeAlias = Union[Dict[str, "TomlTypes"], List["TomlTypes"], str, int, float, bool, None] | ||
|
||
|
||
class TomlLoader(Loader[TomlTypes]): | ||
"""Load configuration from a pyproject.toml file.""" | ||
|
||
def __init__( | ||
self, | ||
section: Section, | ||
overrides: list[Override], | ||
content: Mapping[str, TomlTypes], | ||
) -> None: | ||
if not isinstance(content, Mapping): | ||
path = f"tox{'.' if section.key else ''}{section.key}" | ||
msg = f"{path} must be a mapping, is {content.__class__.__name__!r}" | ||
raise HandledError(msg) | ||
self.content = content | ||
super().__init__(section, overrides) | ||
|
||
def load_raw(self, key: str, conf: Config | None, env_name: str | None) -> TomlTypes: # noqa: ARG002 | ||
return self.content[key] | ||
|
||
def found_keys(self) -> set[str]: | ||
return set(self.content.keys()) | ||
|
||
@staticmethod | ||
def to_str(value: TomlTypes) -> str: | ||
return _ensure_type_correct(value, str) # type: ignore[return-value] # no mypy support | ||
|
||
@staticmethod | ||
def to_bool(value: TomlTypes) -> bool: | ||
return _ensure_type_correct(value, bool) | ||
|
||
@staticmethod | ||
def to_list(value: TomlTypes, of_type: type[Any]) -> Iterator[_T]: | ||
of = List[of_type] # type: ignore[valid-type] # no mypy support | ||
return iter(_ensure_type_correct(value, of)) # type: ignore[call-overload,no-any-return] | ||
|
||
@staticmethod | ||
def to_set(value: TomlTypes, of_type: type[Any]) -> Iterator[_T]: | ||
of = Set[of_type] # type: ignore[valid-type] # no mypy support | ||
return iter(_ensure_type_correct(value, of)) # type: ignore[call-overload,no-any-return] | ||
|
||
@staticmethod | ||
def to_dict(value: TomlTypes, of_type: tuple[type[Any], type[Any]]) -> Iterator[tuple[_T, _T]]: | ||
of = Dict[of_type[0], of_type[1]] # type: ignore[valid-type] # no mypy support | ||
return _ensure_type_correct(value, of).items() # type: ignore[attr-defined,no-any-return] | ||
|
||
@staticmethod | ||
def to_path(value: TomlTypes) -> Path: | ||
return Path(TomlLoader.to_str(value)) | ||
|
||
@staticmethod | ||
def to_command(value: TomlTypes) -> Command: | ||
return Command(args=cast(list[str], value)) # validated during load in _ensure_type_correct | ||
|
||
@staticmethod | ||
def to_env_list(value: TomlTypes) -> EnvList: | ||
return EnvList(envs=list(TomlLoader.to_list(value, str))) | ||
|
||
|
||
_T = TypeVar("_T") | ||
|
||
|
||
def _ensure_type_correct(val: TomlTypes, of_type: type[_T]) -> TypeGuard[_T]: # noqa: C901, PLR0912 | ||
casting_to = getattr(of_type, "__origin__", of_type.__class__) | ||
msg = "" | ||
if casting_to in {list, List}: | ||
entry_type = of_type.__args__[0] # type: ignore[attr-defined] | ||
if isinstance(val, list): | ||
for va in val: | ||
_ensure_type_correct(va, entry_type) | ||
else: | ||
msg = f"{val!r} is not list" | ||
elif isclass(of_type) and issubclass(of_type, Command): | ||
# first we cast it to list then create commands, so for now just validate is a nested list | ||
_ensure_type_correct(val, list[str]) | ||
elif casting_to in {set, Set}: | ||
entry_type = of_type.__args__[0] # type: ignore[attr-defined] | ||
if isinstance(val, set): | ||
for va in val: | ||
_ensure_type_correct(va, entry_type) | ||
else: | ||
msg = f"{val!r} is not set" | ||
elif casting_to in {dict, Dict}: | ||
key_type, value_type = of_type.__args__[0], of_type.__args__[1] # type: ignore[attr-defined] | ||
if isinstance(val, dict): | ||
for va in val: | ||
_ensure_type_correct(va, key_type) | ||
for va in val.values(): | ||
_ensure_type_correct(va, value_type) | ||
else: | ||
msg = f"{val!r} is not dictionary" | ||
elif casting_to == Union: # handle Optional values | ||
args: list[type[Any]] = of_type.__args__ # type: ignore[attr-defined] | ||
for arg in args: | ||
try: | ||
_ensure_type_correct(val, arg) | ||
break | ||
except TypeError: | ||
pass | ||
else: | ||
msg = f"{val!r} is not union of {', '.join(a.__name__ for a in args)}" | ||
elif casting_to in {Literal, type(Literal)}: | ||
choice = of_type.__args__ # type: ignore[attr-defined] | ||
if val not in choice: | ||
msg = f"{val!r} is not one of literal {','.join(repr(i) for i in choice)}" | ||
elif not isinstance(val, of_type): | ||
msg = f"{val!r} is not of type {of_type.__name__!r}" | ||
if msg: | ||
raise TypeError(msg) | ||
return cast(_T, val) # type: ignore[return-value] # logic too complicated for mypy | ||
|
||
|
||
__all__ = [ | ||
"TomlLoader", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
"""Load.""" | ||
|
||
from __future__ import annotations | ||
|
||
import sys | ||
from typing import TYPE_CHECKING, Any, Iterator, Mapping, cast | ||
|
||
from tox.config.loader.section import Section | ||
from tox.config.loader.toml import TomlLoader | ||
|
||
from .api import Source | ||
|
||
if sys.version_info >= (3, 11): # pragma: no cover (py311+) | ||
import tomllib | ||
else: # pragma: no cover (py311+) | ||
import tomli as tomllib | ||
|
||
if TYPE_CHECKING: | ||
from collections.abc import Iterable | ||
from pathlib import Path | ||
|
||
from tox.config.loader.api import Loader, OverrideMap | ||
from tox.config.sets import CoreConfigSet | ||
|
||
TEST_ENV_PREFIX = "env" | ||
|
||
|
||
class TomlSection(Section): | ||
SEP = "." | ||
|
||
@classmethod | ||
def test_env(cls, name: str) -> TomlSection: | ||
return cls(f"tox{cls.SEP}{name}", name) | ||
|
||
@property | ||
def is_test_env(self) -> bool: | ||
return self.prefix == TEST_ENV_PREFIX | ||
|
||
@property | ||
def keys(self) -> Iterable[str]: | ||
return self.key.split(self.SEP) | ||
|
||
|
||
class Toml(Source): | ||
"""Configuration sourced from a pyproject.toml files.""" | ||
|
||
FILENAME = "pyproject.toml" | ||
|
||
def __init__(self, path: Path) -> None: | ||
if path.name != self.FILENAME or not path.exists(): | ||
raise ValueError | ||
with path.open("rb") as file_handler: | ||
toml_content = tomllib.load(file_handler) | ||
try: | ||
content: Mapping[str, Any] = toml_content["tool"]["tox"] | ||
if "legacy_tox_ini" in content: | ||
msg = "legacy_tox_ini" | ||
raise KeyError(msg) # noqa: TRY301 | ||
self._content = content | ||
except KeyError as exc: | ||
raise ValueError(path) from exc | ||
super().__init__(path) | ||
|
||
def __repr__(self) -> str: | ||
return f"{self.__class__.__name__}({self.path!r})" | ||
|
||
def get_core_section(self) -> Section: # noqa: PLR6301 | ||
return TomlSection(prefix=None, name="tox") | ||
|
||
def transform_section(self, section: Section) -> Section: # noqa: PLR6301 | ||
return TomlSection(section.prefix, section.name) | ||
|
||
def get_loader(self, section: Section, override_map: OverrideMap) -> Loader[Any] | None: | ||
current = self._content | ||
for at, key in enumerate(cast(TomlSection, section).keys): | ||
if at == 0: | ||
if key != "tox": | ||
msg = "Internal error, first key is not tox" | ||
raise RuntimeError(msg) | ||
elif key in current: | ||
current = current[key] | ||
else: | ||
return None | ||
return TomlLoader( | ||
section=section, | ||
overrides=override_map.get(section.key, []), | ||
content=current, | ||
) | ||
|
||
def envs(self, core_conf: CoreConfigSet) -> Iterator[str]: | ||
yield from core_conf["env_list"] | ||
yield from [i.key for i in self.sections()] | ||
|
||
def sections(self) -> Iterator[Section]: | ||
for env_name in self._content.get("env", {}): | ||
yield TomlSection.from_key(env_name) | ||
|
||
def get_base_sections(self, base: list[str], in_section: Section) -> Iterator[Section]: # noqa: PLR6301, ARG002 | ||
yield from [TomlSection.from_key(b) for b in base] | ||
|
||
def get_tox_env_section(self, item: str) -> tuple[Section, list[str], list[str]]: # noqa: PLR6301 | ||
return TomlSection.test_env(item), ["tox.env_base"], ["tox.pkgenv"] | ||
|
||
|
||
__all__ = [ | ||
"Toml", | ||
] |
Oops, something went wrong.