7
7
8
8
from collections import defaultdict
9
9
from itertools import chain
10
- from typing import TYPE_CHECKING , Iterable , Iterator
10
+ from typing import TYPE_CHECKING , Any , Iterable , Iterator
11
11
12
12
import tomllib
13
13
14
14
from tox .config .loader .ini .factor import find_envs
15
15
from tox .config .loader .memory import MemoryLoader
16
16
17
17
from .api import Source
18
- from .toml_section import CORE , PKG_ENV_PREFIX , TEST_ENV_PREFIX , TomlSection
18
+ from .toml_section import BASE_TEST_ENV , CORE , PKG_ENV_PREFIX , TEST_ENV_PREFIX , TEST_ENV_ROOT , TomlSection
19
19
20
20
if TYPE_CHECKING :
21
21
from pathlib import Path
25
25
from tox .config .sets import ConfigSet
26
26
27
27
28
+ def _extract_section (raw : dict [str , Any ], section : TomlSection ) -> Any :
29
+ """Extract section from TOML decoded data."""
30
+ result = raw
31
+ for key in chain (section .prefix , (section .name ,)):
32
+ if key in result :
33
+ result = result [key ]
34
+ else :
35
+ return None
36
+ return result
37
+
38
+
28
39
class TomlSource (Source ):
29
40
"""Configuration sourced from a toml file (such as tox.toml).
30
41
31
42
This is experimental API! Expect things to be broken.
32
43
"""
33
44
34
45
CORE_SECTION = CORE
46
+ ROOT_KEY : str | None = None
35
47
36
48
def __init__ (self , path : Path , content : str | None = None ) -> None :
37
49
super ().__init__ (path )
38
50
if content is None :
39
51
if not path .exists ():
40
- raise ValueError
52
+ msg = f"Path { path } does not exist."
53
+ raise ValueError (msg )
41
54
content = path .read_text ()
42
- self ._raw = tomllib .loads (content )
55
+ data = tomllib .loads (content )
56
+ if self .ROOT_KEY :
57
+ if self .ROOT_KEY not in data :
58
+ msg = f"Section { self .ROOT_KEY } not found in { path } ."
59
+ raise ValueError (msg )
60
+ data = data [self .ROOT_KEY ]
61
+ self ._raw = data
43
62
self ._section_mapping : defaultdict [str , list [str ]] = defaultdict (list )
44
63
45
64
def __repr__ (self ) -> str :
@@ -48,32 +67,30 @@ def __repr__(self) -> str:
48
67
def transform_section (self , section : Section ) -> Section :
49
68
return TomlSection (section .prefix , section .name )
50
69
51
- def get_loader (self , section : Section , override_map : OverrideMap ) -> MemoryLoader | None :
52
- # look up requested section name in the generative testenv mapping to find the real config source
53
- for key in self ._section_mapping .get (section .name ) or []:
54
- if section .prefix is None or TomlSection .from_key (key ).prefix == section .prefix :
55
- break
56
- else :
57
- # if no matching section/prefix is found, use the requested section key as-is (for custom prefixes)
58
- key = section .key
59
- if key in self ._raw :
60
- return MemoryLoader (
61
- self ._raw [key ],
62
- section = section ,
63
- overrides = override_map .get (section .key , []),
64
- )
65
- return None
70
+ def get_loader (self , section : TomlSection , override_map : OverrideMap ) -> MemoryLoader | None :
71
+ result = _extract_section (self ._raw , section )
72
+ if result is None :
73
+ return None
74
+
75
+ return MemoryLoader (
76
+ result ,
77
+ section = section ,
78
+ overrides = override_map .get (section .key , []),
79
+ )
66
80
67
81
def get_base_sections (self , base : list [str ], in_section : Section ) -> Iterator [Section ]: # noqa: PLR6301
68
82
for a_base in base :
69
- section = TomlSection .from_key (a_base )
70
- yield section # the base specifier is explicit
71
- if in_section .prefix is not None : # no prefix specified, so this could imply our own prefix
72
- yield TomlSection (in_section .prefix , a_base )
83
+ yield TomlSection (in_section .prefix , a_base )
73
84
74
- def sections (self ) -> Iterator [Section ]:
85
+ def sections (self ) -> Iterator [TomlSection ]:
86
+ # TODO: just return core section and any `tox.env.XXX` sections which exist directly.
75
87
for key in self ._raw :
76
- yield TomlSection .from_key (key )
88
+ section = TomlSection .from_key (key )
89
+ yield section
90
+ if section == self .CORE_SECTION :
91
+ test_env_data = _extract_section (self ._raw , TEST_ENV_ROOT )
92
+ for env_name in test_env_data or {}:
93
+ yield TomlSection (TEST_ENV_PREFIX , env_name )
77
94
78
95
def envs (self , core_config : ConfigSet ) -> Iterator [str ]:
79
96
seen = set ()
@@ -102,8 +119,9 @@ def register_factors(envs: Iterable[str]) -> None:
102
119
for section in self .sections ():
103
120
yield from self ._discover_from_section (section , known_factors )
104
121
105
- def _discover_from_section (self , section : Section , known_factors : set [str ]) -> Iterator [str ]:
106
- for value in self ._raw [section .key ].values ():
122
+ def _discover_from_section (self , section : TomlSection , known_factors : set [str ]) -> Iterator [str ]:
123
+ section_data = _extract_section (self ._raw , section )
124
+ for value in (section_data or {}).values ():
107
125
if isinstance (value , bool ):
108
126
# It's not a value with env definition.
109
127
continue
@@ -113,8 +131,8 @@ def _discover_from_section(self, section: Section, known_factors: set[str]) -> I
113
131
if set (env .split ("-" )) - known_factors :
114
132
yield env
115
133
116
- def get_tox_env_section (self , item : str ) -> tuple [Section , list [str ], list [str ]]: # noqa: PLR6301
117
- return TomlSection .test_env (item ), [TEST_ENV_PREFIX ], [PKG_ENV_PREFIX ]
134
+ def get_tox_env_section (self , item : str ) -> tuple [TomlSection , list [str ], list [str ]]: # noqa: PLR6301
135
+ return TomlSection .test_env (item ), [BASE_TEST_ENV ], [PKG_ENV_PREFIX ]
118
136
119
137
def get_core_section (self ) -> TomlSection :
120
138
return self .CORE_SECTION
@@ -129,12 +147,18 @@ class ToxToml(TomlSource):
129
147
FILENAME = "tox.toml"
130
148
131
149
132
- # TODO: Section model is way too configparser precific for this to work easily.
133
- # class PyProjectToml(TomlSource):
134
- # """Configuration sourced from a pyproject.toml file.
150
+ class PyProjectToml (TomlSource ):
151
+ """Configuration sourced from a pyproject.toml file.
135
152
136
- # This is experimental API! Expect things to be broken.
137
- # """
153
+ This is experimental API! Expect things to be broken.
154
+ """
138
155
139
- # FILENAME = "pyproject.toml"
140
- # CORE_SECTION = IniSection("tool", "tox")
156
+ FILENAME = "pyproject.toml"
157
+ ROOT_KEY = "tool"
158
+
159
+ def __init__ (self , path : Path , content : str | None = None ) -> None :
160
+ super ().__init__ (path , content )
161
+ core_data = _extract_section (self ._raw , self .CORE_SECTION )
162
+ if core_data is not None and tuple (core_data .keys ()) == ("legacy_tox_ini" ,):
163
+ msg = "pyproject.toml is in the legacy mode."
164
+ raise ValueError (msg )
0 commit comments