summaryrefslogtreecommitdiff
path: root/src/tox/config/source/ini.py
blob: fb23c0c3ee34e9c1f2b20456cc8a5fcf3cbad304 (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
"""Load """
from __future__ import annotations

from collections import defaultdict
from configparser import ConfigParser
from itertools import chain
from pathlib import Path
from typing import DefaultDict, Iterable, Iterator

from tox.config.loader.ini.factor import find_envs

from ..loader.api import OverrideMap
from ..loader.ini import IniLoader
from ..loader.section import Section
from ..sets import ConfigSet
from .api import Source
from .ini_section import CORE, PKG_ENV_PREFIX, TEST_ENV_PREFIX, IniSection


class IniSource(Source):
    """Configuration sourced from a ini file (such as tox.ini)"""

    CORE_SECTION = CORE

    def __init__(self, path: Path, content: str | None = None) -> None:
        super().__init__(path)
        self._parser = ConfigParser(interpolation=None)
        if content is None:
            if not path.exists():
                raise ValueError
            content = path.read_text()
        self._parser.read_string(content, str(path))
        self._section_mapping: DefaultDict[str, list[str]] = defaultdict(list)

    def transform_section(self, section: Section) -> Section:
        return IniSection(section.prefix, section.name)

    def sections(self) -> Iterator[IniSection]:
        for section in self._parser.sections():
            yield IniSection.from_key(section)

    def get_loader(self, section: Section, override_map: OverrideMap) -> IniLoader | None:
        # look up requested section name in the generative testenv mapping to find the real config source
        for key in self._section_mapping.get(section.name) or []:
            if section.prefix is None or Section.from_key(key).prefix == section.prefix:
                break
        else:
            # if no matching section/prefix is found, use the requested section key as-is (for custom prefixes)
            key = section.key
        if self._parser.has_section(key):
            return IniLoader(
                section=section,
                parser=self._parser,
                overrides=override_map.get(section.key, []),
                core_section=self.CORE_SECTION,
                section_key=key,
            )
        return None

    def get_core_section(self) -> Section:
        return self.CORE_SECTION

    def get_base_sections(self, base: list[str], in_section: Section) -> Iterator[Section]:
        for a_base in base:
            section = IniSection.from_key(a_base)
            yield section  # the base specifier is explicit
            if in_section.prefix is not None:  # no prefix specified, so this could imply our own prefix
                yield IniSection(in_section.prefix, a_base)

    def get_tox_env_section(self, item: str) -> tuple[Section, list[str], list[str]]:
        return IniSection.test_env(item), [TEST_ENV_PREFIX], [PKG_ENV_PREFIX]

    def envs(self, core_config: ConfigSet) -> Iterator[str]:
        seen = set()
        for name in self._discover_tox_envs(core_config):
            if name not in seen:
                seen.add(name)
                yield name

    def _discover_tox_envs(self, core_config: ConfigSet) -> Iterator[str]:
        def register_factors(envs: Iterable[str]) -> None:
            known_factors.update(chain.from_iterable(e.split("-") for e in envs))

        explicit = list(core_config["env_list"])
        yield from explicit
        known_factors: set[str] = set()
        register_factors(explicit)

        # discover all additional defined environments, including generative section headers
        for section in self.sections():
            if section.is_test_env:
                register_factors(section.names)
                for name in section.names:
                    self._section_mapping[name].append(section.key)
                    yield name
        # add all conditional markers that are not part of the explicitly defined sections
        for section in self.sections():
            yield from self._discover_from_section(section, known_factors)

    def _discover_from_section(self, section: IniSection, known_factors: set[str]) -> Iterator[str]:
        for value in self._parser[section.key].values():
            for env in find_envs(value):
                if set(env.split("-")) - known_factors:
                    yield env

    def __repr__(self) -> str:
        return f"{type(self).__name__}(path={self.path})"


__all__ = [
    "IniSource",
]