@@ -27,6 +27,12 @@ def pytest_addoption(parser: pytest.Parser) -> None:
2727 help_msg = "a line separated list of environment variables of the form (FLAG:)NAME=VALUE"
2828 parser .addini ("env" , type = "linelist" , help = help_msg , default = [])
2929 parser .addini ("env_files" , type = "linelist" , help = "a line separated list of .env files to load" , default = [])
30+ parser .addini (
31+ "env_files_skip_if_set" ,
32+ type = "bool" ,
33+ help = "only set .env file variables when not already defined" ,
34+ default = False ,
35+ )
3036 parser .addoption (
3137 "--envfile" ,
3238 action = "store" ,
@@ -65,10 +71,19 @@ def pytest_load_initial_conftests(
6571 actions : list [tuple [str , str , str , str ]] = []
6672
6773 env_files_list : list [str ] = []
74+ env_files_skip_if_set : bool | None = None
6875 if toml_config := _find_toml_config (early_config ):
69- env_files_list , _ = _load_toml_config (toml_config )
76+ env_files_list , _ , env_files_skip_if_set = _load_toml_config (toml_config )
77+
78+ if env_files_skip_if_set is None :
79+ env_files_skip_if_set = bool (early_config .getini ("env_files_skip_if_set" ))
7080
71- _apply_env_files (early_config , env_files_list , actions if verbose else None )
81+ _apply_env_files (
82+ early_config ,
83+ env_files_list ,
84+ actions if verbose else None ,
85+ skip_if_set = env_files_skip_if_set ,
86+ )
7287 _apply_entries (early_config , actions if verbose else None )
7388
7489 if verbose and actions :
@@ -79,13 +94,20 @@ def _apply_env_files(
7994 early_config : pytest .Config ,
8095 env_files_list : list [str ],
8196 actions : list [tuple [str , str , str , str ]] | None ,
97+ * ,
98+ skip_if_set : bool = False ,
8299) -> None :
100+ preexisting = dict (os .environ ) if skip_if_set else {}
83101 for env_file in _load_env_files (early_config , env_files_list ):
84102 for key , value in dotenv_values (env_file ).items ():
85103 if value is not None :
86- os .environ [key ] = value
87- if actions is not None :
88- actions .append (("SET" , key , value , str (env_file )))
104+ if skip_if_set and key in preexisting :
105+ if actions is not None :
106+ actions .append (("SKIP" , key , preexisting [key ], str (env_file )))
107+ else :
108+ os .environ [key ] = value
109+ if actions is not None :
110+ actions .append (("SET" , key , value , str (env_file )))
89111
90112
91113def _apply_entries (
@@ -146,15 +168,15 @@ def _find_toml_config(early_config: pytest.Config) -> Path | None:
146168def _config_source (early_config : pytest .Config ) -> str :
147169 """Describe the configuration source for verbose output."""
148170 if toml_path := _find_toml_config (early_config ):
149- _ , entries = _load_toml_config (toml_path )
171+ _ , entries , _ = _load_toml_config (toml_path )
150172 if entries :
151173 return str (toml_path )
152174 if early_config .inipath :
153175 return str (early_config .inipath )
154176 return "config" # pragma: no cover
155177
156178
157- def _load_toml_config (config_path : Path ) -> tuple [list [str ], list [Entry ]]:
179+ def _load_toml_config (config_path : Path ) -> tuple [list [str ], list [Entry ], bool | None ]:
158180 """Load env_files and entries from TOML config file."""
159181 with config_path .open ("rb" ) as file_handler :
160182 config = tomllib .load (file_handler )
@@ -164,13 +186,15 @@ def _load_toml_config(config_path: Path) -> tuple[list[str], list[Entry]]:
164186
165187 pytest_env_config = config .get ("pytest_env" , {})
166188 if not pytest_env_config :
167- return [], []
189+ return [], [], None
168190
169191 raw_env_files = pytest_env_config .get ("env_files" )
170192 env_files = [str (f ) for f in raw_env_files ] if isinstance (raw_env_files , list ) else []
193+ raw_skip = pytest_env_config .get ("env_files_skip_if_set" )
194+ env_files_skip_if_set = raw_skip if isinstance (raw_skip , bool ) else None
171195
172196 entries = list (_parse_toml_config (pytest_env_config ))
173- return env_files , entries
197+ return env_files , entries , env_files_skip_if_set
174198
175199
176200def _load_env_files (early_config : pytest .Config , env_files : list [str ]) -> Generator [Path , None , None ]:
@@ -199,7 +223,7 @@ def _load_env_files(early_config: pytest.Config, env_files: list[str]) -> Genera
199223def _load_values (early_config : pytest .Config ) -> Iterator [Entry ]:
200224 """Load env entries from config, preferring TOML over INI."""
201225 if toml_config := _find_toml_config (early_config ):
202- _ , entries = _load_toml_config (toml_config )
226+ _ , entries , _ = _load_toml_config (toml_config )
203227 if entries :
204228 yield from entries
205229 return
@@ -224,6 +248,8 @@ def _parse_toml_config(config: dict[str, Any]) -> Generator[Entry, None, None]:
224248 for key , entry in config .items ():
225249 if key == "env_files" and isinstance (entry , list ):
226250 continue
251+ if key == "env_files_skip_if_set" and isinstance (entry , bool ):
252+ continue
227253 if isinstance (entry , dict ):
228254 unset = bool (entry .get ("unset" ))
229255 value = str (entry .get ("value" , "" )) if not unset else ""
0 commit comments