From ec4f00565218a427ad9fac0af1adfca855a86711 Mon Sep 17 00:00:00 2001 From: Alejandro Angulo Date: Tue, 21 Jul 2020 02:09:29 -0700 Subject: [PATCH] Rely on Django to resolve string references in ForeignKey fields. Refs #243 this commit tries to load & configure Django and use its internal machinery to resolve apps and models in case they are referenced as a string. If this fails it falls back to appending ".models" to the first part of the name and looking for a module with that name to augment. --- .../input/func_noerror_string_foreignkey.py | 11 +++++++++ pylint_django/tests/input/models/author.py | 3 ++- .../tests/input/test_app/__init__.py | 0 pylint_django/tests/input/test_app/apps.py | 5 ++++ pylint_django/tests/input/test_app/models.py | 1 + pylint_django/tests/settings.py | 12 ++++++++++ pylint_django/tests/test_func.py | 2 ++ pylint_django/transforms/foreignkey.py | 24 +++++++++++++++---- 8 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 pylint_django/tests/input/func_noerror_string_foreignkey.py create mode 100644 pylint_django/tests/input/test_app/__init__.py create mode 100644 pylint_django/tests/input/test_app/apps.py create mode 100644 pylint_django/tests/input/test_app/models.py create mode 100644 pylint_django/tests/settings.py diff --git a/pylint_django/tests/input/func_noerror_string_foreignkey.py b/pylint_django/tests/input/func_noerror_string_foreignkey.py new file mode 100644 index 00000000..22985452 --- /dev/null +++ b/pylint_django/tests/input/func_noerror_string_foreignkey.py @@ -0,0 +1,11 @@ +""" +Checks that PyLint correctly handles string foreign keys +https://github.com/PyCQA/pylint-django/issues/243 +""" +# pylint: disable=missing-docstring +from django.db import models + + +class Book(models.Model): + author = models.ForeignKey("test_app.Author", models.CASCADE) + user = models.ForeignKey("auth.User", models.PROTECT) diff --git a/pylint_django/tests/input/models/author.py b/pylint_django/tests/input/models/author.py index 93b9fb90..e09af25b 100644 --- a/pylint_django/tests/input/models/author.py +++ b/pylint_django/tests/input/models/author.py @@ -3,4 +3,5 @@ class Author(models.Model): - pass + class Meta: + app_label = 'test_app' diff --git a/pylint_django/tests/input/test_app/__init__.py b/pylint_django/tests/input/test_app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pylint_django/tests/input/test_app/apps.py b/pylint_django/tests/input/test_app/apps.py new file mode 100644 index 00000000..fc04070e --- /dev/null +++ b/pylint_django/tests/input/test_app/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TestAppConfig(AppConfig): + name = 'test_app' diff --git a/pylint_django/tests/input/test_app/models.py b/pylint_django/tests/input/test_app/models.py new file mode 100644 index 00000000..4062f6e0 --- /dev/null +++ b/pylint_django/tests/input/test_app/models.py @@ -0,0 +1 @@ +from models.author import Author # noqa: F401 diff --git a/pylint_django/tests/settings.py b/pylint_django/tests/settings.py new file mode 100644 index 00000000..eb12bc79 --- /dev/null +++ b/pylint_django/tests/settings.py @@ -0,0 +1,12 @@ +SECRET_KEY = 'fake-key' + +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'test_app', +] + +MIDDLEWARE = [ + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', +] diff --git a/pylint_django/tests/test_func.py b/pylint_django/tests/test_func.py index b0e8fc84..ef2a03a0 100644 --- a/pylint_django/tests/test_func.py +++ b/pylint_django/tests/test_func.py @@ -6,6 +6,8 @@ import pylint +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pylint_django.tests.settings') + try: # pylint 2.5: test_functional has been moved to pylint.testutils from pylint.testutils import FunctionalTestFile, LintModuleTest diff --git a/pylint_django/transforms/foreignkey.py b/pylint_django/transforms/foreignkey.py index 849a2938..d8375713 100644 --- a/pylint_django/transforms/foreignkey.py +++ b/pylint_django/transforms/foreignkey.py @@ -41,6 +41,17 @@ def _get_model_class_defs_from_module(module, model_name, module_name): return class_defs +def _module_name_from_django_model_resolution(model_name, module_name): + import django # pylint: disable=import-outside-toplevel + django.setup() + from django.apps import apps # pylint: disable=import-outside-toplevel + + app = apps.get_app_config(module_name) + model = app.get_model(model_name) + + return model.__module__ + + def infer_key_classes(node, context=None): keyword_args = [] if node.keywords: @@ -87,10 +98,15 @@ def infer_key_classes(node, context=None): module_name = current_module.name elif not module_name.endswith('models'): # otherwise Django allows specifying an app name first, e.g. - # ForeignKey('auth.User') so we try to convert that to - # 'auth.models', 'User' which works nicely with the `endswith()` - # comparison below - module_name += '.models' + # ForeignKey('auth.User') + try: + module_name = _module_name_from_django_model_resolution(model_name, module_name) + except LookupError: + # If Django's model resolution fails we try to convert that to + # 'auth.models', 'User' which works nicely with the `endswith()` + # comparison below + module_name += '.models' + # ensure that module is loaded in astroid_cache, for cases when models is a package if module_name not in MANAGER.astroid_cache: MANAGER.ast_from_module_name(module_name)