Opened 8 years ago
Closed 8 years ago
#28241 closed Bug (fixed)
module_has_submodule() doesn't work correctly if the module_name argument is a dotted path
| Reported by: | tkhyn | Owned by: | tkhyn |
|---|---|---|---|
| Component: | Utilities | Version: | 1.11 |
| Severity: | Normal | Keywords: | module_loading python3 |
| Cc: | Triage Stage: | Accepted | |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description (last modified by )
Hello,
Porting a django project from python 2.7 to 3.6, I noticed an issue with utils.module_loading.module_has_submodule. At some stage my project makes use of autodiscover_modules('module.submodule') to try and discover modules that are nested in my app.
I'm using django 1.11.1, but this bug probably also affects 1.8 as the code of module_has_submodule is the same.
So here we go, with python 2.7 we have the expected behavior (taking contenttypes as an example app):
python2.7 >>> from django.utils.module_loading import module_has_submodule >>> from django.contrib import contenttypes >>> module_has_submodule(contenttypes, 'invalid_module.submodule') False >>> module_has_submodule(contenttypes, 'checks') True >>> module_has_submodule(contenttypes, 'checks.submodule') False
But, with python 3.6
python3.6
>>> from django.utils.module_loading import module_has_submodule
>>> from django.contrib import contenttypes
>>> module_has_submodule(contenttypes, 'invalid_module.submodule')
File "<console>", line 1, in <module>
File "d:\dev\.env\buildout\eggs\django-1.11.1-py3.6.egg\django\utils\module_loading.py", line 79, in module_has_submodule
return importlib_find(full_module_name, package_path) is not None
File "D:\dev\.env\venv\buildout\lib\importlib\util.py", line 88, in find_spec
parent = __import__(parent_name, fromlist=['__path__'])
ModuleNotFoundError: No module named 'django.contrib.contenttypes.invalid_module'
>>> module_has_submodule(contenttypes, 'checks')
True
>>> module_has_submodule(contenttypes, 'checks.submodule')
File "<console>", line 1, in <module>
File "d:\dev\.env\buildout\eggs\django-1.11.1-py3.6.egg\django\utils\module_loading.py", line 79, in module_has_submodule
return importlib_find(full_module_name, package_path) is not None
File "D:\dev\.env\venv\buildout\lib\importlib\util.py", line 89, in find_spec
return _find_spec(fullname, parent.__path__)
AttributeError: module 'django.contrib.contenttypes.checks' has no attribute '__path__'
From the replies I got on the python bug tracker (http://bugs.python.org/issue30436) the AttributeError will be converted to ModuleNotFoundError only in Python 3.7, but that behavior is apparently not expected to be fixed in previous versions.
We'll definitely need to catch ModuleNotFoundError, and to have a proper fix for python < 3.7 we'll need to:
- either catch
AttributeErroras well. The problem I see isAttributeErroris too broad and may result in 'false catches' - or convert a dotted path to [package, name] (basically do what
find_specactually does) but detect if the module has a__path__attribute before callingfind_specso that it can not raise exceptions
What are your thoughts ?
Change History (9)
comment:1 by , 8 years ago
| Summary: | module_has_submodule behavior in Python 3 → module_has_submodule raises exceptions in Python 3 |
|---|
comment:2 by , 8 years ago
| Description: | modified (diff) |
|---|
comment:3 by , 8 years ago
| Description: | modified (diff) |
|---|---|
| Easy pickings: | unset |
| Summary: | module_has_submodule raises exceptions in Python 3 → module_has_submodule() doesn't work correctly if the module_name argument is a dotted path |
comment:4 by , 8 years ago
That's what I said, it works on Python 2, not on Python 3. (sorry, I misread, replied too quickly)
So you're saying that module_has_submodule should not accept dotted paths anyway. As a consequence, autodiscover_modules should not either ... However the dotted path feature I used and that actually worked ok for me until then with python 2.7 would not be hard to support.
The real-life problem I have with that is I need to use autodiscover_modules('path_to.my_module'), which calls module_has_submodule in every app. In Python 2 it attempts to discover this module in all apps (and consistently returns True or False). In Python 3 it raises an exception the first time find_spec fails, preventing loading any module in subsequent apps.
I know one solution is to re-organize my apps structure to put my_module at the apps' root level, but:
- that does mess up with the organization of my code
- this used to work in Python 2 and could easily be made to work in Python 3, and I would like to avoid having to monkey-patch django just to add a try / catch block ...
comment:5 by , 8 years ago
In addition, trying to reproduce your example with python 2.7.13 I get:
>>> from importlib import import_module
>>> from django.utils.module_loading import module_has_submodule
>>> test_module = import_module('utils_tests.test_module')
>>> module_has_submodule(test_module, 'invalid.good_module')
False
comment:6 by , 8 years ago
| Triage Stage: | Unreviewed → Accepted |
|---|
I don't know if module_has_submodule() should not accept dotted paths, however, as far as I can tell, the behavior is undocumented and untested. We could either fix it or raise a more helpful error message.
comment:7 by , 8 years ago
| Owner: | changed from to |
|---|---|
| Status: | new → assigned |
Great, I'll submit a PR with testcases + solution for all currently supported versions.
comment:8 by , 8 years ago
| Has patch: | set |
|---|
I don't think the
module_nameargument ofmodule_has_submodule()is expected to be a dotted path. For example, this test (incorrectly, as far as I see) passes on Python 2:tests/utils_tests/test_module_loading.py
Is Django making any calls like that or is this function used like this outside of Django?