from __future__ import annotations import os from argparse import SUPPRESS, ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace from collections import OrderedDict from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from argparse import Action from collections.abc import Mapping, Sequence from virtualenv.config.convert import get_type from virtualenv.config.env_var import get_env_var from virtualenv.config.ini import IniConfig class VirtualEnvOptions(Namespace): def __init__(self, **kwargs: Any) -> None: # noqa: ANN401 super().__init__(**kwargs) self._src: str | None = None self._sources: dict[str, str] = {} def set_src(self, key: str, value: Any, src: str) -> None: # noqa: ANN401 """Set an option value and record where it came from. :param key: the option name :param value: the option value :param src: the source of the value (e.g. ``"cli"``, ``"env var"``, ``"default"``) """ setattr(self, key, value) if src.startswith("env var"): src = "env var" self._sources[key] = src def __setattr__(self, key: str, value: Any) -> None: # noqa: ANN401 if (src := getattr(self, "_src", None)) is not None: self._sources[key] = src super().__setattr__(key, value) def get_source(self, key: str) -> str | None: """Return the source that provided a given option value. :param key: the option name :returns: the source string (e.g. ``"cli"``, ``"env var"``, ``"default"``), or ``None`` if not tracked """ return self._sources.get(key) @property def verbosity(self) -> int | None: """The verbosity level, computed as ``verbose - quiet``, clamped to zero. :returns: the verbosity level, or ``None`` if neither ``--verbose`` nor ``--quiet`` has been parsed yet """ if not hasattr(self, "verbose") and not hasattr(self, "quiet"): return None return max(self.verbose - self.quiet, 0) def __repr__(self) -> str: return f"{type(self).__name__}({', '.join(f'{k}={v}' for k, v in vars(self).items() if not k.startswith('_'))})" class VirtualEnvConfigParser(ArgumentParser): """Custom option parser which updates its defaults by checking the configuration files and environmental vars.""" def __init__( self, options: VirtualEnvOptions | None = None, env: Mapping[str, str] | None = None, *args: Any, # noqa: ANN401 **kwargs: Any, # noqa: ANN401 ) -> None: env = os.environ if env is None else env self.file_config = IniConfig(env) self.epilog_list = [] self.env = env kwargs["epilog"] = self.file_config.epilog kwargs["add_help"] = False kwargs["formatter_class"] = HelpFormatter kwargs["prog"] = "virtualenv" super().__init__(*args, **kwargs) self._fixed = set() if options is not None and not isinstance(options, VirtualEnvOptions): msg = "options must be of type VirtualEnvOptions" raise TypeError(msg) self.options = VirtualEnvOptions() if options is None else options self._interpreter = None self._app_data = None def _fix_defaults(self) -> None: for action in self._actions: action_id = id(action) if action_id not in self._fixed: self._fix_default(action) self._fixed.add(action_id) def _fix_default(self, action: Action) -> None: if hasattr(action, "default") and hasattr(action, "dest") and action.default != SUPPRESS: as_type = get_type(action) names = OrderedDict((i.lstrip("-").replace("-", "_"), None) for i in action.option_strings) outcome = None for name in names: outcome = get_env_var(name, as_type, self.env) if outcome is not None: break if outcome is None and self.file_config: for name in names: outcome = self.file_config.get(name, as_type) if outcome is not None: break if outcome is not None: action.default, action.default_source = outcome else: outcome = action.default, "default" self.options.set_src(action.dest, *outcome) def enable_help(self) -> None: self._fix_defaults() self.add_argument("-h", "--help", action="help", default=SUPPRESS, help="show this help message and exit") def parse_known_args( self, args: Sequence[str] | None = None, namespace: VirtualEnvOptions | None = None ) -> tuple[VirtualEnvOptions, list[str]]: if namespace is None: namespace = self.options elif namespace is not self.options: msg = "can only pass in parser.options" raise ValueError(msg) self._fix_defaults() self.options._src = "cli" # noqa: SLF001 try: namespace.env = self.env return super().parse_known_args(args, namespace=namespace) finally: self.options._src = None # noqa: SLF001 class HelpFormatter(ArgumentDefaultsHelpFormatter): def __init__(self, prog: str, **kwargs: Any) -> None: # noqa: ANN401 super().__init__(prog, max_help_position=32, width=240, **kwargs) def _get_help_string(self, action: Action) -> str | None: text = super()._get_help_string(action) if text is not None and hasattr(action, "default_source"): default = " (default: %(default)s)" if text.endswith(default): text = f"{text[: -len(default)]} (default: %(default)s -> from %(default_source)s)" return text __all__ = [ "HelpFormatter", "VirtualEnvConfigParser", "VirtualEnvOptions", ]