pyinvoke / invoke

Pythonic task management & command execution.
http://pyinvoke.org
BSD 2-Clause "Simplified" License
4.42k stars 370 forks source link

2.1.0 finds it suddenly unable to find the tasks directory #934

Closed gmmeyer closed 1 year ago

gmmeyer commented 1 year ago

I have a directory /tasks/*.py with a bunch of python files in it. With 2.1.0 I suddenly get the following error:

Traceback (most recent call last):
  File "/usr/local/bin/invoke", line 8, in <module>
    sys.exit(program.run())
  File "/usr/local/lib/python3.10/site-packages/invoke/program.py", line 387, in run
    self.parse_collection()
  File "/usr/local/lib/python3.10/site-packages/invoke/program.py", line 479, in parse_collection
    self.load_collection()
  File "/usr/local/lib/python3.10/site-packages/invoke/program.py", line 716, in load_collection
    module, parent = loader.load(coll_name)
  File "/usr/local/lib/python3.10/site-packages/invoke/loader.py", line 80, in load
    spec.loader.exec_module(module)
  File "<frozen importlib._bootstrap_external>", line 883, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/code/tasks/__init__.py", line 3, in <module>
    from . import (
ModuleNotFoundError: No module named 'tasks'

It works perfectly in 2.0.0. It seems like there was a regression of some kind

rberger commented 1 year ago

We're seeing the same problem


inv --list
Traceback (most recent call last):
  File "/Users/rberger/.pyenv/versions/3.11.2/bin/inv", line 8, in <module>
    sys.exit(program.run())
             ^^^^^^^^^^^^^
  File "/Users/rberger/.pyenv/versions/3.11.2/lib/python3.11/site-packages/invoke/program.py", line 387, in run
    self.parse_collection()
  File "/Users/rberger/.pyenv/versions/3.11.2/lib/python3.11/site-packages/invoke/program.py", line 479, in parse_collection
    self.load_collection()
  File "/Users/rberger/.pyenv/versions/3.11.2/lib/python3.11/site-packages/invoke/program.py", line 716, in load_collection
    module, parent = loader.load(coll_name)
                     ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/rberger/.pyenv/versions/3.11.2/lib/python3.11/site-packages/invoke/loader.py", line 80, in load
    spec.loader.exec_module(module)
  File "<frozen importlib._bootstrap_external>", line 940, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/code/tasks/__init__.py", line 2, in <module>
    from . import install, utils
ModuleNotFoundError: No module named 'tasks'
bitprophet commented 1 year ago

Thanks for the reports! Almost certainly something to do with #919, except all our tests and CI pass (eg: https://app.circleci.com/pipelines/github/pyinvoke/invoke/288/workflows/9aa4cffc-8199-4158-ae94-cf29e1dd761d). I think the crux is:

I have a directory /tasks/*.py

I can confirm that this breaks in my one (personal) project using a tasks/ directory, but works everywhere else, so it's clearly specific to the package-not-module variant. We do have tests for that though, see eg this test and the package it should be loading.

@kuwv you got time to hunt this down? If not I will try to take a look early next week.

Anyone afflicted should have no problem temporarily pinning to invoke<2.1, 2.1 itself doesn't have anything super juicy in it.

kuwv commented 1 year ago

Unfortunately, I'm currently traveling at the moment. Soonest I'll be able get to it might be Wednesday.

bitprophet commented 1 year ago

Dug some (but also got sniped by adjacent issue that throws irritating test errors, now fixed). Already kind of cross as I blew half my freakin Saturday getting turned in circles with all this, but the upshot is:

My offhand guess is either:

I will try taking a closer look early next week.

bitprophet commented 1 year ago

Moar dig:

So this is pretty weird. Will continue reading importlib's docs and/or seeing if this is somehow a stdlib bug (would be pretty damn weird if so, to exist for so long & in such a critical spot, people do from . imports all the time these days).

bitprophet commented 1 year ago
bitprophet commented 1 year ago

https://docs.python.org/3/library/importlib.html#approximating-importlib-import-module implies we might be missing a step (or two) here, such as manually manipulating sys.modules. This does seem to work!

(As long as I don't fuck up and do that manipulation after the exec_module call instead of before...wasn't reading the example closely enough, but in hindsight it makes perfect sense, execution of the module-level code in our tasks file is what needs to 'see' the module?)

I'm a bit salty that this "new improved" importlib API still requires the user to do this, but I guess I also shouldn't fault other folks for "here's our new much more flexible API and also oops you still have to do some manual labor in corner cases that wasn't required before, sorry"...


So it looks like @kuwv missed this one spot when adapting that importlib example snippet to Invoke's loader classes. I'm going to put out a bugfix on this basis, assuming all the other tests pass for me afterwards...

bitprophet commented 1 year ago

In trying to get the unit test for this breaking properly, I found:

rberger commented 1 year ago

Thank you!