EmilStenstrom / django-components

Create simple reusable template components in Django.
MIT License
1.16k stars 76 forks source link

0.100: TypeError: the 'package' argument is required to perform a relative import #669

Closed danjac closed 1 month ago

danjac commented 1 month ago

This error happens at startup:

  File "/PATH-TO-PROJECT/.venv/lib/python3.12/site-packages/django_components/autodiscover.py", line 36, in autodiscover
    return _import_modules(modules, map_module)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/PATH-TO-PROJECT/.venv/lib/python3.12/site-packages/django_components/autodiscover.py", line 65, in _import_modules
    importlib.import_module(module_name)
  File "/usr/lib64/python3.12/importlib/__init__.py", line 84, in import_module
    raise TypeError("the 'package' argument is required to perform a "
TypeError: the 'package' argument is required to perform a relative import for '.venv.lib.python3.12.site-packages.django_components.components.__init__'

Settings:

COMPONENTS = {
    "app_dirs": ["components"],
    "dirs": [BASE_DIR / "django_project" / "components"],
}

Previous (working) 0.97 settings:

COMPONENTS = {
    "libraries": [
        "django_project.components.buttons",
        "django_project.components.cards",
        "django_project.components.page_header",
        "django_project.components.pagination",
        "django_project.components.search",
        "django_project.my_app.components.cards",
    ]
}

i.e. I have a number of components defined in modules under django_project/components e.g. django_project/components/buttons.py and app-specific components under django_project/my_app/components e.g. django_project/my_app/components/cards.py.

Using Python 3.12.

EmilStenstrom commented 1 month ago

@danjac What happens if you just remove all settings? The way you've set things up is the default, so you shouldn't need any settings, either app_dirs, dirs, or libraries.

We shouldn't break like that, so just trying to find a workaround.

danjac commented 1 month ago

I get the same error with the COMPONENTS setting commented out.

JuroOravec commented 1 month ago

@danjac Could you create a demo project? I wasn't able to reproduce this.

JuroOravec commented 1 month ago

My guess is that it has to do with this import path:

.venv.lib.python3.12.site-packages.django_components.components.__init__

It's the path to django_components.components, and it's there because we also look for 3rd party installed Django apps that are installed (django_components), and search for app_dirs in them (so django_components.components).

Then there's one safeguard to ignore those paths that are outside of the project root.

Could be that you have BASE_DIR set one level higher. When I implemented this I assumed that BASE_DIR would be the root, so any path below it would be one of Django apps. Your Django apps are one level deeper, in [BASE_DIR / "django_project". So you would have to set the root to [BASE_DIR / "django_project". Then the path to .venv is not inside of the project root anymore, and so we would NOT try to load it.

But the whole problem with this import is that we try to import from .venv. The way we obtain the path is that we use Django's AppConfig.path which is a filesystem path (with forward slashes), e.g.

./.venv/lib/python3.12/site-packages/django_components/components

And we convert it to a module path (with dots), e.g.

.venv.lib.python3.12.site-packages.django_components.components

And so then we get a module path that starts with a dot, which causes the error.


So I see now that the strategy to ignore paths outside of the root project is not sufficient. And we need a way to be able to handle third party packages. Don't know if we can get that info from the AppConfig instance. What we can do alternatively is that if the app path includes site-packages, then we drop the path leading up to it (inclusive).

So

.venv.lib.python3.12.site-packages.django_components.components

Would become

django_components.components

danjac commented 1 month ago

Right - for the record, I'm using pdm, which has the default behavior of creating a .venv virtualenv directory in the project root - other packaging systems e.g. Poetry or uv do something similar, so this is a very common scenario.

Second, you're correct that my BASE_DIR is the top level, under which I have my django_project and under django_project I have individual apps. Again, this is a common pattern and normally works fine.

I'm not aware of how your autodiscover is implemented, but looking at prior art we have template tags: I can add templatetags packages under each app, and I can specify specific modules under OPTIONS.builtins in the TEMPLATES directory, and Django is able to find all of these provided they are available to the PYTHONPATH, so I was assuming django-components does the same?

For reference: how template tag lookups are done in Django: https://github.com/django/django/blob/main/django/template/backends/django.py

JuroOravec commented 1 month ago

@danjac Thanks for linking Django's code. So they use AppConfig.name as the import path.

The relevant part is line 140:

f"{app_config.name}.templatetags" for app_config in apps.get_app_configs()

I had the same idea this morning. Though ChatGPT suggested that technically the AppConfig.name doesn't necessarily have to match the import path for given app.

But seeing that Django themselves use AppConfig.name like this, then IMO we can do the same :)


Btw, @danjac, does it then mean that your my_app app from your example earlier has apps.py like this?

class MyAppConfig(AppConfig):
    name = "django_project.my_app"
    ...

That is, that the name is django_project.my_app?

danjac commented 1 month ago

Yes that's correct, something like

class MyAppConfig(AppConfig):
    name = "django_project.my_app"
JuroOravec commented 1 month ago

This has now been fixed in https://github.com/EmilStenstrom/django-components/releases/tag/0.101 🎉

danjac commented 1 month ago

Works great, thank you!