Skip to content
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

Many tests fails with generator raised StopIteration #200

Closed
mtelka opened this issue Sep 9, 2024 · 19 comments · Fixed by #203
Closed

Many tests fails with generator raised StopIteration #200

mtelka opened this issue Sep 9, 2024 · 19 comments · Fixed by #203

Comments

@mtelka
Copy link

mtelka commented Sep 9, 2024

I'm seeing 131 tests to fail like this:

FAILED tests/test_pre_compile.py::test_invalid_examples_api[cibuildwheel/overrides-noselect.toml-cli_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_examples_api[setuptools/07-pyproject.toml-cli_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_invalid_examples_api[cibuildwheel/overrides-noselect.toml-api_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_invalid_examples_api[setuptools/package-dir/invalid-name.toml-api_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_invalid_examples_api[localtool/fail2.toml-cli_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_examples_api[atoml/pyproject.toml-cli_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_invalid_examples_api[pep621/missing-fields/missing-version.toml-cli_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_invalid_examples_api[setuptools/dependencies/invalid-extra-name.toml-api_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_examples_api[localtool/working.toml-cli_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_invalid_examples_api[setuptools/packages/missing-find-arguments.toml-api_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_examples_api[ruff/modern.toml-cli_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_examples_api[setuptools/06-pyproject.toml-cli_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_invalid_examples_api[store/ruff-unknown.toml-api_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_invalid_examples_api[setuptools/pep621/license/both-text-and-file.toml-api_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_invalid_examples_api[cibuildwheel/unknown-option.toml-api_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_invalid_examples_api[setuptools/cmdclass/invalid-value.toml-api_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_examples_api[cibuildwheel/default.toml-cli_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_invalid_examples_api[setuptools/dynamic/readme-too-many.toml-cli_pre_compile] - RuntimeError: generator raised StopIteration
FAILED tests/test_pre_compile.py::test_examples_api[pep_text/pyproject.toml-api_pre_compile] - RuntimeError: generator raised StopIteration
=========================================== 131 failed, 352 passed, 1 skipped in 361.60s (0:06:01) ===========================================

I tested both the 0.19 sdist and the latest main git branch using tox -e py39.

I found that when I do this: touch src/LICENSE src/LICENSE.txt then all tests pass.

@mtelka
Copy link
Author

mtelka commented Sep 9, 2024

Using git bisect I found that the issue was introduced by 9868013.

@mtelka
Copy link
Author

mtelka commented Sep 9, 2024

It looks like the proper fix for the issue is this:

$ git diff .
diff --git a/pyproject.toml b/pyproject.toml
index 13fa1a1..239424c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -78,7 +78,7 @@ norecursedirs = [
     "build",
     ".tox",
 ]
-testpaths = ["src", "tests"]
+testpaths = ["tests"]
 
 [tool.mypy]
 show_traceback = true
$

@henryiii
Copy link
Collaborator

henryiii commented Sep 9, 2024

Would you like to make the PR?

@abravalheri
Copy link
Owner

@mtelka do you have the verbose traces for the error so we can investigate the error cause?

@mtelka
Copy link
Author

mtelka commented Sep 9, 2024

@abravalheri, here it is:

_ test_invalid_examples_api[setuptools/packages/invalid-stub-name.toml-cli_pre_compile] _

tmp_path = PosixPath('/tmp/pytest-of-marcel/pytest-62/test_invalid_examples_api_setu0')
pre_compiled_validate = <function pre_compiled_validate.<locals>._validate at 0x7fffaa655dc0>
invalid_example = PosixPath('/data/builds/validate-pyproject/tests/invalid-examples/setuptools/packages/invalid-stub-name.toml')
pre_compiled = <function cli_pre_compile at 0x7fffad5ff5e0>

    @pytest.mark.parametrize("pre_compiled", _PRE_COMPILED)
    def test_invalid_examples_api(
        tmp_path, pre_compiled_validate, invalid_example, pre_compiled
    ):  
        expected_error = error_file(invalid_example).read_text("utf-8")
        toml_equivalent = tomllib.loads(invalid_example.read_text())
>       pre_compiled_path = pre_compiled(Path(tmp_path), example=invalid_example)

tests/test_pre_compile.py:163:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests/test_pre_compile.py:108: in cli_pre_compile
    cli.run([*args, "-O", str(path)])
src/validate_pyproject/pre_compile/cli.py:108: in run
    pre_compile(
src/validate_pyproject/pre_compile/__init__.py:55: in pre_compile
    write_notice(out, main_file, original_cmd, replacements)
src/validate_pyproject/pre_compile/__init__.py:95: in write_notice
    notice = notice.format(notice=opening, main_file=main_file, **load_licenses())
src/validate_pyproject/pre_compile/__init__.py:104: in load_licenses
    "validate_pyproject_license": _find_and_load_licence(_M.files(dist_name)),
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

files = []

    def _find_and_load_licence(files: Optional[Sequence[_M.PackagePath]]) -> str:
        if files is None:  # pragma: no cover
            raise ImportError("Could not find LICENSE for package")
        try:
>           return next(f for f in files if f.stem.upper() == "LICENSE").read_text("UTF-8")
E           StopIteration

src/validate_pyproject/pre_compile/__init__.py:124: StopIteration

The above exception was the direct cause of the following exception:

cls = <class '_pytest.runner.CallInfo'>
func = <function call_and_report.<locals>.<lambda> at 0x7fffa3f19a60>
when = 'call'
reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    @classmethod
    def from_call(
        cls,
        func: Callable[[], TResult],
        when: Literal["collect", "setup", "call", "teardown"],
        reraise: type[BaseException] | tuple[type[BaseException], ...] | None = None,
    ) -> CallInfo[TResult]:
        """Call func, wrapping the result in a CallInfo.

        :param func:
            The function to call. Called without arguments.
        :type func: Callable[[], _pytest.runner.TResult]
        :param when:
            The phase in which the function is called.
        :param reraise:
            Exception or exceptions that shall propagate if raised by the
            function, instead of being wrapped in the CallInfo.
        """
        excinfo = None
        start = timing.time()
        precise_start = timing.perf_counter()
        try:
>           result: TResult | None = func()

.tox/py39/lib/python3.9/site-packages/_pytest/runner.py:341:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.tox/py39/lib/python3.9/site-packages/_pytest/runner.py:242: in <lambda>
    lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
.tox/py39/lib/python3.9/site-packages/pluggy/_hooks.py:513: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
.tox/py39/lib/python3.9/site-packages/pluggy/_manager.py:120: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
.tox/py39/lib/python3.9/site-packages/_pytest/threadexception.py:92: in pytest_runtest_call
    yield from thread_exception_runtest_hook()
.tox/py39/lib/python3.9/site-packages/_pytest/threadexception.py:68: in thread_exception_runtest_hook
    yield
.tox/py39/lib/python3.9/site-packages/_pytest/unraisableexception.py:95: in pytest_runtest_call
    yield from unraisable_exception_runtest_hook()
.tox/py39/lib/python3.9/site-packages/_pytest/unraisableexception.py:70: in unraisable_exception_runtest_hook
    yield
.tox/py39/lib/python3.9/site-packages/_pytest/logging.py:848: in pytest_runtest_call
    yield from self._runtest_for(item, "call")
.tox/py39/lib/python3.9/site-packages/_pytest/logging.py:831: in _runtest_for
    yield
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <CaptureManager _method='fd' _global_capturing=<MultiCapture out=<FDCapture 1 oldfd=5 _state='suspended' tmpfile=<_io....xtIOWrapper name='/dev/null' mode='r' encoding='utf-8'>> _state='suspended' _in_suspended=False> _capture_fixture=None>
item = <Function test_invalid_examples_api[setuptools/packages/invalid-stub-name.toml-cli_pre_compile]>

    @hookimpl(wrapper=True)
    def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
        with self.item_capture("call", item):
>           return (yield)
E           RuntimeError: generator raised StopIteration

.tox/py39/lib/python3.9/site-packages/_pytest/capture.py:879: RuntimeError

@mtelka
Copy link
Author

mtelka commented Sep 9, 2024

And here is the same test when run without pytest-randomly:

_ test_invalid_examples_api[setuptools/packages/invalid-stub-name.toml-cli_pre_compile] _

tmp_path = PosixPath('/tmp/pytest-of-marcel/pytest-63/test_invalid_examples_api_setu1')
pre_compiled_validate = <function pre_compiled_validate.<locals>._validate at 0x7fffa47c7d30>
invalid_example = PosixPath('/data/builds/validate-pyproject/tests/invalid-examples/setuptools/packages/invalid-stub-name.toml')
pre_compiled = <function cli_pre_compile at 0x7fffad59cca0>

    @pytest.mark.parametrize("pre_compiled", _PRE_COMPILED)
    def test_invalid_examples_api(
        tmp_path, pre_compiled_validate, invalid_example, pre_compiled
    ):  
        expected_error = error_file(invalid_example).read_text("utf-8")
        toml_equivalent = tomllib.loads(invalid_example.read_text())
>       pre_compiled_path = pre_compiled(Path(tmp_path), example=invalid_example)

tests/test_pre_compile.py:163:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests/test_pre_compile.py:108: in cli_pre_compile
    cli.run([*args, "-O", str(path)])
src/validate_pyproject/pre_compile/cli.py:108: in run
    pre_compile(
src/validate_pyproject/pre_compile/__init__.py:55: in pre_compile
    write_notice(out, main_file, original_cmd, replacements)
src/validate_pyproject/pre_compile/__init__.py:95: in write_notice
    notice = notice.format(notice=opening, main_file=main_file, **load_licenses())
src/validate_pyproject/pre_compile/__init__.py:104: in load_licenses
    "validate_pyproject_license": _find_and_load_licence(_M.files(dist_name)),
src/validate_pyproject/pre_compile/__init__.py:124: in _find_and_load_licence
    return next(f for f in files if f.stem.upper() == "LICENSE").read_text("UTF-8")
/usr/lib/python3.9/importlib/metadata.py:142: in read_text
    with self.locate().open(encoding=encoding) as stream:
/usr/lib/python3.9/pathlib.py:1252: in open
    return io.open(self, mode, buffering, encoding, errors, newline,
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = PosixPath('/data/builds/validate-pyproject/src/LICENSE.txt')
name = '/data/builds/validate-pyproject/src/LICENSE.txt', flags = 8388608
mode = 438

    def _opener(self, name, flags, mode=0o666):
        # A stub for the opener argument to built-in open()
>       return self._accessor.open(self, flags, mode)
E       FileNotFoundError: [Errno 2] No such file or directory: '/data/builds/validate-pyproject/src/LICENSE.txt'

/usr/lib/python3.9/pathlib.py:1120: FileNotFoundError
------------------------------ Captured log call -------------------------------
WARNING  validate_pyproject.pre_compile:__init__.py:131 Please make sure to install `validate-pyproject` and `fastjsonschema` in a NON-EDITABLE way. This is necessary due to the issue #112 in python/importlib_metadata.
_ test_invalid_examples_api[setuptools/packages/missing-find-arguments.toml-api_pre_compile] _

tmp_path = PosixPath('/tmp/pytest-of-marcel/pytest-63/test_invalid_examples_api_setu2')
pre_compiled_validate = <function pre_compiled_validate.<locals>._validate at 0x7fffa4603940>
invalid_example = PosixPath('/data/builds/validate-pyproject/tests/invalid-examples/setuptools/packages/missing-find-arguments.toml')
pre_compiled = <function api_pre_compile at 0x7fffad59cc10>

    @pytest.mark.parametrize("pre_compiled", _PRE_COMPILED)
    def test_invalid_examples_api(
        tmp_path, pre_compiled_validate, invalid_example, pre_compiled
    ):  
        expected_error = error_file(invalid_example).read_text("utf-8")
        toml_equivalent = tomllib.loads(invalid_example.read_text())
>       pre_compiled_path = pre_compiled(Path(tmp_path), example=invalid_example)

tests/test_pre_compile.py:163:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests/test_pre_compile.py:102: in api_pre_compile
    return pre_compile(Path(tmp_path / PRE_COMPILED_NAME), extra_plugins=plugins)
src/validate_pyproject/pre_compile/__init__.py:55: in pre_compile
    write_notice(out, main_file, original_cmd, replacements)
src/validate_pyproject/pre_compile/__init__.py:95: in write_notice
    notice = notice.format(notice=opening, main_file=main_file, **load_licenses())
src/validate_pyproject/pre_compile/__init__.py:104: in load_licenses
    "validate_pyproject_license": _find_and_load_licence(_M.files(dist_name)),
src/validate_pyproject/pre_compile/__init__.py:124: in _find_and_load_licence
    return next(f for f in files if f.stem.upper() == "LICENSE").read_text("UTF-8")
/usr/lib/python3.9/importlib/metadata.py:142: in read_text
    with self.locate().open(encoding=encoding) as stream:
/usr/lib/python3.9/pathlib.py:1252: in open
    return io.open(self, mode, buffering, encoding, errors, newline,
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = PosixPath('/data/builds/validate-pyproject/src/LICENSE.txt')
name = '/data/builds/validate-pyproject/src/LICENSE.txt', flags = 8388608
mode = 438

    def _opener(self, name, flags, mode=0o666):
        # A stub for the opener argument to built-in open()
>       return self._accessor.open(self, flags, mode)
E       FileNotFoundError: [Errno 2] No such file or directory: '/data/builds/validate-pyproject/src/LICENSE.txt'

/usr/lib/python3.9/pathlib.py:1120: FileNotFoundError
------------------------------ Captured log call -------------------------------
WARNING  validate_pyproject.pre_compile:__init__.py:131 Please make sure to install `validate-pyproject` and `fastjsonschema` in a NON-EDITABLE way. This is necessary due to the issue #112 in python/importlib_metadata.

@abravalheri
Copy link
Owner

abravalheri commented Sep 9, 2024

Thank you very much @mtelka.

So this is interesting:

src/validate_pyproject/pre_compile/__init__.py:104: in load_licenses
    "validate_pyproject_license": _find_and_load_licence(_M.files(dist_name)),
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

files = []

    def _find_and_load_licence(files: Optional[Sequence[_M.PackagePath]]) -> str:
        if files is None:  # pragma: no cover
            raise ImportError("Could not find LICENSE for package")
        try:
>           return next(f for f in files if f.stem.upper() == "LICENSE").read_text("UTF-8")
E           StopIteration

_M here is importlib.metadata, and dist_name is validate-pyproject.

Why is importlib.metadata.files("validate-pyproject") not returning anything?

Are you sure that you are installing validate-pyproject (either regular or editable) before running the test suite? When the package is installed the license files should be available in the valdiate_pyproject-*.dist-info directory.

(The test suite is designed to run with the package installed, if that is not possible you might want to skip the pre_compile tests, as the implementation requires importlib.metadata to find the license files).

The second error is more weird, though:

src/validate_pyproject/pre_compile/__init__.py:104: in load_licenses
    "validate_pyproject_license": _find_and_load_licence(_M.files(dist_name)),
src/validate_pyproject/pre_compile/__init__.py:124: in _find_and_load_licence
    return next(f for f in files if f.stem.upper() == "LICENSE").read_text("UTF-8")
/usr/lib/python3.9/importlib/metadata.py:142: in read_text
    with self.locate().open(encoding=encoding) as stream:
/usr/lib/python3.9/pathlib.py:1252: in open
    return io.open(self, mode, buffering, encoding, errors, newline,
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = PosixPath('/data/builds/validate-pyproject/src/LICENSE.txt')
name = '/data/builds/validate-pyproject/src/LICENSE.txt', flags = 8388608
mode = 438

    def _opener(self, name, flags, mode=0o666):
        # A stub for the opener argument to built-in open()
>       return self._accessor.open(self, flags, mode)
E       FileNotFoundError: [Errno 2] No such file or directory: '/data/builds/validate-pyproject/src/LICENSE.txt'

Why is importlib.metadata saying that /data/builds/validate-pyproject/src/LICENSE.txt' is a file when it is not? Where did importlib.metadata got that information from?

Finally weirder part is: why this problem only triggers when src is in the test paths?

@mtelka
Copy link
Author

mtelka commented Sep 9, 2024

I test using these steps:

git clone https://github.com/abravalheri/validate-pyproject.git
cd validate-pyproject/
tox -e py39

AFAIK, tox installs the validate-pyproject so I do not need to do that manually:

$ tox -e py39
.pkg: install_requires> python -I -m pip install 'setuptools>=61.2' 'setuptools_scm[toml]>=7.1'
.pkg: _optional_hooks> python /usr/lib/python3.9/vendor-packages/pyproject_api/_backend.py True setuptools.build_meta
.pkg: get_requires_for_build_sdist> python /usr/lib/python3.9/vendor-packages/pyproject_api/_backend.py True setuptools.build_meta
.pkg: build_sdist> python /usr/lib/python3.9/vendor-packages/pyproject_api/_backend.py True setuptools.build_meta
py39: install_package_deps> python -I -m pip install 'fastjsonschema<=3,>=2.16.2' 'packaging>=20.4' pytest pytest-cov pytest-randomly pytest-xdist 'repo-review; python_version >= "3.10"' setuptools 'tomli>=1.2.1; python_version < "3.11"' 'trove-classifiers>=2021.10.20'
py39: install_package> python -I -m pip install --force-reinstall --no-deps /data/builds/validate-pyproject/.tox/.tmp/package/1/validate_pyproject-0.19.post1.dev8+g84aa9e5.tar.gz
py39: commands[0]> pytest --doctest-modules src
============================================================================================================= test session starts =============================================================================================================
platform sunos5 -- Python 3.9.20, pytest-8.3.2, pluggy-1.5.0 -- /data/builds/validate-pyproject/.tox/py39/bin/python
...
...
...

@mtelka
Copy link
Author

mtelka commented Sep 9, 2024

Why is importlib.metadata saying that /data/builds/validate-pyproject/src/LICENSE.txt' is a file when it is not? Where did importlib.metadata got that information from?

Isn't validate-pyproject expecting newer importlib.metadata than the one bundled with Python 3.9?

@abravalheri
Copy link
Owner

Isn't validate-pyproject expecting newer importlib.metadata than the one bundled with Python 3.9?

In principle no...

According to the docs in importlib_metadata, Python 3.8 stdlib bundles what would be equivalent to importlib_metadata==1.4. The files() was introduced in version 0.9. So in theory the version in stdlib should match the needs of validate-pyproject. (Unless there is a bug that never received a fix).


Thank you for all the information, I will need to have a deeper look at this problem.

@abravalheri
Copy link
Owner

OK, I think I am starting to have some level of understanding now, specially for the weird error:

E       FileNotFoundError: [Errno 2] No such file or directory: '/data/builds/validate-pyproject/src/LICENSE.txt'

Apparently importlib.metadata is finding the validate_pyproject.egg-info folder inside of src when testpaths contains src (possibly pytest adds testpaths to sys.path?) before it finds the *.dist-info folder inside of site-packages.

Once src/validate_pyproject.egg-info if found, then it is likely that we hit the problem in python/importlib_metadata#112.


Now if the hypothesis that pytest adds all testpaths to sys.path is true, then that configuration is problematic because we want to test the package as installed and all the effort to use a src-layout for isolation is wasted.

@abravalheri
Copy link
Owner

abravalheri commented Sep 10, 2024

I am not finding empirical evidence that the hypothesis about testpaths and pytest is correct.
I run a quick minimal test and things look fine:

> docker run --rm -it python:3.12-bookworm /bin/bash

mkdir -p /tmp/proj && cd /tmp/proj

cat <<EOF > pyproject.toml
[build-system]
requires = ["setuptools>=74.1.2"]
build-backed = "setuptools.build_meta"
EOF

mkdir -p src tests
touch src/proj.py

cat <<EOF > tests/test_paths.py
import sys

def test_paths():
    for path in sys.path:
        assert "src" not in path
EOF

cat <<EOF > tox.ini
[testenv]
deps = pytest>=8.3.2
commands = pytest {posargs}
EOF

pip install tox
tox   # ==> 1 passed in 0.01s

cat <<EOF > pytest.ini
[pytest]
testpaths =
    src
    tests
EOF

tox # ==> 1 passed in 0.01s

rm pytest.ini

cat <<EOF >> pyproject.toml
[tool.pytest.ini_options]
testpaths = [
    "src",
    "tests",
]
EOF

tox # ==> 1 passed in 0.01s

So now the question is: how the src directory is ending up on sys.path if it is not related to testpaths?

@mtelka
Copy link
Author

mtelka commented Sep 10, 2024

I am not finding empirical evidence that the hypothesis about testpaths and pytest is correct. I run a quick minimal test and things look fine:

> docker run --rm -it python:3.12-bookworm /bin/bash

Could you please try the same with Python 3.9 too?

@abravalheri
Copy link
Owner

abravalheri commented Sep 10, 2024

Yes, sorry, it is the same result. pytest always passes.

For the sake of completeness:

> docker run --rm -it python:3.9-bookworm /bin/bash

mkdir -p /tmp/proj && cd /tmp/proj

cat <<EOF > pyproject.toml
[build-system]
requires = ["setuptools>=74.1.2"]
build-backed = "setuptools.build_meta"
EOF

mkdir -p src tests
touch src/proj.py

cat <<EOF > tests/test_paths.py
import sys
from importlib import metadata

def test_paths():
    for path in sys.path:
        assert "src" not in path

def test_importlib():
    for dist in metadata.Distribution.discover(name="proj"):
        assert "src" not in str(dist._path)
EOF

cat <<EOF > tox.ini
[testenv]
deps = pytest>=8.3.2
commands = pytest {posargs}
EOF

pip install tox
tox   # ==> 2 passed in 0.01s

cat <<EOF > pytest.ini
[pytest]
testpaths =
    src
    tests
EOF

tox # ==> 2 passed in 0.01s 

rm pytest.ini

cat <<EOF >> pyproject.toml
[tool.pytest.ini_options]
testpaths = [
    "src",
    "tests",
]
EOF

tox # ==> 2 passed in 0.01s

@mtelka
Copy link
Author

mtelka commented Sep 10, 2024

Maybe the difference is that you do not have the src/validate_pyproject.egg-info while running your tests?

@ptrcnull
Copy link

just chiming in: we hit the same issue with the Alpine package for validate-pyproject ( which, in turn, is pulling sources from GitHub tagged releases ) - and the fix from #200 (comment) actually works here

@abravalheri
Copy link
Owner

abravalheri commented Sep 11, 2024

Sorry guys, I still need to investigate this, but I have been running short of time lately.

The behaviour is weird. I need to find out why/when/how the src directory ends up on sys.path during the tests (tox is set to perform a proper installation, not editable - and we use the src-layout to isolate the packages from CWD automatically being added to sys.path).

Maybe the difference is that you do not have the src/validate_pyproject.egg-info while running your tests?

I don't think so, that folder is automatically created when the project is build by a build-backend. So even when starting from a fresh clone, it will be there...

@abravalheri
Copy link
Owner

abravalheri commented Sep 12, 2024

So I did this trick:

cat <<EOF > conftest.py
import inspect
import sys

class _ImmuatableList(list): pass

def _prevent_modification(target: object, method: str):
    fn = getattr(target, method)

    def _replacement(self, *args, **kwargs):
        curframe = inspect.currentframe()
        calframe = inspect.getouterframes(curframe, 2)
        print('caller name:', calframe[1][3])
        raise NotImplementedError(f"""
           Trying to modify sys.path
           {method=} {args=} {kwargs=}
           {calframe[1].lineno}:{calframe[1].function}:{calframe[1].filename}
           {calframe[2].lineno}:{calframe[2].function}:{calframe[2].filename}
           {calframe[3].lineno}:{calframe[3].function}:{calframe[3].filename}
        """)

    setattr(target, method, _replacement)

for _method in (
    '__delitem__',
    '__iadd__',
    '__setitem__',
    'append',
    'clear',
    'extend',
    'insert',
    'remove',
    'reverse',
    'pop',
):
    _prevent_modification(_ImmuatableList, _method)


sys.path = _ImmuatableList(sys.path)
EOF

tox -e py39

And obtained the following error:

conftest.py:13: in _replacement
    raise NotImplementedError(f"""
E   NotImplementedError:
E              Trying to modify sys.path
E              method='insert' args=(0, '/home/abravalheri/workspace/validate-pyproject/src') kwargs={}
E              529:import_path:/home/abravalheri/workspace/validate-pyproject/.tox/py39/lib/python3.9/site-packages/_pytest/pathlib.py
E              545:collect:/home/abravalheri/workspace/validate-pyproject/.tox/py39/lib/python3.9/site-packages/_pytest/doctest.py
E              371:<lambda>:/home/abravalheri/workspace/validate-pyproject/.tox/py39/lib/python3.9/site-packages/_pytest/runner.py
------------------------------------------------------------------------------------------------ Captured stdout ------------------------------------------------------------------------------------------------
caller name: import_path

## Repeats the same error for other files

I will have to get back to this investigation later (busy days), but I just wanted to document that src seems to be added to sys.path by _pytest.doctest... I wonder if there were any changes in Pytest recently that altered that. (Intriguing that src in testpaths passed all the tests on #192).

@abravalheri
Copy link
Owner

abravalheri commented Sep 17, 2024

This is probably related to the change in behaviour?

pytest 7.1.3 (2022-08-31)
#3396 (https://github.com/pytest-dev/pytest/issues/3396): Doctests now respect the --import-mode flag.

Let me think how we can better change that. Ideally I would not like to exclude the src directory from doctests, and remove the separate pytest --doctest-modules src command.

(The recommended import mode for new projects, i.e. "importlib" also seem to have problems pytest-dev/pytest#7652, which I am hitting when working on this).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants