77
88from collections import defaultdict
99from itertools import chain
10- from typing import TYPE_CHECKING , Iterable , Iterator
10+ from typing import TYPE_CHECKING , Any , Iterable , Iterator
1111
1212import tomllib
1313
1414from tox .config .loader .ini .factor import find_envs
1515from tox .config .loader .memory import MemoryLoader
1616
1717from .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
1919
2020if TYPE_CHECKING :
2121 from pathlib import Path
2525 from tox .config .sets import ConfigSet
2626
2727
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+
2839class TomlSource (Source ):
2940 """Configuration sourced from a toml file (such as tox.toml).
3041
3142 This is experimental API! Expect things to be broken.
3243 """
3344
3445 CORE_SECTION = CORE
46+ ROOT_KEY : str | None = None
3547
3648 def __init__ (self , path : Path , content : str | None = None ) -> None :
3749 super ().__init__ (path )
3850 if content is None :
3951 if not path .exists ():
40- raise ValueError
52+ msg = f"Path { path } does not exist."
53+ raise ValueError (msg )
4154 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
4362 self ._section_mapping : defaultdict [str , list [str ]] = defaultdict (list )
4463
4564 def __repr__ (self ) -> str :
@@ -48,32 +67,30 @@ def __repr__(self) -> str:
4867 def transform_section (self , section : Section ) -> Section :
4968 return TomlSection (section .prefix , section .name )
5069
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+ )
6680
6781 def get_base_sections (self , base : list [str ], in_section : Section ) -> Iterator [Section ]: # noqa: PLR6301
6882 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 )
7384
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.
7587 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 )
7794
7895 def envs (self , core_config : ConfigSet ) -> Iterator [str ]:
7996 seen = set ()
@@ -102,8 +119,9 @@ def register_factors(envs: Iterable[str]) -> None:
102119 for section in self .sections ():
103120 yield from self ._discover_from_section (section , known_factors )
104121
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 ():
107125 if isinstance (value , bool ):
108126 # It's not a value with env definition.
109127 continue
@@ -113,8 +131,8 @@ def _discover_from_section(self, section: Section, known_factors: set[str]) -> I
113131 if set (env .split ("-" )) - known_factors :
114132 yield env
115133
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 ]
118136
119137 def get_core_section (self ) -> TomlSection :
120138 return self .CORE_SECTION
@@ -129,12 +147,18 @@ class ToxToml(TomlSource):
129147 FILENAME = "tox.toml"
130148
131149
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.
135152
136- # This is experimental API! Expect things to be broken.
137- # """
153+ This is experimental API! Expect things to be broken.
154+ """
138155
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