1616from dataclasses import dataclass
1717from multiprocessing .pool import ThreadPool
1818from typing import Any , Collection , DefaultDict , Iterable , Iterator , Mapping
19+ from uuid import uuid4
1920
2021from dev_cmd import __version__ , color , parse , venv
2122from dev_cmd .color import ColorChoice
3536 VenvConfig ,
3637)
3738from dev_cmd .parse import parse_dev_config
38- from dev_cmd .placeholder import DEFAULT_ENVIRONMENT
39+ from dev_cmd .placeholder import Environment
3940from dev_cmd .project import find_pyproject_toml
4041
4142DEFAULT_EXIT_STYLE = ExitStyle .AFTER_STEP
@@ -208,12 +209,19 @@ class Options:
208209 quiet : bool
209210 parallel : bool
210211 timings : bool
212+ hashseed : int
211213 extra_args : tuple [str , ...]
212214 python : str | None = None
213215 exit_style : ExitStyle | None = None
214216 grace_period : float | None = None
215217
216218
219+ def _random_hashseed () -> int :
220+ # The PYTHONHASHSEED is an integer in the range 0 to 4294967295. We use the time_low field of
221+ # the UUID which is 32 bits.
222+ return min (4294967295 , uuid4 ().time_low )
223+
224+
217225def _parse_args () -> Options :
218226 parser = ArgumentParser (
219227 description = (
@@ -263,6 +271,12 @@ def _parse_args() -> Options:
263271 action = "store_true" ,
264272 help = "Emit timing information for each command run." ,
265273 )
274+ parser .add_argument (
275+ "--hashseed" ,
276+ type = int ,
277+ default = _random_hashseed (),
278+ help = "Set the {--hashseed} command placeholder value." ,
279+ )
266280
267281 if venv .AVAILABLE :
268282 parser .add_argument (
@@ -373,6 +387,7 @@ def _parse_args() -> Options:
373387 quiet = options .quiet ,
374388 parallel = parallel ,
375389 timings = options .timings ,
390+ hashseed = options .hashseed ,
376391 extra_args = tuple (extra_args ) if extra_args is not None else (),
377392 python = getattr (options , "python" , None ),
378393 exit_style = options .exit_style ,
@@ -383,6 +398,7 @@ def _parse_args() -> Options:
383398def _list (
384399 console , # type: Console
385400 config , # type: Configuration
401+ placeholder_env , # type: Environment
386402):
387403 # type: (...) -> Any
388404
@@ -423,11 +439,11 @@ def _list(
423439 extra_info = ""
424440 if flag_value is not None :
425441 extra_info = f"{ flag_value } "
426- substituted_flag_value = DEFAULT_ENVIRONMENT .substitute (flag_value ).value
442+ substituted_flag_value = placeholder_env .substitute (flag_value ).value
427443 if substituted_flag_value != flag_value :
428444 extra_info += f"(currently { substituted_flag_value } ) "
429445 if default is not None :
430- substituted_default = DEFAULT_ENVIRONMENT .substitute (default ).value
446+ substituted_default = placeholder_env .substitute (default ).value
431447 if substituted_default != default :
432448 extra_info += f"[default: { default } (currently { substituted_default } )]"
433449 else :
@@ -476,14 +492,17 @@ def main() -> Any:
476492 options = _parse_args ()
477493 console = Console (quiet = options .quiet )
478494 python = Python (options .python ) if options .python else None
495+ placeholder_env = Environment (hashseed = options .hashseed )
479496 try :
480497 pyproject_toml = find_pyproject_toml ()
481- config , steps = parse_dev_config (pyproject_toml , * options .steps , requested_python = python )
498+ config , steps = parse_dev_config (
499+ pyproject_toml , * options .steps , placeholder_env = placeholder_env , requested_python = python
500+ )
482501 except DevCmdError as e :
483502 return 1 if console .quiet else f"{ color .red ('Configuration error' )} : { color .yellow (str (e ))} "
484503
485504 if options .list :
486- return _list (console , config )
505+ return _list (console , config , placeholder_env )
487506
488507 success = False
489508 try :
0 commit comments