Skip to content

Commit 36fc49f

Browse files
committed
Moved the Django foreignkey field into a separate checker to enable the open() pre-configuration. This allows the django settings module to be set from a command-line option or .pylintrc option as well as an environment variable (refs #286). This commit also uses default Django settings if none are specified, raising an error to the user to inform them that it is better to set it explicitly, but avoiding the need to raise a Fatal or RuntimeError if Django is not configured (refs #277 and #243)
1 parent 369a473 commit 36fc49f

File tree

6 files changed

+138
-8
lines changed

6 files changed

+138
-8
lines changed

pylint_django/checkers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pylint_django.checkers.json_response import JsonResponseChecker
55
from pylint_django.checkers.forms import FormChecker
66
from pylint_django.checkers.auth_user import AuthUserChecker
7+
from pylint_django.checkers.foreign_key_strings import ForeignKeyStringsChecker
78

89

910
def register_checkers(linter):
@@ -13,3 +14,4 @@ def register_checkers(linter):
1314
linter.register_checker(JsonResponseChecker(linter))
1415
linter.register_checker(FormChecker(linter))
1516
linter.register_checker(AuthUserChecker(linter))
17+
linter.register_checker(ForeignKeyStringsChecker(linter))

pylint_django/checkers/django_installed.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ class DjangoInstalledChecker(BaseChecker):
2020
}
2121

2222
@check_messages('django-not-available')
23-
def close(self):
23+
def open(self):
2424
try:
2525
__import__('django')
2626
except ImportError:
27-
self.add_message('F%s01' % BASE_ID)
27+
# mild hack: this error is added before any modules have been inspected
28+
# so we need to set some kind of value to prevent the linter failing to it
29+
self.linter.set_current_module('project global')
30+
self.add_message('django-not-available')
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
from __future__ import absolute_import
2+
from pylint.checkers import BaseChecker
3+
from pylint.interfaces import IAstroidChecker
4+
from pylint.checkers.utils import check_messages
5+
from pylint_django.__pkginfo__ import BASE_ID
6+
from pylint_django.transforms import foreignkey
7+
import astroid
8+
9+
10+
class ForeignKeyStringsChecker(BaseChecker):
11+
"""
12+
Adds transforms to be able to do type inference for model ForeignKeyField
13+
properties which use a string to name the foreign relationship. This uses
14+
Django's model name resolution and this checker wraps the setup to ensure
15+
Django is able to configure itself before attempting to use the lookups.
16+
"""
17+
18+
_LONG_MESSAGE = """Finding foreign-key relationships from strings in pylint-django requires configuring Django.
19+
This can be done via the DJANGO_SETTINGS_MODULE environment variable or the pylint option django-settings-module, eg:
20+
21+
`pylint --load-plugins=pylint_django --django-settings-module=myproject.settings`
22+
23+
. This can also be set as an option in a .pylintrc configuration file.
24+
25+
Some basic default settings were used, however this will lead to less accurate linting.
26+
Consider passing in an explicit Django configuration file to match your project to improve accuracy."""
27+
28+
__implements__ = (IAstroidChecker,)
29+
30+
name = "Django foreign keys referenced by strings"
31+
32+
options = (
33+
(
34+
"django-settings-module",
35+
{
36+
"default": None,
37+
"type": "string",
38+
"metavar": "<django settings module>",
39+
"help": "A module containing Django settings to be used while linting.",
40+
},
41+
),
42+
)
43+
44+
msgs = {"E%s10" % BASE_ID: ("Django was not configured. For more information run pylint --load-plugins=pylint_django --help-msg=django-not-configured", "django-not-configured", _LONG_MESSAGE),
45+
"F%s10" % BASE_ID: (
46+
'Provided Django settings %s could not be loaded',
47+
'django-settings-module-not-found',
48+
'The provided Django settings module %s was not found on the path'
49+
)}
50+
51+
def open(self):
52+
self._raise_warning = False
53+
# This is a bit of a hacky workaround. pylint-django does not *require* that
54+
# Django is configured explicitly, and will use some basic defaults in that
55+
# case. However, as this is a WARNING not a FATAL, the error must be raised
56+
# with an AST node - only F and R messages are scope exempt (see
57+
# https://github.com/PyCQA/pylint/blob/master/pylint/constants.py#L24)
58+
59+
# However, testing to see if Django is configured happens in `open()`
60+
# before any modules are inspected, as Django needs to be configured with
61+
# defaults before the foreignkey checker can work properly. At this point,
62+
# there are no nodes.
63+
64+
# Therefore, during the initialisation in `open()`, if django was configured
65+
# using defaults by pylint-django, it cannot raise the warning yet and
66+
# must wait until some module is inspected to be able to raise... so that
67+
# state is stashed in this property.
68+
69+
from django.core.exceptions import (
70+
ImproperlyConfigured,
71+
) # pylint: disable=import-outside-toplevel
72+
73+
try:
74+
import django # pylint: disable=import-outside-toplevel
75+
76+
django.setup()
77+
from django.apps import apps # pylint: disable=import-outside-toplevel
78+
except ImproperlyConfigured:
79+
# this means that Django wasn't able to configure itself using some defaults
80+
# provided (likely in a DJANGO_SETTINGS_MODULE environment variable)
81+
# so see if the user has specified a pylint option
82+
if self.config.django_settings_module is None:
83+
# we will warn the user that they haven't actually configured Django themselves
84+
self._raise_warning = True
85+
# but use django defaults then...
86+
from django.conf import settings # pylint: disable=import-outside-toplevel
87+
settings.configure()
88+
django.setup()
89+
else:
90+
# see if we can load the provided settings module
91+
try:
92+
from django.conf import settings, Settings # pylint: disable=import-outside-toplevel
93+
settings.configure(Settings(self.config.django_settings_module))
94+
django.setup()
95+
except ImportError:
96+
# we could not find the provided settings module...
97+
# at least here it is a fatal error so we can just raise this immediately
98+
self.add_message('django-settings-module-not-found', args=self.config.django_settings_module)
99+
# however we'll trundle on with basic settings
100+
from django.conf import settings # pylint: disable=import-outside-toplevel
101+
settings.configure()
102+
django.setup()
103+
104+
# now we can add the trasforms speciifc to this checker
105+
foreignkey.add_transform(astroid.MANAGER)
106+
107+
# TODO: this is a bit messy having so many inline imports but in order to avoid
108+
# duplicating the django_installed checker, it'll do for now. In the future, merging
109+
# those two checkers together might make sense.
110+
111+
112+
@check_messages("django-not-configured")
113+
def visit_module(self, node):
114+
if self._raise_warning:
115+
# just add it to the first node we see... which isn't nice but not sure what else to do
116+
self.add_message("django-not-configured", node=node)
117+
self._raise_warning = False # only raise it once...

pylint_django/checkers/migrations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def func(apps, schema_editor):
156156
return is_migrations_module(node.parent)
157157

158158

159-
def load_configuration(linter):
159+
def load_configuration(linter): # TODO this is redundant and can be removed
160160
# don't blacklist migrations for this checker
161161
new_black_list = list(linter.config.black_list)
162162
if 'migrations' in new_black_list:

pylint_django/transforms/__init__.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
1-
"""Transforms."""
1+
"""
2+
These transforms replace the Django types with adapted versions to provide
3+
additional typing and method inference to pylint. All of these transforms
4+
are considered "global" to pylint-django, in that all checks and improvements
5+
requre them to be loaded. Additional transforms specific to checkers are loaded
6+
by the checker rather than here.
7+
8+
For example, the ForeignKeyStringsChecker loads the foreignkey.py transforms
9+
itself as it may be disabled independently of the rest of pylint-django
10+
"""
211
import os
312
import re
413

514
import astroid
615

7-
from pylint_django.transforms import foreignkey, fields
16+
from pylint_django.transforms import fields
817

9-
10-
foreignkey.add_transform(astroid.MANAGER)
1118
fields.add_transforms(astroid.MANAGER)
1219

1320

21+
1422
def _add_transform(package_name):
1523
def fake_module_builder():
1624
"""

pylint_django/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from pylint_django.compat import Uninferable
1010

11-
PY3 = sys.version_info >= (3, 0)
11+
PY3 = sys.version_info >= (3, 0) # TODO: pylint_django doesn't support Py2 any more
1212

1313

1414
def node_is_subclass(cls, *subclass_names):

0 commit comments

Comments
 (0)