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

Improve handling of pyargs #3010

Merged
merged 4 commits into from
Dec 13, 2017
Merged

Conversation

cryvate
Copy link
Contributor

@cryvate cryvate commented Dec 7, 2017

The conversion of pyargs to paths is improved here:

Note that this might not fix the issue in python 3.0-3, as pkgutil.find_loader still worked like in python2.7 but as it is not supported I did not look into it.

I couldn't just use the backport of importlib, as it does not have importlib.find_loader or importlib.util.find_spec which is what is needed. The current (3.4+) pkgutil.find_loader uses importlib.util.find_spec under the hood, instead of parts of pkgutil.

@cryvate cryvate force-pushed the fix-issue-2985 branch 2 times, most recently from 7ccc72d to 4e5344d Compare December 7, 2017 12:56

# The structure of the test directory is now:
# .
# ├── local
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to keep close to the actual issue, and kept the virtualenv structure.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.3%) to 92.416% when pulling 4e5344d on cryvate:fix-issue-2985 into 88f2cc9 on pytest-dev:master.

@@ -645,6 +647,69 @@ def join_pythonpath(*dirs):
"*1 passed*"
])

@pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires symlinks")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes if the python/os supports symlinks, so does the filesystem the temporary directory is created. This is not a given, but as this is in testing only, and generally this is the case (both in CI and locally) in most workflows, I think this is good enough.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree 😁

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.1%) to 92.57% when pulling 4e5344d on cryvate:fix-issue-2985 into 88f2cc9 on pytest-dev:master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.1%) to 92.57% when pulling df6885d on cryvate:fix-issue-2985 into 88f2cc9 on pytest-dev:master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.1%) to 92.57% when pulling b62fd79 on cryvate:fix-issue-2985 into 88f2cc9 on pytest-dev:master.

Copy link
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! Thanks for debugging and studying the underlying issue, it is quite a ride.

I made a few comments, please take a look at them when you have the chance.

_pytest/main.py Outdated
return None
return pkgutil.ImpLoader(fullname, file, filename, etc)

pkgutil.ImpImporter.find_module = find_module_patched
Copy link
Member

@nicoddemus nicoddemus Dec 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little concerned about patching this globally like it is done here... who knows what we might break by patching this.

I would rather be conservative and patch this only while we are calling find_loader in the lines below this one. I think a nice way to implement this would be by using a contextmanager:

try:
    with _patched_find_module():
        loader = pkgutil.find_loader(x)
except ImportError:

And _patched_find_module can be implemented like so:

@contextlib.contextmanager
def _patched_find_module():
    """ ... """ 
    if six.PY2:  # python 3.4+ uses importlib instead
        def find_module_patched(self, fullname, path=None):
            subname = fullname.split(".")[-1]
            if subname != fullname and self.path is None:
                return None
            if self.path is None:
                path = None
            else:
                path = [self.path]
            try:
                file, filename, etc = pkgutil.imp.find_module(subname,
                                                              path)
            except ImportError:
                return None
            return pkgutil.ImpLoader(fullname, file, filename, etc)

        old_find_module = pkgutil.ImpImporter.find_module
        pkgutil.ImpImporter.find_module = find_module_patched
        try:            
            yield
        finally:
            pkgutil.ImpImporter.find_module = old_find_module
    else:
        yield

We can then even use _patched_find_module()'s docstring to better explain what's going on.

@@ -645,6 +647,69 @@ def join_pythonpath(*dirs):
"*1 passed*"
])

@pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires symlinks")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree 😁

# The structure of the test directory is now:
# .
# ├── local
# │ └── lib -> ../world
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment is incorrect, there's no world directory being created

# └── bar
# ├── __init__.py
# ├── conftest.py
# └── test_world.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here, the code above is creating test_bar.py

# └── test_world.py

def join_pythonpath(*dirs):
cur = py.std.os.environ.get('PYTHONPATH')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use os.environ directly, we are planning on dropping py.std usage eventually

except ImportError:
return x
if loader is None:
return x
# This method is sometimes invoked when AssertionRewritingHook, which
# does not define a get_filename method, is already in place:
try:
path = loader.get_filename(x)
with _patched_find_module():
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure why it is needed here, but I do know the tests fail if you don't have patching in both places.

@cryvate cryvate force-pushed the fix-issue-2985 branch 3 times, most recently from b462110 to 9703053 Compare December 12, 2017 08:35
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.2%) to 92.582% when pulling b462110 on cryvate:fix-issue-2985 into 5c6d773 on pytest-dev:master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.2%) to 92.582% when pulling 3ca1e4b on cryvate:fix-issue-2985 into 5c6d773 on pytest-dev:master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.2%) to 92.582% when pulling 3ca1e4b on cryvate:fix-issue-2985 into 5c6d773 on pytest-dev:master.

_pytest/main.py Outdated
@@ -728,17 +730,57 @@ def _tryconvertpyarg(self, x):
"""Convert a dotted module name to path.

"""
import pkgutil
@contextlib.contextmanager
def _patched_find_module():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably better in the global namespace than a local function because it clutters the code quite a bit. Sorry I wasn't clear about that before.

@nicoddemus
Copy link
Member

@RonnyPfannschmidt I would love a second set of eyes on this.

@RonnyPfannschmidt
Copy link
Member

i did a quick skim and it looks ok, im not fluent with the details either, i wonder if we should look into using importlib

@cryvate
Copy link
Contributor Author

cryvate commented Dec 12, 2017

On python3.4+ we are basically using importlib: pkgutil.find_loader is a thin wrapper around importlib.util.find_spec there. We could use importlib.util.find_spec directly in python3 as I did in this commit, however this is largely equivalent to using pkgutil.find_loader when you check the source so I think at least until python2 support is dropped, it is better to stick with using pkgutil.find_loader.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.2%) to 92.582% when pulling 1e29553 on cryvate:fix-issue-2985 into 5c6d773 on pytest-dev:master.

@RonnyPfannschmidt
Copy link
Member

@cryvate thanks for elaborating 👍

Copy link
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @cryvate!

@RonnyPfannschmidt we can merge IMO unless you want more time to study the PR.

@RonnyPfannschmidt RonnyPfannschmidt merged commit 476d4df into pytest-dev:master Dec 13, 2017
@RonnyPfannschmidt
Copy link
Member

i really dont have the time ^^


monkeypatch.setenv('PYTHONPATH', join_pythonpath(*search_path))
for p in search_path:
monkeypatch.syspath_prepend(p)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cryvate
Is the different order between PYTHONPATH and sys.path intentional here?
Fixed it in 2f058ad, which is required to fix the test failure when using realpath() on the args (18e7358), but have not really investigated why.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blueyed
Though git blame will assign the 'blame' to me, actually this part of the code was a copy-paste from the test_cmdline_python_namespace_package test so I'd pass on the 'blame' to this PR. Regardless, I'd think that the different order (the opposite way around if I'm reading it correctly) is accidental here as well as in the other test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cryvate
Thanks and sorry for not looking good enough.

as well as in the other test

Which one do you mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blueyed
Not a problem at all! I meant test_cmdline_python_namespace_package where I copy-pasted from.

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

Successfully merging this pull request may close these issues.

5 participants