-
-
Notifications
You must be signed in to change notification settings - Fork 3k
fix: show correct program name in argparse help/errors #14126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Improved argparse program name to show ``pytest``, ``python -m pytest``, or ``pytest.main()`` based on how pytest was invoked, making help and error messages clearer. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -169,9 +169,33 @@ def print_usage_error(e: UsageError, file: TextIO) -> None: | |
| tw.line(f"ERROR: {msg}\n", red=True) | ||
|
|
||
|
|
||
| def _get_prog_name( | ||
| *, invoked_from_console: bool, _argv: list[str] | None = None | ||
| ) -> str: | ||
| """Determine the appropriate program name for argparse based on invocation context. | ||
|
|
||
| :param invoked_from_console: Whether pytest was invoked from the CLI entry point. | ||
| :param _argv: Optional argv list for testing; defaults to sys.argv. | ||
| :returns: The program name to display in help and error messages. | ||
| """ | ||
| if not invoked_from_console: | ||
| # Called programmatically via pytest.main() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would drop this comment as it's pretty clear without it. |
||
| return "pytest.main()" | ||
|
|
||
| # Called from CLI - check if it's `python -m pytest` or direct `pytest` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would drop this comment. |
||
| argv = sys.argv if _argv is None else _argv | ||
| argv0 = argv[0] if argv else "" | ||
| # When running as `python -m pytest`, argv[0] is the path to __main__.py | ||
| if os.path.basename(argv0) == "__main__.py": | ||
| return "python -m pytest" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems a bit odd to me, is it really necessary to distinguish
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. theres sys.path differences between the 2 |
||
| return "pytest" | ||
|
|
||
|
|
||
| def main( | ||
| args: list[str] | os.PathLike[str] | None = None, | ||
| plugins: Sequence[str | _PluggyPlugin] | None = None, | ||
| *, | ||
| _invoked_from_console: bool = False, | ||
|
||
| ) -> int | ExitCode: | ||
| """Perform an in-process test run. | ||
|
|
||
|
|
@@ -188,11 +212,13 @@ def main( | |
| sys.stdout.write(f"pytest {__version__}\n") | ||
| return ExitCode.OK | ||
|
|
||
| prog = _get_prog_name(invoked_from_console=_invoked_from_console) | ||
|
|
||
| old_pytest_version = os.environ.get("PYTEST_VERSION") | ||
| try: | ||
| os.environ["PYTEST_VERSION"] = __version__ | ||
| try: | ||
| config = _prepareconfig(new_args, plugins) | ||
| config = _prepareconfig(new_args, plugins, prog=prog) | ||
| except ConftestImportFailure as e: | ||
| print_conftest_import_error(e, file=sys.stderr) | ||
| return ExitCode.USAGE_ERROR | ||
|
|
@@ -222,7 +248,7 @@ def console_main() -> int: | |
| """ | ||
| # https://docs.python.org/3/library/signal.html#note-on-sigpipe | ||
| try: | ||
| code = main() | ||
| code = main(_invoked_from_console=True) | ||
| sys.stdout.flush() | ||
| return code | ||
| except BrokenPipeError: | ||
|
|
@@ -308,6 +334,8 @@ def directory_arg(path: str, optname: str) -> str: | |
| def get_config( | ||
| args: Iterable[str] | None = None, | ||
| plugins: Sequence[str | _PluggyPlugin] | None = None, | ||
| *, | ||
| prog: str | None = None, | ||
| ) -> Config: | ||
| # Subsequent calls to main will create a fresh instance. | ||
| pluginmanager = PytestPluginManager() | ||
|
|
@@ -316,7 +344,7 @@ def get_config( | |
| plugins=plugins, | ||
| dir=pathlib.Path.cwd(), | ||
| ) | ||
| config = Config(pluginmanager, invocation_params=invocation_params) | ||
| config = Config(pluginmanager, invocation_params=invocation_params, prog=prog) | ||
|
|
||
| if invocation_params.args: | ||
| # Handle any "-p no:plugin" args. | ||
|
|
@@ -342,6 +370,8 @@ def get_plugin_manager() -> PytestPluginManager: | |
| def _prepareconfig( | ||
| args: list[str] | os.PathLike[str], | ||
| plugins: Sequence[str | _PluggyPlugin] | None = None, | ||
| *, | ||
| prog: str | None = None, | ||
| ) -> Config: | ||
| if isinstance(args, os.PathLike): | ||
| args = [os.fspath(args)] | ||
|
|
@@ -351,7 +381,7 @@ def _prepareconfig( | |
| ) | ||
| raise TypeError(msg.format(args, type(args))) | ||
|
|
||
| initial_config = get_config(args, plugins) | ||
| initial_config = get_config(args, plugins, prog=prog) | ||
| pluginmanager = initial_config.pluginmanager | ||
| try: | ||
| if plugins: | ||
|
|
@@ -1081,6 +1111,7 @@ def __init__( | |
| pluginmanager: PytestPluginManager, | ||
| *, | ||
| invocation_params: InvocationParams | None = None, | ||
| prog: str | None = None, | ||
| ) -> None: | ||
| if invocation_params is None: | ||
| invocation_params = self.InvocationParams( | ||
|
|
@@ -1104,6 +1135,8 @@ def __init__( | |
| processopt=self._processopt, | ||
| _ispytest=True, | ||
| ) | ||
| if prog is not None: | ||
| self._parser.prog = prog | ||
|
Comment on lines
+1138
to
+1139
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't it be better to add a |
||
| self.pluginmanager = pluginmanager | ||
| """The plugin manager handles plugin registration and hook invocation. | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would just make
argva required argument and passsys.argvwhere needed instead of making it optional.Also, since the list is not modified, should type is as
Sequence[str].