From 1d352d6624a3f420950dbc57ff7344afbe366115 Mon Sep 17 00:00:00 2001 From: Liam Clark Date: Fri, 6 Dec 2024 11:46:26 -0800 Subject: [PATCH 1/4] added more docstrings and comments to the file src/_pytest/main.py from lines 250-455 --- src/_pytest/main.py | 156 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 129 insertions(+), 27 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 26b0db74ff9..bc5c0f1edbe 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -248,24 +248,42 @@ def pytest_addoption(parser: Parser) -> None: def validate_basetemp(path: str) -> str: - # GH 7119 + """ + Validate that the given base temp directory path is not empty, + the current working directory, or any of its parent directories. + + :param path: The directory path to validate. + :type path: str + :return: The validated directory path. + :rtype: str + :raises argparse.ArgumentTypeError: If the path is invalid. + """ msg = "basetemp must not be empty, the current working directory or any parent directory of it" - # empty path + # Check if the provided path is empty if not path: raise argparse.ArgumentTypeError(msg) def is_ancestor(base: Path, query: Path) -> bool: - """Return whether query is an ancestor of base.""" + """ + Determine if a query path is an ancestor of the base path. + + :param base: The base path to check. + :type base: Path + :param query: The query path to compare against. + :type query: Path + :return: True if the query is an ancestor of the base path, False otherwise. + :rtype: bool + """ if base == query: return True return query in base.parents - # check if path is an ancestor of cwd + # Check if the path is an ancestor of the current working directory if is_ancestor(Path.cwd(), Path(path).absolute()): raise argparse.ArgumentTypeError(msg) - # check symlinks for ancestors + # Check if the resolved path (following symlinks) is an ancestor if is_ancestor(Path.cwd().resolve(), Path(path).resolve()): raise argparse.ArgumentTypeError(msg) @@ -275,12 +293,22 @@ def is_ancestor(base: Path, query: Path) -> bool: def wrap_session( config: Config, doit: Callable[[Config, Session], int | ExitCode | None] ) -> int | ExitCode: - """Skeleton command line program.""" + """ + Execute a session wrapped with proper setup and teardown logic. + + :param config: Configuration object for the session. + :type config: Config + :param doit: Callable function to execute within the session. + :type doit: Callable[[Config, Session], int | ExitCode | None] + :return: The session's exit status. + :rtype: int | ExitCode + """ session = Session.from_config(config) session.exitstatus = ExitCode.OK initstate = 0 try: try: + # Configure the session config._do_configure() initstate = 1 config.hook.pytest_sessionstart(session=session) @@ -292,6 +320,7 @@ def wrap_session( except Failed: session.exitstatus = ExitCode.TESTS_FAILED except (KeyboardInterrupt, exit.Exception): + # Handle interruptions excinfo = _pytest._code.ExceptionInfo.from_current() exitstatus: int | ExitCode = ExitCode.INTERRUPTED if isinstance(excinfo.value, exit.Exception): @@ -302,6 +331,7 @@ def wrap_session( config.hook.pytest_keyboard_interrupt(excinfo=excinfo) session.exitstatus = exitstatus except BaseException: + # Handle unexpected exceptions session.exitstatus = ExitCode.INTERNAL_ERROR excinfo = _pytest._code.ExceptionInfo.from_current() try: @@ -313,10 +343,9 @@ def wrap_session( else: if isinstance(excinfo.value, SystemExit): sys.stderr.write("mainloop: caught unexpected SystemExit!\n") - finally: - # Explicitly break reference cycle. - excinfo = None # type: ignore + # Cleanup and unconfigure session + excinfo = None # Explicitly break reference cycle os.chdir(session.startpath) if initstate >= 2: try: @@ -332,37 +361,74 @@ def wrap_session( def pytest_cmdline_main(config: Config) -> int | ExitCode: + """ + Main command-line entry point for pytest. + + :param config: Configuration object. + :type config: Config + :return: Exit code of the pytest execution. + :rtype: int | ExitCode + """ return wrap_session(config, _main) def _main(config: Config, session: Session) -> int | ExitCode | None: - """Default command line protocol for initialization, session, - running tests and reporting.""" + """ + Default pytest protocol for initialization, running tests, and reporting. + + :param config: Configuration object. + :type config: Config + :param session: Session object for the test run. + :type session: Session + :return: Exit code based on test outcomes. + :rtype: int | ExitCode | None + """ + # Run collection and test loop hooks config.hook.pytest_collection(session=session) config.hook.pytest_runtestloop(session=session) + # Return appropriate exit code based on test results if session.testsfailed: return ExitCode.TESTS_FAILED elif session.testscollected == 0: return ExitCode.NO_TESTS_COLLECTED return None - def pytest_collection(session: Session) -> None: + """ + Perform the collection phase of the pytest session. + + :param session: The pytest session object. + :type session: Session + """ + # Trigger the collection process to gather tests. session.perform_collect() def pytest_runtestloop(session: Session) -> bool: + """ + Run the collected tests in a loop. + + :param session: The pytest session object containing the collected tests. + :type session: Session + :return: True if the test loop completes successfully. + :rtype: bool + :raises session.Interrupted: If an error or stop condition occurs during test execution. + """ + # Check if there were collection errors and if those should stop further testing if session.testsfailed and not session.config.option.continue_on_collection_errors: raise session.Interrupted( f"{session.testsfailed} error{'s' if session.testsfailed != 1 else ''} during collection" ) + # If the user only requested collection (no execution), stop here. if session.config.option.collectonly: return True + # Iterate through collected tests and execute them for i, item in enumerate(session.items): nextitem = session.items[i + 1] if i + 1 < len(session.items) else None + # Call the test protocol hook for each test item item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) if session.shouldfail: raise session.Failed(session.shouldfail) @@ -372,56 +438,71 @@ def pytest_runtestloop(session: Session) -> bool: def _in_venv(path: Path) -> bool: - """Attempt to detect if ``path`` is the root of a Virtual Environment by - checking for the existence of the pyvenv.cfg file. - - [https://peps.python.org/pep-0405/] - - For regression protection we also check for conda environments that do not include pyenv.cfg yet -- - https://github.com/conda/conda/issues/13337 is the conda issue tracking adding pyenv.cfg. + """ + Check if the given path is the root of a virtual environment. - Checking for the `conda-meta/history` file per https://github.com/pytest-dev/pytest/issues/12652#issuecomment-2246336902. + This is done by checking for specific files such as `pyvenv.cfg` (PEP 405) + or Conda environment metadata. + :param path: The path to check. + :type path: Path + :return: True if the path is a virtual environment root, False otherwise. + :rtype: bool """ try: + # Look for standard virtual environment or Conda metadata files return ( path.joinpath("pyvenv.cfg").is_file() or path.joinpath("conda-meta", "history").is_file() ) except OSError: + # Return False if an OSError occurs (e.g., permission issues) return False def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None: + """ + Determine if a given path should be ignored during test collection. + + This function checks for ignored paths, globs, and virtual environment roots. + + :param collection_path: Path to the file or directory being checked. + :type collection_path: Path + :param config: The pytest configuration object. + :type config: Config + :return: True if the path should be ignored, None otherwise. + :rtype: bool | None + """ + # Ignore `__pycache__` directories by default if collection_path.name == "__pycache__": return True + # Check for paths explicitly marked as ignored ignore_paths = config._getconftest_pathlist( "collect_ignore", path=collection_path.parent - ) - ignore_paths = ignore_paths or [] + ) or [] excludeopt = config.getoption("ignore") if excludeopt: ignore_paths.extend(absolutepath(x) for x in excludeopt) - if collection_path in ignore_paths: return True + # Check for ignored paths using glob patterns ignore_globs = config._getconftest_pathlist( "collect_ignore_glob", path=collection_path.parent - ) - ignore_globs = ignore_globs or [] + ) or [] excludeglobopt = config.getoption("ignore_glob") if excludeglobopt: ignore_globs.extend(absolutepath(x) for x in excludeglobopt) - if any(fnmatch.fnmatch(str(collection_path), str(glob)) for glob in ignore_globs): return True + # Ignore virtual environment roots unless explicitly allowed allow_in_venv = config.getoption("collect_in_virtualenv") if not allow_in_venv and _in_venv(collection_path): return True + # Check if the directory matches patterns for non-recursion if collection_path.is_dir(): norecursepatterns = config.getini("norecursedirs") if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns): @@ -433,27 +514,48 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None: def pytest_collect_directory( path: Path, parent: nodes.Collector ) -> nodes.Collector | None: + """ + Create a collector node for a directory during test collection. + + :param path: The directory path being collected. + :type path: Path + :param parent: The parent collector node. + :type parent: nodes.Collector + :return: A collector node for the directory. + :rtype: nodes.Collector | None + """ + # Create a directory collector node from the parent return Dir.from_parent(parent, path=path) def pytest_collection_modifyitems(items: list[nodes.Item], config: Config) -> None: + """ + Modify the list of collected items, deselecting items based on prefixes. + + :param items: List of collected test items. + :type items: list[nodes.Item] + :param config: The pytest configuration object. + :type config: Config + """ + # Get deselection prefixes from configuration deselect_prefixes = tuple(config.getoption("deselect") or []) if not deselect_prefixes: return remaining = [] deselected = [] + # Separate items into deselected and remaining based on prefixes for colitem in items: if colitem.nodeid.startswith(deselect_prefixes): deselected.append(colitem) else: remaining.append(colitem) + # Notify about deselected items and update the remaining items list if deselected: config.hook.pytest_deselected(items=deselected) items[:] = remaining - class FSHookProxy: def __init__( self, From 7ebf887c078bbc459dca95ffff5a0f9f2ee56fa8 Mon Sep 17 00:00:00 2001 From: Liam Clark Date: Fri, 6 Dec 2024 12:33:19 -0800 Subject: [PATCH 2/4] added inline comments and made sure code was the same --- src/_pytest/main.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index bc5c0f1edbe..19e3d5fe4b7 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -473,35 +473,35 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None: :return: True if the path should be ignored, None otherwise. :rtype: bool | None """ - # Ignore `__pycache__` directories by default if collection_path.name == "__pycache__": return True - + # Check for paths explicitly marked as ignored ignore_paths = config._getconftest_pathlist( "collect_ignore", path=collection_path.parent - ) or [] + ) + ignore_paths = ignore_paths or [] excludeopt = config.getoption("ignore") if excludeopt: ignore_paths.extend(absolutepath(x) for x in excludeopt) + if collection_path in ignore_paths: return True - # Check for ignored paths using glob patterns ignore_globs = config._getconftest_pathlist( "collect_ignore_glob", path=collection_path.parent - ) or [] + ) + ignore_globs = ignore_globs or [] excludeglobopt = config.getoption("ignore_glob") if excludeglobopt: ignore_globs.extend(absolutepath(x) for x in excludeglobopt) + if any(fnmatch.fnmatch(str(collection_path), str(glob)) for glob in ignore_globs): return True - # Ignore virtual environment roots unless explicitly allowed allow_in_venv = config.getoption("collect_in_virtualenv") if not allow_in_venv and _in_venv(collection_path): return True - # Check if the directory matches patterns for non-recursion if collection_path.is_dir(): norecursepatterns = config.getini("norecursedirs") From 07c51724803dcfda3c6c17fb4d6cf623a9a0da8d Mon Sep 17 00:00:00 2001 From: Liam Clark Date: Fri, 6 Dec 2024 12:35:49 -0800 Subject: [PATCH 3/4] undid some documentation and kept orginal --- src/_pytest/main.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 19e3d5fe4b7..4d467350399 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -438,25 +438,23 @@ def pytest_runtestloop(session: Session) -> bool: def _in_venv(path: Path) -> bool: - """ - Check if the given path is the root of a virtual environment. + """Attempt to detect if ``path`` is the root of a Virtual Environment by + checking for the existence of the pyvenv.cfg file. - This is done by checking for specific files such as `pyvenv.cfg` (PEP 405) - or Conda environment metadata. + [https://peps.python.org/pep-0405/] + + For regression protection we also check for conda environments that do not include pyenv.cfg yet -- + https://github.com/conda/conda/issues/13337 is the conda issue tracking adding pyenv.cfg. + + Checking for the `conda-meta/history` file per https://github.com/pytest-dev/pytest/issues/12652#issuecomment-2246336902. - :param path: The path to check. - :type path: Path - :return: True if the path is a virtual environment root, False otherwise. - :rtype: bool """ try: - # Look for standard virtual environment or Conda metadata files return ( path.joinpath("pyvenv.cfg").is_file() or path.joinpath("conda-meta", "history").is_file() ) except OSError: - # Return False if an OSError occurs (e.g., permission issues) return False From 388d507dac33b3d2b7d5a49994b2d5d379dd72e2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 23:55:05 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 4d467350399..b6c24b08a6c 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -394,6 +394,7 @@ def _main(config: Config, session: Session) -> int | ExitCode | None: return ExitCode.NO_TESTS_COLLECTED return None + def pytest_collection(session: Session) -> None: """ Perform the collection phase of the pytest session. @@ -473,7 +474,7 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None: """ if collection_path.name == "__pycache__": return True - + # Check for paths explicitly marked as ignored ignore_paths = config._getconftest_pathlist( "collect_ignore", path=collection_path.parent @@ -554,6 +555,7 @@ def pytest_collection_modifyitems(items: list[nodes.Item], config: Config) -> No config.hook.pytest_deselected(items=deselected) items[:] = remaining + class FSHookProxy: def __init__( self,