Skip to content

Commit

Permalink
Rely on Django to resolve string references in ForeignKey fields. Refs
Browse files Browse the repository at this point in the history
…#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.
  • Loading branch information
alejandro-angulo committed Jul 21, 2020
1 parent 6df2eeb commit ec4f005
Show file tree
Hide file tree
Showing 8 changed files with 53 additions and 5 deletions.
11 changes: 11 additions & 0 deletions 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)
3 changes: 2 additions & 1 deletion pylint_django/tests/input/models/author.py
Expand Up @@ -3,4 +3,5 @@


class Author(models.Model):
pass
class Meta:
app_label = 'test_app'
Empty file.
5 changes: 5 additions & 0 deletions pylint_django/tests/input/test_app/apps.py
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class TestAppConfig(AppConfig):
name = 'test_app'
1 change: 1 addition & 0 deletions pylint_django/tests/input/test_app/models.py
@@ -0,0 +1 @@
from models.author import Author # noqa: F401
12 changes: 12 additions & 0 deletions 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',
]
2 changes: 2 additions & 0 deletions pylint_django/tests/test_func.py
Expand Up @@ -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
Expand Down
24 changes: 20 additions & 4 deletions pylint_django/transforms/foreignkey.py
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit ec4f005

Please sign in to comment.