-
Notifications
You must be signed in to change notification settings - Fork 117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improved Django settings handling #287
Changes from all commits
36fc49f
26f3af3
21487b4
c7730fa
08ad9de
f7b556b
aa7f80b
7721691
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
from __future__ import absolute_import | ||
|
||
import astroid | ||
from pylint.checkers import BaseChecker | ||
from pylint.checkers.utils import check_messages | ||
from pylint.interfaces import IAstroidChecker | ||
|
||
from pylint_django.__pkginfo__ import BASE_ID | ||
from pylint_django.transforms import foreignkey | ||
|
||
|
||
class ForeignKeyStringsChecker(BaseChecker): | ||
""" | ||
Adds transforms to be able to do type inference for model ForeignKeyField | ||
properties which use a string to name the foreign relationship. This uses | ||
Django's model name resolution and this checker wraps the setup to ensure | ||
Django is able to configure itself before attempting to use the lookups. | ||
""" | ||
|
||
_LONG_MESSAGE = """Finding foreign-key relationships from strings in pylint-django requires configuring Django. | ||
This can be done via the DJANGO_SETTINGS_MODULE environment variable or the pylint option django-settings-module, eg: | ||
|
||
`pylint --load-plugins=pylint_django --django-settings-module=myproject.settings` | ||
|
||
. This can also be set as an option in a .pylintrc configuration file. | ||
|
||
Some basic default settings were used, however this will lead to less accurate linting. | ||
Consider passing in an explicit Django configuration file to match your project to improve accuracy.""" | ||
|
||
__implements__ = (IAstroidChecker,) | ||
|
||
name = "Django foreign keys referenced by strings" | ||
|
||
options = ( | ||
( | ||
"django-settings-module", | ||
{ | ||
"default": None, | ||
"type": "string", | ||
"metavar": "<django settings module>", | ||
"help": "A module containing Django settings to be used while linting.", | ||
}, | ||
), | ||
) | ||
|
||
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, | ||
), | ||
"F%s10" | ||
% BASE_ID: ( | ||
"Provided Django settings %s could not be loaded", | ||
"django-settings-module-not-found", | ||
"The provided Django settings module %s was not found on the path", | ||
), | ||
} | ||
|
||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
self._raise_warning = False | ||
|
||
def open(self): | ||
# This is a bit of a hacky workaround. pylint-django does not *require* that | ||
# Django is configured explicitly, and will use some basic defaults in that | ||
# case. However, as this is a WARNING not a FATAL, the error must be raised | ||
# with an AST node - only F and R messages are scope exempt (see | ||
# https://github.com/PyCQA/pylint/blob/master/pylint/constants.py#L24) | ||
|
||
# However, testing to see if Django is configured happens in `open()` | ||
# before any modules are inspected, as Django needs to be configured with | ||
# defaults before the foreignkey checker can work properly. At this point, | ||
# there are no nodes. | ||
|
||
# Therefore, during the initialisation in `open()`, if django was configured | ||
# using defaults by pylint-django, it cannot raise the warning yet and | ||
# must wait until some module is inspected to be able to raise... so that | ||
# state is stashed in this property. | ||
|
||
from django.core.exceptions import ( # pylint: disable=import-outside-toplevel | ||
ImproperlyConfigured, | ||
) | ||
|
||
try: | ||
import django # pylint: disable=import-outside-toplevel | ||
|
||
django.setup() | ||
from django.apps import apps # noqa pylint: disable=import-outside-toplevel,unused-import | ||
except ImproperlyConfigured: | ||
# this means that Django wasn't able to configure itself using some defaults | ||
# provided (likely in a DJANGO_SETTINGS_MODULE environment variable) | ||
# so see if the user has specified a pylint option | ||
if self.config.django_settings_module is None: | ||
# we will warn the user that they haven't actually configured Django themselves | ||
self._raise_warning = True | ||
# but use django defaults then... | ||
from django.conf import ( # pylint: disable=import-outside-toplevel | ||
settings, | ||
) | ||
settings.configure() | ||
django.setup() | ||
else: | ||
# see if we can load the provided settings module | ||
try: | ||
from django.conf import ( # pylint: disable=import-outside-toplevel | ||
settings, | ||
Settings, | ||
) | ||
|
||
settings.configure(Settings(self.config.django_settings_module)) | ||
django.setup() | ||
except ImportError: | ||
# we could not find the provided settings module... | ||
# at least here it is a fatal error so we can just raise this immediately | ||
self.add_message( | ||
"django-settings-module-not-found", | ||
args=self.config.django_settings_module, | ||
) | ||
# however we'll trundle on with basic settings | ||
from django.conf import ( # pylint: disable=import-outside-toplevel | ||
settings, | ||
) | ||
|
||
settings.configure() | ||
django.setup() | ||
|
||
# now we can add the trasforms specifc to this checker | ||
foreignkey.add_transform(astroid.MANAGER) | ||
|
||
# TODO: this is a bit messy having so many inline imports but in order to avoid | ||
# duplicating the django_installed checker, it'll do for now. In the future, merging | ||
# those two checkers together might make sense. | ||
|
||
@check_messages("django-not-configured") | ||
def visit_module(self, node): | ||
if self._raise_warning: | ||
# just add it to the first node we see... which isn't nice but not sure what else to do | ||
self.add_message("django-not-configured", node=node) | ||
self._raise_warning = False # only raise it once... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,20 @@ | ||
"""Transforms.""" | ||
""" | ||
These transforms replace the Django types with adapted versions to provide | ||
additional typing and method inference to pylint. All of these transforms | ||
are considered "global" to pylint-django, in that all checks and improvements | ||
requre them to be loaded. Additional transforms specific to checkers are loaded | ||
by the checker rather than here. | ||
|
||
For example, the ForeignKeyStringsChecker loads the foreignkey.py transforms | ||
itself as it may be disabled independently of the rest of pylint-django | ||
""" | ||
import os | ||
import re | ||
|
||
import astroid | ||
|
||
from pylint_django.transforms import foreignkey, fields | ||
from pylint_django.transforms import fields | ||
|
||
|
||
foreignkey.add_transform(astroid.MANAGER) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could benefit from a comment either here or on the doc-string text and/or the foreignkey module to explicitly state why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a module-level docstring explaining this already which I think is sufficient: https://github.com/PyCQA/pylint-django/blob/feature/improve-django-settings-handling/pylint_django/transforms/__init__.py#L1 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also added this issue as a discussion for moving all transforms/augmentations into a checker rather than being loaded immediately by the plugin: #290 |
||
fields.add_transforms(astroid.MANAGER) | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -69,7 +69,7 @@ echo "..... Trying to install the new tarball inside a virtualenv" | |
# note: installs with the optional dependency to verify this is still working | ||
virtualenv .venv/test-tarball | ||
source .venv/test-tarball/bin/activate | ||
pip install --no-binary :all: -f dist/ pylint_django[with_django] | ||
pip install -f dist/ pylint_django[with_django] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason for this line & the reason why it is failing is the same - it tries installing pylint-django and all of its dependencies from tartballs. One of them is either new or stopped shipping tarballs, and only has wheel packages. See 10 lines below for how we deal with another package which doesn't ship wheels. My preferred way for dealing with this is to find which package is the one not providing tarballs and
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For now I'll accept this in our own CI while I track down which dependency is the guilty party - I am fairly sure it's clikit and I'll raise a PR/issue there if that's true or as you suggest, explicitly install it ourselves. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I created a new issue to separate this out: #291 |
||
pip freeze | grep Django | ||
deactivate | ||
rm -rf .venv/ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for adding the TODOs as separate issues so we don't forget about them. Interestingly enough the pylint test job in Travis should be catching this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed - I had initially not bothered as I had planned to immediately address them in a new PR but I got distracted / didn't get time. Therefore I've made #289 and #288