55import os
66import sys
77from dataclasses import dataclass
8- from itertools import chain
98from typing import TYPE_CHECKING , Any
109
1110import pytest
@@ -46,7 +45,11 @@ def pytest_load_initial_conftests(
4645 parser : pytest .Parser , # noqa: ARG001
4746) -> None :
4847 """Load environment variables from configuration files."""
49- for env_file in _load_env_files (early_config ):
48+ env_files_list : list [str ] = []
49+ if toml_config := _find_toml_config (early_config ):
50+ env_files_list , _ = _load_toml_config (toml_config )
51+
52+ for env_file in _load_env_files (early_config , env_files_list ):
5053 for key , value in dotenv_values (env_file ).items ():
5154 if value is not None :
5255 os .environ [key ] = value
@@ -56,77 +59,62 @@ def pytest_load_initial_conftests(
5659 elif entry .skip_if_set and entry .key in os .environ :
5760 continue
5861 else :
59- # transformation -> replace environment variables, e.g. TEST_DIR={USER}/repo_test_dir.
6062 os .environ [entry .key ] = entry .value .format (** os .environ ) if entry .transform else entry .value
6163
6264
63- def _env_files_from_toml (early_config : pytest .Config ) -> list [str ]:
64- for path in chain .from_iterable ([[early_config .rootpath ], early_config .rootpath .parents ]):
65- for pytest_toml_name in ("pytest.toml" , ".pytest.toml" , "pyproject.toml" ):
66- pytest_toml_file = path / pytest_toml_name
67- if not pytest_toml_file .exists ():
68- continue
69- with pytest_toml_file .open ("rb" ) as file_handler :
70- try :
71- config = tomllib .load (file_handler )
72- except tomllib .TOMLDecodeError :
73- return []
74- if pytest_toml_name == "pyproject.toml" :
75- config = config .get ("tool" , {})
76- if (
77- (pytest_env := config .get ("pytest_env" ))
78- and isinstance (pytest_env , dict )
79- and (raw := pytest_env .get ("env_files" ))
80- ):
81- return [str (f ) for f in (raw if isinstance (raw , list ) else [raw ])]
82- return []
83- return []
84-
85-
86- def _load_env_files (early_config : pytest .Config ) -> Generator [Path , None , None ]:
87- if not (env_files := _env_files_from_toml (early_config )):
88- env_files = list (early_config .getini ("env_files" ))
89- for env_file_str in env_files :
90- if (resolved := early_config .rootpath / env_file_str ).is_file ():
91- yield resolved
65+ def _find_toml_config (early_config : pytest .Config ) -> Path | None :
66+ """Find TOML config file by checking inipath first, then walking up the tree."""
67+ if (
68+ early_config .inipath
69+ and early_config .inipath .suffix == ".toml"
70+ and early_config .inipath .name in {"pytest.toml" , ".pytest.toml" , "pyproject.toml" }
71+ ):
72+ return early_config .inipath
9273
74+ start_path = early_config .inipath .parent if early_config .inipath is not None else early_config .rootpath
75+ for current_path in [start_path , * start_path .parents ]:
76+ for toml_name in ("pytest.toml" , ".pytest.toml" , "pyproject.toml" ):
77+ toml_file = current_path / toml_name
78+ if toml_file .exists ():
79+ return toml_file
80+ return None
9381
94- def _parse_toml_config (config : dict [str , Any ]) -> Generator [Entry , None , None ]:
95- for key , entry in config .items ():
96- if key == "env_files" and isinstance (entry , list ):
97- continue
98- if isinstance (entry , dict ):
99- unset = bool (entry .get ("unset" ))
100- value = str (entry .get ("value" , "" )) if not unset else ""
101- transform , skip_if_set = bool (entry .get ("transform" )), bool (entry .get ("skip_if_set" ))
102- else :
103- value , transform , skip_if_set , unset = str (entry ), False , False , False
104- yield Entry (key , value , transform , skip_if_set , unset = unset )
10582
83+ def _load_toml_config (config_path : Path ) -> tuple [list [str ], list [Entry ]]:
84+ """Load env_files and entries from TOML config file."""
85+ with config_path .open ("rb" ) as file_handler :
86+ config = tomllib .load (file_handler )
10687
107- def _load_values (early_config : pytest .Config ) -> Iterator [Entry ]:
108- has_toml = False
109- start_path = early_config .inipath .parent if early_config .inipath is not None else early_config .rootpath
110- for path in chain .from_iterable ([[start_path ], start_path .parents ]):
111- for pytest_toml_name in ("pytest.toml" , ".pytest.toml" , "pyproject.toml" ):
112- pytest_toml_file = path / pytest_toml_name
113- if pytest_toml_file .exists ():
114- with pytest_toml_file .open ("rb" ) as file_handler :
115- config = tomllib .load (file_handler )
88+ if config_path .name == "pyproject.toml" :
89+ config = config .get ("tool" , {})
90+
91+ pytest_env_config = config .get ("pytest_env" , {})
92+ if not pytest_env_config :
93+ return [], []
11694
117- if pytest_toml_name == "pyproject.toml" : # in pyproject.toml the path is tool.pytest_env
118- config = config . get ( "tool" , {})
95+ raw_env_files = pytest_env_config . get ( "env_files" )
96+ env_files = [ str ( f ) for f in raw_env_files ] if isinstance ( raw_env_files , list ) else []
11997
120- if "pytest_env" in config :
121- has_toml = True
122- yield from _parse_toml_config (config ["pytest_env" ])
98+ entries = list (_parse_toml_config (pytest_env_config ))
99+ return env_files , entries
123100
124- break # breaks the pytest_toml_name forloop
125- if has_toml : # breaks the path forloop
126- break
127101
128- if has_toml :
129- return
102+ def _load_env_files (early_config : pytest .Config , env_files : list [str ]) -> Generator [Path , None , None ]:
103+ """Resolve and yield existing env files."""
104+ if not env_files :
105+ env_files = list (early_config .getini ("env_files" ))
106+ for env_file_str in env_files :
107+ if (resolved := early_config .rootpath / env_file_str ).is_file ():
108+ yield resolved
109+
110+
111+ def _load_values (early_config : pytest .Config ) -> Iterator [Entry ]:
112+ """Load env entries from config, preferring TOML over INI."""
113+ if toml_config := _find_toml_config (early_config ):
114+ _ , entries = _load_toml_config (toml_config )
115+ if entries :
116+ yield from entries
117+ return
130118
131119 for line in early_config .getini ("env" ):
132120 # INI lines e.g. D:R:NAME=VAL has two flags (R and D), NAME key, and VAL value
@@ -142,3 +130,16 @@ def _load_values(early_config: pytest.Config) -> Iterator[Entry]:
142130 key = ini_key_parts [- 1 ].strip ()
143131 value = parts [2 ].strip ()
144132 yield Entry (key , value , transform , skip_if_set , unset = unset )
133+
134+
135+ def _parse_toml_config (config : dict [str , Any ]) -> Generator [Entry , None , None ]:
136+ for key , entry in config .items ():
137+ if key == "env_files" and isinstance (entry , list ):
138+ continue
139+ if isinstance (entry , dict ):
140+ unset = bool (entry .get ("unset" ))
141+ value = str (entry .get ("value" , "" )) if not unset else ""
142+ transform , skip_if_set = bool (entry .get ("transform" )), bool (entry .get ("skip_if_set" ))
143+ else :
144+ value , transform , skip_if_set , unset = str (entry ), False , False , False
145+ yield Entry (key , value , transform , skip_if_set , unset = unset )
0 commit comments