parser.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. from __future__ import annotations
  2. import os
  3. from argparse import SUPPRESS, ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace
  4. from collections import OrderedDict
  5. from typing import TYPE_CHECKING, Any
  6. if TYPE_CHECKING:
  7. from argparse import Action
  8. from collections.abc import Mapping, Sequence
  9. from virtualenv.config.convert import get_type
  10. from virtualenv.config.env_var import get_env_var
  11. from virtualenv.config.ini import IniConfig
  12. class VirtualEnvOptions(Namespace):
  13. def __init__(self, **kwargs: Any) -> None: # noqa: ANN401
  14. super().__init__(**kwargs)
  15. self._src: str | None = None
  16. self._sources: dict[str, str] = {}
  17. def set_src(self, key: str, value: Any, src: str) -> None: # noqa: ANN401
  18. """Set an option value and record where it came from.
  19. :param key: the option name
  20. :param value: the option value
  21. :param src: the source of the value (e.g. ``"cli"``, ``"env var"``, ``"default"``)
  22. """
  23. setattr(self, key, value)
  24. if src.startswith("env var"):
  25. src = "env var"
  26. self._sources[key] = src
  27. def __setattr__(self, key: str, value: Any) -> None: # noqa: ANN401
  28. if (src := getattr(self, "_src", None)) is not None:
  29. self._sources[key] = src
  30. super().__setattr__(key, value)
  31. def get_source(self, key: str) -> str | None:
  32. """Return the source that provided a given option value.
  33. :param key: the option name
  34. :returns: the source string (e.g. ``"cli"``, ``"env var"``, ``"default"``), or ``None`` if not tracked
  35. """
  36. return self._sources.get(key)
  37. @property
  38. def verbosity(self) -> int | None:
  39. """The verbosity level, computed as ``verbose - quiet``, clamped to zero.
  40. :returns: the verbosity level, or ``None`` if neither ``--verbose`` nor ``--quiet`` has been parsed yet
  41. """
  42. if not hasattr(self, "verbose") and not hasattr(self, "quiet"):
  43. return None
  44. return max(self.verbose - self.quiet, 0)
  45. def __repr__(self) -> str:
  46. return f"{type(self).__name__}({', '.join(f'{k}={v}' for k, v in vars(self).items() if not k.startswith('_'))})"
  47. class VirtualEnvConfigParser(ArgumentParser):
  48. """Custom option parser which updates its defaults by checking the configuration files and environmental vars."""
  49. def __init__(
  50. self,
  51. options: VirtualEnvOptions | None = None,
  52. env: Mapping[str, str] | None = None,
  53. *args: Any, # noqa: ANN401
  54. **kwargs: Any, # noqa: ANN401
  55. ) -> None:
  56. env = os.environ if env is None else env
  57. self.file_config = IniConfig(env)
  58. self.epilog_list = []
  59. self.env = env
  60. kwargs["epilog"] = self.file_config.epilog
  61. kwargs["add_help"] = False
  62. kwargs["formatter_class"] = HelpFormatter
  63. kwargs["prog"] = "virtualenv"
  64. super().__init__(*args, **kwargs)
  65. self._fixed = set()
  66. if options is not None and not isinstance(options, VirtualEnvOptions):
  67. msg = "options must be of type VirtualEnvOptions"
  68. raise TypeError(msg)
  69. self.options = VirtualEnvOptions() if options is None else options
  70. self._interpreter = None
  71. self._app_data = None
  72. def _fix_defaults(self) -> None:
  73. for action in self._actions:
  74. action_id = id(action)
  75. if action_id not in self._fixed:
  76. self._fix_default(action)
  77. self._fixed.add(action_id)
  78. def _fix_default(self, action: Action) -> None:
  79. if hasattr(action, "default") and hasattr(action, "dest") and action.default != SUPPRESS:
  80. as_type = get_type(action)
  81. names = OrderedDict((i.lstrip("-").replace("-", "_"), None) for i in action.option_strings)
  82. outcome = None
  83. for name in names:
  84. outcome = get_env_var(name, as_type, self.env)
  85. if outcome is not None:
  86. break
  87. if outcome is None and self.file_config:
  88. for name in names:
  89. outcome = self.file_config.get(name, as_type)
  90. if outcome is not None:
  91. break
  92. if outcome is not None:
  93. action.default, action.default_source = outcome
  94. else:
  95. outcome = action.default, "default"
  96. self.options.set_src(action.dest, *outcome)
  97. def enable_help(self) -> None:
  98. self._fix_defaults()
  99. self.add_argument("-h", "--help", action="help", default=SUPPRESS, help="show this help message and exit")
  100. def parse_known_args(
  101. self, args: Sequence[str] | None = None, namespace: VirtualEnvOptions | None = None
  102. ) -> tuple[VirtualEnvOptions, list[str]]:
  103. if namespace is None:
  104. namespace = self.options
  105. elif namespace is not self.options:
  106. msg = "can only pass in parser.options"
  107. raise ValueError(msg)
  108. self._fix_defaults()
  109. self.options._src = "cli" # noqa: SLF001
  110. try:
  111. namespace.env = self.env
  112. return super().parse_known_args(args, namespace=namespace)
  113. finally:
  114. self.options._src = None # noqa: SLF001
  115. class HelpFormatter(ArgumentDefaultsHelpFormatter):
  116. def __init__(self, prog: str, **kwargs: Any) -> None: # noqa: ANN401
  117. super().__init__(prog, max_help_position=32, width=240, **kwargs)
  118. def _get_help_string(self, action: Action) -> str | None:
  119. text = super()._get_help_string(action)
  120. if text is not None and hasattr(action, "default_source"):
  121. default = " (default: %(default)s)"
  122. if text.endswith(default):
  123. text = f"{text[: -len(default)]} (default: %(default)s -> from %(default_source)s)"
  124. return text
  125. __all__ = [
  126. "HelpFormatter",
  127. "VirtualEnvConfigParser",
  128. "VirtualEnvOptions",
  129. ]