| 
 | 1 | +from __future__ import annotations  | 
 | 2 | + | 
 | 3 | +import sys  | 
 | 4 | +from pathlib import Path  | 
 | 5 | +from typing import (  | 
 | 6 | +    TYPE_CHECKING,  | 
 | 7 | +    Any,  | 
 | 8 | +    Dict,  | 
 | 9 | +    Iterator,  | 
 | 10 | +    List,  | 
 | 11 | +    Literal,  | 
 | 12 | +    Mapping,  | 
 | 13 | +    Set,  | 
 | 14 | +    TypeVar,  | 
 | 15 | +    Union,  | 
 | 16 | +    cast,  | 
 | 17 | +)  | 
 | 18 | + | 
 | 19 | +from tox.config.loader.api import Loader, Override  | 
 | 20 | +from tox.config.types import Command, EnvList  | 
 | 21 | +from tox.report import HandledError  | 
 | 22 | + | 
 | 23 | +if TYPE_CHECKING:  | 
 | 24 | +    from tox.config.loader.section import Section  | 
 | 25 | +    from tox.config.main import Config  | 
 | 26 | + | 
 | 27 | +if sys.version_info >= (3, 11):  # pragma: no cover (py311+)  | 
 | 28 | +    from typing import TypeGuard  | 
 | 29 | +else:  # pragma: no cover (py311+)  | 
 | 30 | +    from typing_extensions import TypeGuard  | 
 | 31 | +if sys.version_info >= (3, 10):  # pragma: no cover (py310+)  | 
 | 32 | +    from typing import TypeAlias  | 
 | 33 | +else:  # pragma: no cover (py310+)  | 
 | 34 | +    from typing_extensions import TypeAlias  | 
 | 35 | + | 
 | 36 | +TomlTypes: TypeAlias = Union[Dict[str, "TomlTypes"], List["TomlTypes"], str, int, float, bool, None]  | 
 | 37 | + | 
 | 38 | + | 
 | 39 | +class TomlLoader(Loader[TomlTypes]):  | 
 | 40 | +    """Load configuration from a pyproject.toml file."""  | 
 | 41 | + | 
 | 42 | +    def __init__(  | 
 | 43 | +        self,  | 
 | 44 | +        section: Section,  | 
 | 45 | +        overrides: list[Override],  | 
 | 46 | +        content: Mapping[str, TomlTypes],  | 
 | 47 | +    ) -> None:  | 
 | 48 | +        if not isinstance(content, Mapping):  | 
 | 49 | +            msg = f"tox.{section.key} must be a mapping"  | 
 | 50 | +            raise HandledError(msg)  | 
 | 51 | +        self.content = content  | 
 | 52 | +        super().__init__(section, overrides)  | 
 | 53 | + | 
 | 54 | +    def load_raw(self, key: str, conf: Config | None, env_name: str | None) -> TomlTypes:  # noqa: ARG002  | 
 | 55 | +        return self.content[key]  | 
 | 56 | + | 
 | 57 | +    def found_keys(self) -> set[str]:  | 
 | 58 | +        return set(self.content.keys())  | 
 | 59 | + | 
 | 60 | +    @staticmethod  | 
 | 61 | +    def to_str(value: TomlTypes) -> str:  | 
 | 62 | +        return _ensure_type_correct(value, str)  # type: ignore[return-value] # no mypy support  | 
 | 63 | + | 
 | 64 | +    @staticmethod  | 
 | 65 | +    def to_bool(value: TomlTypes) -> bool:  | 
 | 66 | +        return _ensure_type_correct(value, bool)  | 
 | 67 | + | 
 | 68 | +    @staticmethod  | 
 | 69 | +    def to_list(value: TomlTypes, of_type: type[Any]) -> Iterator[_T]:  | 
 | 70 | +        of = List[of_type]  # type: ignore[valid-type] # no mypy support  | 
 | 71 | +        return iter(_ensure_type_correct(value, of))  # type: ignore[call-overload,no-any-return]  | 
 | 72 | + | 
 | 73 | +    @staticmethod  | 
 | 74 | +    def to_set(value: TomlTypes, of_type: type[Any]) -> Iterator[_T]:  | 
 | 75 | +        of = Set[of_type]  # type: ignore[valid-type] # no mypy support  | 
 | 76 | +        return iter(_ensure_type_correct(value, of))  # type: ignore[call-overload,no-any-return]  | 
 | 77 | + | 
 | 78 | +    @staticmethod  | 
 | 79 | +    def to_dict(value: TomlTypes, of_type: tuple[type[Any], type[Any]]) -> Iterator[tuple[_T, _T]]:  | 
 | 80 | +        of = Mapping[of_type[0], of_type[1]]  # type: ignore[valid-type] # no mypy support  | 
 | 81 | +        return _ensure_type_correct(value, of).items()  # type: ignore[type-abstract,attr-defined,no-any-return]  | 
 | 82 | + | 
 | 83 | +    @staticmethod  | 
 | 84 | +    def to_path(value: TomlTypes) -> Path:  | 
 | 85 | +        return Path(TomlLoader.to_str(value))  | 
 | 86 | + | 
 | 87 | +    @staticmethod  | 
 | 88 | +    def to_command(value: TomlTypes) -> Command:  | 
 | 89 | +        return Command(args=cast(list[str], value))  # validated during load in _ensure_type_correct  | 
 | 90 | + | 
 | 91 | +    @staticmethod  | 
 | 92 | +    def to_env_list(value: TomlTypes) -> EnvList:  | 
 | 93 | +        return EnvList(envs=list(TomlLoader.to_list(value, str)))  | 
 | 94 | + | 
 | 95 | + | 
 | 96 | +_T = TypeVar("_T")  | 
 | 97 | + | 
 | 98 | + | 
 | 99 | +def _ensure_type_correct(val: TomlTypes, of_type: type[_T]) -> TypeGuard[_T]:  # noqa: C901, PLR0912  | 
 | 100 | +    casting_to = getattr(of_type, "__origin__", of_type.__class__)  | 
 | 101 | +    msg = ""  | 
 | 102 | +    if casting_to in {list, List}:  | 
 | 103 | +        entry_type = of_type.__args__[0]  # type: ignore[attr-defined]  | 
 | 104 | +        if not (isinstance(val, list) and all(_ensure_type_correct(v, entry_type) for v in val)):  | 
 | 105 | +            msg = f"{val} is not list"  | 
 | 106 | +    elif issubclass(of_type, Command):  | 
 | 107 | +        # first we cast it to list then create commands, so for now just validate is a nested list  | 
 | 108 | +        _ensure_type_correct(val, list[str])  | 
 | 109 | +    elif casting_to in {set, Set}:  | 
 | 110 | +        entry_type = of_type.__args__[0]  # type: ignore[attr-defined]  | 
 | 111 | +        if not (isinstance(val, set) and all(_ensure_type_correct(v, entry_type) for v in val)):  | 
 | 112 | +            msg = f"{val} is not set"  | 
 | 113 | +    elif casting_to in {dict, Dict}:  | 
 | 114 | +        key_type, value_type = of_type.__args__[0], of_type.__args__[1]  # type: ignore[attr-defined]  | 
 | 115 | +        if not (  | 
 | 116 | +            isinstance(val, dict)  | 
 | 117 | +            and all(  | 
 | 118 | +                _ensure_type_correct(dict_key, key_type) and _ensure_type_correct(dict_value, value_type)  | 
 | 119 | +                for dict_key, dict_value in val.items()  | 
 | 120 | +            )  | 
 | 121 | +        ):  | 
 | 122 | +            msg = f"{val} is not dictionary"  | 
 | 123 | +    elif casting_to == Union:  # handle Optional values  | 
 | 124 | +        args: list[type[Any]] = of_type.__args__  # type: ignore[attr-defined]  | 
 | 125 | +        for arg in args:  | 
 | 126 | +            try:  | 
 | 127 | +                _ensure_type_correct(val, arg)  | 
 | 128 | +                break  | 
 | 129 | +            except TypeError:  | 
 | 130 | +                pass  | 
 | 131 | +        else:  | 
 | 132 | +            msg = f"{val} is not union of {args}"  | 
 | 133 | +    elif casting_to in {Literal, type(Literal)}:  | 
 | 134 | +        choice = of_type.__args__  # type: ignore[attr-defined]  | 
 | 135 | +        if val not in choice:  | 
 | 136 | +            msg = f"{val} is not one of literal {choice}"  | 
 | 137 | +    elif not isinstance(val, of_type):  | 
 | 138 | +        msg = f"{val} is not one of {of_type}"  | 
 | 139 | +    if msg:  | 
 | 140 | +        raise TypeError(msg)  | 
 | 141 | +    return cast(_T, val)  # type: ignore[return-value] # logic too complicated for mypy  | 
 | 142 | + | 
 | 143 | + | 
 | 144 | +__all__ = [  | 
 | 145 | +    "TomlLoader",  | 
 | 146 | +]  | 
0 commit comments