summaryrefslogtreecommitdiff
path: root/src/tox/config/main.py
blob: 00aaaed81bd0d461540eeb8ac707d72895706b06 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
from __future__ import annotations

import os
from collections import OrderedDict, defaultdict
from pathlib import Path
from typing import TYPE_CHECKING, Any, Iterator, Sequence, TypeVar

from tox.config.loader.api import Loader, OverrideMap

from .loader.section import Section
from .sets import ConfigSet, CoreConfigSet, EnvConfigSet
from .source import Source

if TYPE_CHECKING:
    from .cli.parser import Parsed


T = TypeVar("T", bound=ConfigSet)


class Config:
    """Main configuration object for tox."""

    def __init__(
        self,
        config_source: Source,
        options: Parsed,
        root: Path,
        pos_args: Sequence[str] | None,
        work_dir: Path,
    ) -> None:
        self._pos_args = None if pos_args is None else tuple(pos_args)
        self._work_dir = work_dir
        self._root = root
        self._options = options

        self._overrides: OverrideMap = defaultdict(list)
        for override in options.override:
            self._overrides[override.namespace].append(override)

        self._src = config_source
        self._key_to_conf_set: dict[tuple[str, str], ConfigSet] = OrderedDict()
        self._core_set: CoreConfigSet | None = None

    def pos_args(self, to_path: Path | None) -> tuple[str, ...] | None:
        """
        :param to_path: if not None rewrite relative posargs paths from cwd to to_path
        :return: positional argument
        """
        if self._pos_args is not None and to_path is not None and Path.cwd() != to_path:
            args = []
            to_path_str = os.path.abspath(str(to_path))  # we use os.path to unroll .. in path without resolve
            for arg in self._pos_args:
                path_arg = Path(arg)
                if path_arg.exists() and not path_arg.is_absolute():
                    path_arg_str = os.path.abspath(str(path_arg))  # we use os.path to unroll .. in path without resolve
                    relative = os.path.relpath(path_arg_str, to_path_str)  # we use os.path to not fail when not within
                    args.append(relative)
                else:
                    args.append(arg)
            return tuple(args)
        return self._pos_args

    @property
    def work_dir(self) -> Path:
        """:return: working directory for this project"""
        return self._work_dir

    @property
    def src_path(self) -> Path:
        """:return: the location of the tox configuration source"""
        return self._src.path

    def __iter__(self) -> Iterator[str]:
        """:return: an iterator that goes through existing environments"""
        return self._src.envs(self.core)

    def sections(self) -> Iterator[Section]:
        yield from self._src.sections()

    def __repr__(self) -> str:
        return f"{type(self).__name__}(config_source={self._src!r})"

    def __contains__(self, item: str) -> bool:
        """:return: check if an environment already exists"""
        return any(name for name in self if name == item)

    @classmethod
    def make(cls, parsed: Parsed, pos_args: Sequence[str] | None, source: Source) -> Config:
        """Make a tox configuration object."""
        # root is the project root, where the configuration file is at
        # work dir is where we put our own files
        root: Path = source.path.parent if parsed.root_dir is None else parsed.root_dir
        work_dir: Path = source.path.parent if parsed.work_dir is None else parsed.work_dir
        # if these are relative we need to expand them them to ensure paths built on this can resolve independent on cwd
        root = root.resolve()
        work_dir = work_dir.resolve()
        return cls(
            config_source=source,
            options=parsed,
            pos_args=pos_args,
            root=root,
            work_dir=work_dir,
        )

    @property
    def options(self) -> Parsed:
        return self._options

    @property
    def core(self) -> CoreConfigSet:
        """:return: the core configuration"""
        if self._core_set is not None:
            return self._core_set
        core_section = self._src.get_core_section()
        core = CoreConfigSet(self, core_section, self._root, self.src_path)
        core.loaders.extend(self._src.get_loaders(core_section, base=[], override_map=self._overrides, conf=core))
        self._core_set = core
        return core

    def get_section_config(
        self,
        section: Section,
        base: list[str] | None,
        of_type: type[T],
        for_env: str | None,
        loaders: Sequence[Loader[Any]] | None = None,
    ) -> T:
        key = section.key, for_env or ""
        try:
            return self._key_to_conf_set[key]  # type: ignore[return-value] # expected T but found ConfigSet
        except KeyError:
            conf_set = of_type(self, section, for_env)
            self._key_to_conf_set[key] = conf_set
            for loader in self._src.get_loaders(section, base, self._overrides, conf_set):
                conf_set.loaders.append(loader)
            if loaders is not None:
                conf_set.loaders.extend(loaders)
            return conf_set

    def get_env(
        self,
        item: str,
        package: bool = False,
        loaders: Sequence[Loader[Any]] | None = None,
    ) -> EnvConfigSet:
        """
        Return the configuration for a given tox environment (will create if not exist yet).

        :param item: the name of the environment
        :param package: a flag indicating if the environment is of type packaging or not (only used for creation)
        :param loaders: loaders to use for this configuration (only used for creation)
        :return: the tox environments config
        """
        section, base_test, base_pkg = self._src.get_tox_env_section(item)
        conf_set = self.get_section_config(
            section,
            base=base_pkg if package else base_test,
            of_type=EnvConfigSet,
            for_env=item,
            loaders=loaders,
        )
        return conf_set

    def clear_env(self, name: str) -> None:
        section, _, __ = self._src.get_tox_env_section(name)
        del self._key_to_conf_set[(section.key, name)]


___all__ = [
    "Config",
]