├── pylint_django ├── tests │ ├── __init__.py │ ├── input │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ ├── 0002_new_column.txt │ │ │ ├── 0003_without_backwards.txt │ │ │ ├── 0004_noerror_with_backwards.py │ │ │ ├── 0003_without_backwards.py │ │ │ ├── 0001_noerror_initial.py │ │ │ └── 0002_new_column.py │ │ ├── test_app │ │ │ ├── __init__.py │ │ │ ├── models.py │ │ │ └── apps.py │ │ ├── external_factory_boy_noerror.rc │ │ ├── external_drf_noerror_serializer.rc │ │ ├── external_psycopg2_noerror_postgres_fields.rc │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── author.py │ │ │ └── func_noerror_foreign_key_key_cls_unbound_in_same_package.py │ │ ├── external_model_utils_noerror_override_manager.rc │ │ ├── func_modelform_exclude.txt │ │ ├── func_model_does_not_use_unicode_py33.txt │ │ ├── func_model_no_explicit_unicode_str_compat.txt │ │ ├── func_unused_arguments.txt │ │ ├── func_noerror_import_q.py │ │ ├── func_noerror_views.py │ │ ├── func_hard_coded_auth_user.txt │ │ ├── func_noerror_gettext_lazy_format.py │ │ ├── func_noerror_ugettext_lazy_format.py │ │ ├── func_noerror_formview_ancestors.py │ │ ├── func_noerror_issue_46.py │ │ ├── func_modelform_exclude.py │ │ ├── external_drf_noerror_serializer.py │ │ ├── func_noerror_ignore_meta_subclass.py │ │ ├── func_noerror_model_unicode_lambda.py │ │ ├── func_noerror_style_members.py │ │ ├── func_noerror_string_foreignkey.py │ │ ├── func_noerror_foreign_key_package.py │ │ ├── func_noerror_model_unicode_callable.py │ │ ├── func_hard_coded_auth_user.py │ │ ├── func_noerror_wsgi.py │ │ ├── func_noerror_urls.py │ │ ├── func_json_response.txt │ │ ├── func_model_does_not_use_unicode_py33.py │ │ ├── func_noerror_factory_post_generation.py │ │ ├── external_django_tables2_noerror_meta_class.py │ │ ├── func_noerror_foreign_key_ids.py │ │ ├── func_noerror_foreign_key_key_cls_unbound.py │ │ ├── func_noerror_unicode_py2_compatible.py │ │ ├── func_noerror_model_objects.py │ │ ├── func_model_no_explicit_unicode_str_compat.py │ │ ├── func_noerror_duplicate_except_doesnotexist.py │ │ ├── func_noerror_uuid_field.py │ │ ├── func_noerror_test_wsgi_request.py │ │ ├── func_noerror_generic_foreign_key.py │ │ ├── func_noerror_model_methods.py │ │ ├── func_noerror_foreign_key_attributes.py │ │ ├── func_noerror_protected_meta_access.py │ │ ├── func_noerror_models_py33.py │ │ ├── func_json_response.py │ │ ├── func_noerror_forms_py33.py │ │ ├── func_noerror_foreign_key_sets.py │ │ ├── external_model_utils_noerror_override_manager.py │ │ ├── func_noerror_managers_return_querysets.py │ │ ├── func_unused_arguments.py │ │ ├── func_noerror_classviews.py │ │ ├── external_factory_boy_noerror.py │ │ ├── external_psycopg2_noerror_postgres_fields.py │ │ ├── func_noerror_manytomanyfield.py │ │ ├── func_noerror_foreignkeys.py │ │ ├── func_noerror_form_fields.py │ │ └── func_noerror_model_fields.py │ ├── settings.py │ └── test_func.py ├── transforms │ ├── transforms │ │ ├── __init__.py │ │ ├── django_utils_translation.py │ │ └── django_db_models_fields_files.py │ ├── __init__.py │ ├── fields.py │ └── foreignkey.py ├── __pkginfo__.py ├── __init__.py ├── checkers │ ├── __init__.py │ ├── django_installed.py │ ├── auth_user.py │ ├── forms.py │ ├── json_response.py │ ├── models.py │ ├── foreign_key_strings.py │ └── migrations.py ├── compat.py ├── utils.py ├── plugin.py └── augmentations │ └── __init__.py ├── scripts ├── test.sh └── build.sh ├── .coveragerc ├── .landscape.yaml ├── MANIFEST.in ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── SECURITY.md ├── CONTRIBUTORS.md ├── setup.py ├── .travis.yml ├── tox.ini ├── README.rst ├── LICENSE └── CHANGELOG.rst /pylint_django/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pylint_django/tests/input/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pylint_django/tests/input/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pylint_django/tests/input/test_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pylint_django/transforms/transforms/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pylint_django/__pkginfo__.py: -------------------------------------------------------------------------------- 1 | """pkginfo.""" 2 | BASE_ID = 51 3 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python pylint_django/tests/test_func.py -v "$@" 3 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source=pylint_django 3 | 4 | [report] 5 | omit=pylint_django/tests/* 6 | -------------------------------------------------------------------------------- /pylint_django/tests/input/external_factory_boy_noerror.rc: -------------------------------------------------------------------------------- 1 | [testoptions] 2 | requires = factory 3 | -------------------------------------------------------------------------------- /pylint_django/tests/input/test_app/models.py: -------------------------------------------------------------------------------- 1 | from models.author import Author # noqa: F401 2 | -------------------------------------------------------------------------------- /pylint_django/tests/input/external_drf_noerror_serializer.rc: -------------------------------------------------------------------------------- 1 | [testoptions] 2 | requires = rest_framework 3 | -------------------------------------------------------------------------------- /pylint_django/tests/input/external_psycopg2_noerror_postgres_fields.rc: -------------------------------------------------------------------------------- 1 | [testoptions] 2 | requires = psycopg2 3 | -------------------------------------------------------------------------------- /pylint_django/tests/input/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .author import Author 2 | 3 | 4 | __all__ = ('Author',) 5 | -------------------------------------------------------------------------------- /pylint_django/tests/input/external_model_utils_noerror_override_manager.rc: -------------------------------------------------------------------------------- 1 | [testoptions] 2 | requires = model_utils 3 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_modelform_exclude.txt: -------------------------------------------------------------------------------- 1 | modelform-uses-exclude:10:PersonForm.Meta:Use explicit fields instead of exclude in ModelForm 2 | -------------------------------------------------------------------------------- /pylint_django/tests/input/test_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestAppConfig(AppConfig): 5 | name = 'test_app' 6 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_model_does_not_use_unicode_py33.txt: -------------------------------------------------------------------------------- 1 | model-has-unicode:9:SomeModel:Found __unicode__ method on model (SomeModel). Python3 uses __str__. 2 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_model_no_explicit_unicode_str_compat.txt: -------------------------------------------------------------------------------- 1 | model-no-explicit-unicode:18:SomeModel:Model does not explicitly define __unicode__ (SomeModel) 2 | -------------------------------------------------------------------------------- /.landscape.yaml: -------------------------------------------------------------------------------- 1 | uses: 2 | - django 3 | strictness: high 4 | doc-warnings: no 5 | max-line-length: 120 6 | ignore-paths: 7 | - scripts/ 8 | - pylint_django/compat.py 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include *.rst 3 | include LICENSE 4 | include pylint_django/transforms/transforms/*.py 5 | recursive-include pylint_django/tests/ *.py *.rc *.txt 6 | -------------------------------------------------------------------------------- /pylint_django/tests/input/migrations/0002_new_column.txt: -------------------------------------------------------------------------------- 1 | new-db-field-with-default:28:Migration:pylint_django.tests.input.migrations.0002_new_column AddField with default value 2 | -------------------------------------------------------------------------------- /pylint_django/transforms/transforms/django_utils_translation.py: -------------------------------------------------------------------------------- 1 | def gettext_lazy(_): 2 | return '' 3 | 4 | 5 | ugettext_lazy = gettext_lazy # pylint:disable=invalid-name 6 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_unused_arguments.txt: -------------------------------------------------------------------------------- 1 | unused-argument:18:user_detail:Unused argument 'user_id':HIGH 2 | unused-argument:24:UserView.get:Unused argument 'user_id':INFERENCE 3 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_import_q.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about import of Q. 3 | """ 4 | # pylint: disable=missing-docstring,unused-import 5 | from django.db.models import Q # noqa: F401 6 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_views.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain when using function based views. 3 | """ 4 | # pylint: disable=missing-docstring 5 | 6 | 7 | def empty_view(request): 8 | pass 9 | -------------------------------------------------------------------------------- /pylint_django/tests/input/models/author.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring,wrong-import-position 2 | from django.db import models 3 | 4 | 5 | class Author(models.Model): 6 | class Meta: 7 | app_label = 'test_app' 8 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_hard_coded_auth_user.txt: -------------------------------------------------------------------------------- 1 | imported-auth-user:5::User model imported from django.contrib.auth.models 2 | imported-auth-user:6::User model imported from django.contrib.auth.models 3 | hard-coded-auth-user:10:PullRequest:Hard-coded 'auth.User' 4 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_gettext_lazy_format.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about django lazy proxy 3 | when using gettext_lazy 4 | """ 5 | from django.utils.translation import gettext_lazy 6 | 7 | gettext_lazy('{something}').format(something='lala') 8 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_ugettext_lazy_format.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about django lazy proxy 3 | when using ugettext_lazy 4 | """ 5 | from django.utils.translation import ugettext_lazy 6 | 7 | ugettext_lazy('{something}').format(something='lala') 8 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_formview_ancestors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about django FormViews 3 | having too many ancestors 4 | """ 5 | # pylint: disable=missing-docstring 6 | from django.views.generic import FormView 7 | 8 | 9 | class SomeView(FormView): 10 | pass 11 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_issue_46.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about raising DoesNotExist 3 | """ 4 | # pylint: disable=missing-docstring 5 | from django.db import models 6 | 7 | 8 | class SomeModel(models.Model): 9 | pass 10 | 11 | 12 | raise SomeModel.DoesNotExist 13 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_modelform_exclude.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint complains about ModelForm using exclude 3 | """ 4 | # pylint: disable=missing-docstring 5 | from django import forms 6 | 7 | 8 | class PersonForm(forms.ModelForm): 9 | class Meta: 10 | exclude = ('email',) # [modelform-uses-exclude] 11 | -------------------------------------------------------------------------------- /pylint_django/tests/input/external_drf_noerror_serializer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about DRF serializers 3 | """ 4 | # pylint: disable=C0111,W5101 5 | 6 | from rest_framework import serializers 7 | 8 | 9 | class TestSerializerSubclass(serializers.ModelSerializer): 10 | class Meta: 11 | pass 12 | -------------------------------------------------------------------------------- /pylint_django/tests/settings.py: -------------------------------------------------------------------------------- 1 | SECRET_KEY = 'fake-key' 2 | 3 | INSTALLED_APPS = [ 4 | 'django.contrib.auth', 5 | 'django.contrib.contenttypes', 6 | 'test_app', 7 | ] 8 | 9 | MIDDLEWARE = [ 10 | 'django.contrib.sessions.middleware.SessionMiddleware', 11 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 12 | ] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **WARNING:** Please do not report issues about missing Django, see 2 | [README](https://github.com/PyCQA/pylint-django#installation)! 3 | 4 | **TODO:** make sure to post the output of `pip freeze` 5 | 6 | **NOTES:** make sure you have the latest version of 3rd party packages 7 | like `rest_framework`, `factory`, `model_utils`, etc. before reporting 8 | issues! 9 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_ignore_meta_subclass.py: -------------------------------------------------------------------------------- 1 | """ 2 | This test ensures that a 'Meta' class defined on a Django model does 3 | not raise warnings such as 'old-style-class' and 'too-few-public-methods' 4 | """ 5 | # pylint: disable=missing-docstring 6 | 7 | from django.db import models 8 | 9 | 10 | class SomeModel(models.Model): 11 | class Meta: 12 | ordering = ('-id',) 13 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_model_unicode_lambda.py: -------------------------------------------------------------------------------- 1 | """ 2 | Ensures that django models without a __unicode__ method are flagged 3 | """ 4 | # pylint: disable=missing-docstring,wrong-import-position 5 | 6 | from django.db import models 7 | 8 | 9 | class SomeModel(models.Model): 10 | something = models.CharField(max_length=255) 11 | __unicode__ = lambda s: str(s.something) # noqa: E731 12 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_style_members.py: -------------------------------------------------------------------------------- 1 | # Test that using `color_style` or `no_style` 2 | # doesn't raise no-member error 3 | # 4 | # pylint: disable=missing-docstring 5 | 6 | from django.core.management.color import color_style, no_style 7 | 8 | 9 | def function(): 10 | style = color_style() 11 | print(style.SUCCESS("test")) 12 | 13 | style = no_style() 14 | print(style.SUCCESS("test")) 15 | -------------------------------------------------------------------------------- /pylint_django/tests/input/migrations/0003_without_backwards.txt: -------------------------------------------------------------------------------- 1 | missing-backwards-migration-callable:12:Migration:Always include backwards migration callable 2 | missing-backwards-migration-callable:13:Migration:Always include backwards migration callable 3 | missing-backwards-migration-callable:15:Migration:Always include backwards migration callable 4 | missing-backwards-migration-callable:17:Migration:Always include backwards migration callable -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_string_foreignkey.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that PyLint correctly handles string foreign keys 3 | https://github.com/PyCQA/pylint-django/issues/243 4 | """ 5 | # pylint: disable=missing-docstring, hard-coded-auth-user 6 | from django.db import models 7 | 8 | 9 | class Book(models.Model): 10 | author = models.ForeignKey("test_app.Author", models.CASCADE) 11 | user = models.ForeignKey("auth.User", models.PROTECT) 12 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_foreign_key_package.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about ForeignKey pointing to model 3 | in module of models package 4 | """ 5 | # pylint: disable=missing-docstring 6 | from django.db import models 7 | 8 | 9 | class Book(models.Model): 10 | author = models.ForeignKey(to='pylint_django.tests.input.Author', on_delete=models.CASCADE) 11 | 12 | def get_author_name(self): 13 | return self.author.id 14 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_model_unicode_callable.py: -------------------------------------------------------------------------------- 1 | """ 2 | Ensures that django models without a __unicode__ method are flagged 3 | """ 4 | # pylint: disable=missing-docstring,wrong-import-position 5 | 6 | from django.db import models 7 | 8 | 9 | def external_unicode_func(model): 10 | return model.something 11 | 12 | 13 | class SomeModel(models.Model): 14 | something = models.CharField(max_length=255) 15 | __unicode__ = external_unicode_func 16 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_hard_coded_auth_user.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring, wildcard-import, unused-wildcard-import 2 | # flake8: noqa=F401, F403 3 | 4 | from django.db import models 5 | from django.contrib.auth.models import * # [imported-auth-user] 6 | from django.contrib.auth.models import User # [imported-auth-user] 7 | 8 | 9 | class PullRequest(models.Model): 10 | author = models.ForeignKey("auth.User", models.CASCADE) # [hard-coded-auth-user] 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | .env 37 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Standard WSGI config created by django-admin startproject. 3 | 4 | Used to verify pylint_django doesn't produce invalid-name for 5 | the application variable. See: 6 | https://github.com/PyCQA/pylint-django/issues/77 7 | """ 8 | 9 | import os 10 | 11 | from django.core.wsgi import get_wsgi_application 12 | 13 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproj.settings") 14 | 15 | application = get_wsgi_application() 16 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about attributes and methods 3 | when creating a typical urls.py 4 | """ 5 | # pylint: disable=missing-docstring 6 | 7 | from django.views.generic import TemplateView 8 | from django.conf.urls import url 9 | 10 | 11 | class BoringView(TemplateView): 12 | pass 13 | 14 | 15 | urlpatterns = [ 16 | url(r'^something', 17 | BoringView.as_view(), 18 | name='something'), 19 | ] 20 | -------------------------------------------------------------------------------- /pylint_django/__init__.py: -------------------------------------------------------------------------------- 1 | """pylint_django module.""" 2 | from __future__ import absolute_import 3 | 4 | import sys 5 | 6 | from pylint_django import plugin 7 | 8 | if sys.version_info < (3, ): 9 | raise DeprecationWarning("Version 0.11.1 was the last to support Python 2. " 10 | "Please migrate to Python 3!") 11 | 12 | register = plugin.register # pylint: disable=invalid-name 13 | load_configuration = plugin.load_configuration # pylint: disable=invalid-name 14 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_json_response.txt: -------------------------------------------------------------------------------- 1 | http-response-with-json-dumps:9:say_yes:Instead of HttpResponse(json.dumps(data)) use JsonResponse(data) 2 | http-response-with-json-dumps:14:say_yes2:Instead of HttpResponse(json.dumps(data)) use JsonResponse(data) 3 | redundant-content-type-for-json-response:23:redundant_content_type:Redundant content_type parameter for JsonResponse() 4 | http-response-with-content-type-json:28:content_type_json:Instead of HttpResponse(content_type='application/json') use JsonResponse() 5 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_model_does_not_use_unicode_py33.py: -------------------------------------------------------------------------------- 1 | """ 2 | Ensures that under PY3 django models with a __unicode__ method are flagged 3 | """ 4 | # pylint: disable=missing-docstring 5 | 6 | from django.db import models 7 | 8 | 9 | class SomeModel(models.Model): # [model-has-unicode] 10 | something = models.CharField(max_length=255) 11 | # no __str__ method 12 | 13 | something.something_else = 1 14 | 15 | def lala(self): 16 | pass 17 | 18 | def __unicode__(self): 19 | return self.something 20 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_factory_post_generation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about no self argument in 3 | factory.post_generation method. 4 | """ 5 | # pylint: disable=missing-docstring,too-few-public-methods,unused-argument,no-member 6 | import factory 7 | 8 | 9 | class SomeModelFactory(factory.Factory): 10 | class Meta: 11 | pass 12 | 13 | @factory.post_generation 14 | def action(obj, create, extracted, **kwargs): 15 | if extracted: 16 | obj.do_action() 17 | obj.save() 18 | -------------------------------------------------------------------------------- /pylint_django/tests/input/external_django_tables2_noerror_meta_class.py: -------------------------------------------------------------------------------- 1 | # Check that Meta class definitions for django_tables2 classes 2 | # don't produce old-style-class warnings, see 3 | # https://github.com/PyCQA/pylint-django/issues/56 4 | 5 | # pylint: disable=missing-docstring,too-few-public-methods 6 | 7 | from django.db import models 8 | import django_tables2 as tables 9 | 10 | 11 | class SimpleModel(models.Model): 12 | name = models.CharField() 13 | 14 | 15 | class SimpleTable(tables.Table): 16 | class Meta: 17 | model = SimpleModel 18 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_foreign_key_ids.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about foreign key id access 3 | """ 4 | # pylint: disable=missing-docstring,wrong-import-position 5 | from django.db import models 6 | 7 | 8 | class SomeModel(models.Model): 9 | count = models.IntegerField() 10 | 11 | 12 | class SomeOtherModel(models.Model): 13 | some_model = models.ForeignKey(SomeModel, on_delete=models.CASCADE) 14 | number = models.IntegerField() 15 | 16 | def do_something(self): 17 | self.number = self.some_model_id 18 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_foreign_key_key_cls_unbound.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about ForeignKey pointing to model 3 | in module of models package 4 | """ 5 | # pylint: disable=missing-docstring 6 | from django.db import models 7 | 8 | 9 | class FairyTail(models.Model): 10 | # fails with "UnboundLocalError: local variable 'key_cls' referenced before assignment" 11 | author = models.ForeignKey(to='input.Author', null=True, on_delete=models.CASCADE) 12 | 13 | def get_author_name(self): 14 | return self.author.id 15 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_unicode_py2_compatible.py: -------------------------------------------------------------------------------- 1 | """ 2 | Ensures that no '__unicode__ missing' warning is emitted if the 3 | Django python3/2 compatability dectorator is used 4 | 5 | See https://github.com/PyCQA/pylint-django/issues/10 6 | """ 7 | # pylint: disable=missing-docstring 8 | from six import python_2_unicode_compatible 9 | from django.db import models 10 | 11 | 12 | @python_2_unicode_compatible 13 | class ModelName(models.Model): 14 | name = models.CharField(max_length=200) 15 | 16 | def __str__(self): 17 | return self.name 18 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_model_objects.py: -------------------------------------------------------------------------------- 1 | # Test that defining `objects` as a regular model manager 2 | # doesn't raise issues, see 3 | # https://github.com/PyCQA/pylint-django/issues/144 4 | # 5 | # pylint: disable=missing-docstring 6 | 7 | from django.db import models 8 | 9 | 10 | class ModelWithRegularManager(models.Model): 11 | objects = models.Manager() 12 | 13 | name = models.CharField() 14 | 15 | 16 | def function(): 17 | record = ModelWithRegularManager.objects.all()[0] 18 | 19 | for record in ModelWithRegularManager.objects.all(): 20 | print(record.name) 21 | -------------------------------------------------------------------------------- /pylint_django/tests/input/migrations/0004_noerror_with_backwards.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring, invalid-name 2 | from django.db import migrations 3 | 4 | 5 | def forwards_test(apps, schema_editor): 6 | pass 7 | 8 | 9 | def backwards_test(apps, schema_editor): 10 | pass 11 | 12 | 13 | class Migration(migrations.Migration): 14 | 15 | operations = [ 16 | migrations.RunPython(forwards_test, backwards_test), 17 | migrations.RunPython(forwards_test, reverse_code=backwards_test), 18 | migrations.RunPython(code=forwards_test, reverse_code=backwards_test) 19 | ] 20 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_model_no_explicit_unicode_str_compat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Ensures that django models with a __str__ method defined in an ancestor and 3 | python_2_unicode_compatible decorator applied are flagged correctly as not 4 | having explicitly defined __unicode__ 5 | """ 6 | # pylint: disable=missing-docstring 7 | 8 | from django.db import models 9 | from six import python_2_unicode_compatible 10 | 11 | 12 | @python_2_unicode_compatible 13 | class BaseModel(models.Model): 14 | def __str__(self): 15 | return 'Foo' 16 | 17 | 18 | class SomeModel(BaseModel): # [model-no-explicit-unicode] 19 | pass 20 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | [latest](https://pypi.org/project/pylint-django/) | :heavy_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | In case you have found a security problem with pylint-django *DO NOT* report 12 | it into GitHub Issues. Instead go to 13 | [https://tidelift.com/security](https://tidelift.com/security) 14 | and follow the instructions there. 15 | 16 | At least one of the package maintainers ([@atodorov](http://github.com/atodorov)) 17 | is a lifter at Tidelift and will be notified when you report the security 18 | problem with them! 19 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_duplicate_except_doesnotexist.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about duplicate 3 | except blocks catching DoesNotExist exceptions: 4 | https://github.com/PyCQA/pylint-django/issues/81 5 | """ 6 | # pylint: disable=missing-docstring 7 | from django.db import models 8 | 9 | 10 | class Book(models.Model): 11 | name = models.CharField(max_length=100) 12 | 13 | 14 | class Author(models.Model): 15 | name = models.CharField(max_length=100) 16 | 17 | 18 | def dummy_func(): 19 | try: 20 | print("foo") 21 | except Book.DoesNotExist: 22 | print("bar") 23 | except Author.DoesNotExist: 24 | print("baz") 25 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_uuid_field.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about UUID fields. 3 | """ 4 | # pylint: disable=C0111,W5101 5 | from __future__ import print_function 6 | from django.db import models 7 | 8 | 9 | class LotsOfFieldsModel(models.Model): 10 | uuidfield = models.UUIDField() 11 | 12 | def uuidfield_tests(self): 13 | print(self.uuidfield.bytes) 14 | print(self.uuidfield.bytes_le) 15 | print(self.uuidfield.fields[2]) 16 | print(self.uuidfield.hex) 17 | # print(self.uuidfield.int) # Don't know how to properly check this one 18 | print(self.uuidfield.variant) 19 | print(self.uuidfield.version) 20 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_test_wsgi_request.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about a standard test. See: 3 | https://github.com/PyCQA/pylint-django/issues/78 4 | """ 5 | 6 | from django.test import TestCase 7 | from django.db import models 8 | 9 | 10 | class SomeModel(models.Model): 11 | """Just a model.""" 12 | 13 | 14 | class SomeTestCase(TestCase): 15 | """A test cast.""" 16 | def test_thing(self): 17 | """Test a thing.""" 18 | expected_object = SomeModel() 19 | response = self.client.get('/get/some/thing/') 20 | self.assertEqual(response.status_code, 200) 21 | self.assertEqual(response.context['object'], expected_object) 22 | -------------------------------------------------------------------------------- /pylint_django/tests/input/migrations/0003_without_backwards.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring, invalid-name 2 | from django.db import migrations 3 | 4 | 5 | def forwards_test(apps, schema_editor): 6 | pass 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | operations = [ 12 | migrations.RunPython(), # [missing-backwards-migration-callable] 13 | migrations.RunPython( # [missing-backwards-migration-callable] 14 | forwards_test), 15 | migrations.RunPython( # [missing-backwards-migration-callable] 16 | code=forwards_test), 17 | migrations.RunPython( # [missing-backwards-migration-callable] 18 | code=forwards_test, atomic=False) 19 | ] 20 | -------------------------------------------------------------------------------- /pylint_django/transforms/transforms/django_db_models_fields_files.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=super-init-not-called 2 | from django.db.models.fields import files as django_fields 3 | 4 | 5 | class FileField(django_fields.FieldFile, django_fields.FileField): 6 | def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, **kwargs): 7 | django_fields.FileField.__init__(verbose_name, name, upload_to, storage, **kwargs) 8 | 9 | 10 | class ImageField(django_fields.ImageFieldFile, django_fields.ImageField): 11 | def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs): 12 | django_fields.ImageField.__init__(verbose_name, name, width_field, height_field, **kwargs) 13 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_generic_foreign_key.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about GenericForeignKey fields: 3 | https://github.com/PyCQA/pylint-django/issues/230 4 | """ 5 | # pylint: disable=missing-docstring 6 | 7 | from django.db import models 8 | from django.contrib.contenttypes.models import ContentType 9 | from django.contrib.contenttypes.fields import GenericForeignKey 10 | 11 | 12 | class Ownership(models.Model): 13 | # for #230 the important bit is this FK field which doesn't 14 | # have any keyword arguments! 15 | owner_type = models.ForeignKey(ContentType, models.CASCADE) 16 | owner_id = models.PositiveIntegerField() 17 | owner = GenericForeignKey("owner_type", "owner_id") 18 | -------------------------------------------------------------------------------- /pylint_django/tests/input/migrations/0001_noerror_initial.py: -------------------------------------------------------------------------------- 1 | """ 2 | Initial migration which should not raise any pylint warnings. 3 | """ 4 | # pylint: disable=missing-docstring, invalid-name 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | operations = [ 10 | migrations.CreateModel( 11 | name='TestRun', 12 | fields=[ 13 | ('id', models.AutoField(serialize=False, primary_key=True)), 14 | ('summary', models.TextField()), 15 | ('environment_id', models.IntegerField(default=0)), 16 | ('auto_update_run_status', models.BooleanField(default=False)), 17 | ], 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_model_methods.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about using Model and Manager methods 3 | """ 4 | # pylint: disable=missing-docstring 5 | from django.db import models 6 | 7 | 8 | class SomeModel(models.Model): 9 | name = models.CharField(max_length=64) 10 | 11 | 12 | if __name__ == '__main__': 13 | MODEL = SomeModel() 14 | MODEL.save() 15 | MODEL.delete() 16 | 17 | COUNT = SomeModel.objects.count() 18 | # added in django 1.6 19 | FIRST = SomeModel.objects.first() 20 | LAST = SomeModel.objects.last() 21 | 22 | DB_RECORD, CREATED = SomeModel.objects.get_or_create(name='Tester') 23 | EXCLUDED_IDS = [obj.pk for obj in SomeModel.objects.exclude(name__isnull=True)] 24 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_foreign_key_attributes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about foreign key sets on models 3 | """ 4 | # pylint: disable=missing-docstring 5 | 6 | from django.db import models 7 | 8 | 9 | class SomeModel(models.Model): 10 | name = models.CharField(max_length=20) 11 | timestamp = models.DateTimeField() 12 | 13 | 14 | class OtherModel(models.Model): 15 | something = models.ForeignKey(SomeModel, on_delete=models.CASCADE) 16 | elsething = models.OneToOneField(SomeModel, on_delete=models.CASCADE) 17 | 18 | def something_doer(self): 19 | part_a = '%s - %s' % (self.something.name, self.something.timestamp) 20 | part_b = self.elsething.name 21 | return part_a, part_b 22 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_protected_meta_access.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests to make sure that access to _meta on a model does not raise 3 | a protected-access warning, as it is part of the public API since 4 | Django 1.8 5 | (see https://github.com/PyCQA/pylint-django/issues/66, 6 | and https://docs.djangoproject.com/en/1.9/ref/models/meta/) 7 | """ 8 | # pylint: disable=missing-docstring 9 | from __future__ import print_function 10 | from django.db import models 11 | 12 | 13 | class ModelWhichLikesMeta(models.Model): 14 | ursuary = models.BooleanField(default=False) 15 | 16 | def do_a_thing(self): 17 | return self._meta.get_field('ursuary') 18 | 19 | 20 | if __name__ == '__main__': 21 | MODEL = ModelWhichLikesMeta() 22 | MODEL.save() 23 | print(MODEL._meta.get_field('ursuary')) 24 | print(MODEL.do_a_thing()) 25 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_models_py33.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about a fairly standard 3 | Django Model 4 | """ 5 | # pylint: disable=missing-docstring 6 | from django.db import models 7 | 8 | 9 | class SomeModel(models.Model): 10 | class Meta: 11 | pass 12 | 13 | some_field = models.CharField(max_length=20) 14 | 15 | other_fields = models.ManyToManyField('AnotherModel') 16 | 17 | def stuff(self): 18 | try: 19 | print(self._meta) 20 | print(self.other_fields.all()[0]) 21 | except self.DoesNotExist: 22 | print('does not exist') 23 | except self.MultipleObjectsReturned: 24 | print('lala') 25 | 26 | print(self.get_some_field_display()) 27 | 28 | 29 | class SubclassModel(SomeModel): 30 | class Meta: 31 | pass 32 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | * [carlio](https://github.com/carlio) 2 | * [mbarrien](https://github.com/mbarrien) 3 | * [frost-nzcr4](https://github.com/frost-nzcr4) 4 | * [ustun](https://github.com/ustun) 5 | * [jproffitt](https://github.com/jproffitt) 6 | * [lhupfeldt](https://github.com/lhupfeldt) 7 | * [smirolo](https://github.com/smirolo) 8 | * [mbertolacci](https://github.com/mbertolacci) 9 | * [atodorov](https://github.com/atodorov) 10 | * [bittner](https://github.com/bittner) 11 | * [federicobond](https://github.com/federicobond) 12 | * [matusvalo](https://github.com/matusvalo) 13 | * [fadedDexofan](https://github.com/fadeddexofan) 14 | * [imomaliev](https://github.com/imomaliev) 15 | * [psrb](https://github.com/psrb) 16 | * [WayneLambert](https://github.com/WayneLambert) 17 | * [alejandro-angulo](https://github.com/alejandro-angulo) 18 | * [brymut](https://github.com/brymut) 19 | 20 | -------------------------------------------------------------------------------- /pylint_django/checkers/__init__.py: -------------------------------------------------------------------------------- 1 | """Checkers.""" 2 | from pylint_django.checkers.django_installed import DjangoInstalledChecker 3 | from pylint_django.checkers.models import ModelChecker 4 | from pylint_django.checkers.json_response import JsonResponseChecker 5 | from pylint_django.checkers.forms import FormChecker 6 | from pylint_django.checkers.auth_user import AuthUserChecker 7 | from pylint_django.checkers.foreign_key_strings import ForeignKeyStringsChecker 8 | 9 | 10 | def register_checkers(linter): 11 | """Register checkers.""" 12 | linter.register_checker(ModelChecker(linter)) 13 | linter.register_checker(DjangoInstalledChecker(linter)) 14 | linter.register_checker(JsonResponseChecker(linter)) 15 | linter.register_checker(FormChecker(linter)) 16 | linter.register_checker(AuthUserChecker(linter)) 17 | linter.register_checker(ForeignKeyStringsChecker(linter)) 18 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_json_response.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring, line-too-long 2 | 3 | import json 4 | from django import http 5 | from django.http import HttpResponse 6 | 7 | 8 | def say_yes(): 9 | return HttpResponse(json.dumps({'rc': 0, 'response': 'ok'})) # [http-response-with-json-dumps] 10 | 11 | 12 | def say_yes2(): 13 | data = {'rc': 0, 'response': 'ok'} 14 | return http.HttpResponse(json.dumps(data)) # [http-response-with-json-dumps] 15 | 16 | 17 | def say_no(): 18 | return HttpResponse("no") 19 | 20 | 21 | def redundant_content_type(): 22 | data = {'rc': 0, 'response': 'ok'} 23 | return http.JsonResponse(data, content_type='application/json') # [redundant-content-type-for-json-response] 24 | 25 | 26 | def content_type_json(): 27 | json_data = "this comes from somewhere" 28 | return HttpResponse(json_data, content_type='application/json') # [http-response-with-content-type-json] 29 | -------------------------------------------------------------------------------- /pylint_django/compat.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # pylint: skip-file 3 | # no sane linter can figure out the hackiness in this compatability layer... 4 | 5 | try: 6 | from astroid.nodes import ClassDef, FunctionDef, ImportFrom, AssignName, Attribute 7 | except ImportError: 8 | from astroid.nodes import ( 9 | Class as ClassDef, 10 | Function as FunctionDef, 11 | From as ImportFrom, 12 | AssName as AssignName, 13 | Getattr as Attribute, 14 | ) 15 | 16 | # pylint 2.04->2.2 : YES was renamed to Uninferable, then YES became deprecated, then was removed 17 | try: 18 | from astroid.bases import YES as Uninferable 19 | except ImportError: 20 | try: 21 | from astroid.util import YES as Uninferable 22 | except ImportError: 23 | from astroid.util import Uninferable 24 | 25 | import pylint 26 | 27 | # pylint before version 2.3 does not support load_configuration() hook. 28 | LOAD_CONFIGURATION_SUPPORTED = pylint.__pkginfo__.numversion >= (2, 3) 29 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_forms_py33.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about django Forms 3 | """ 4 | # pylint: disable=missing-docstring,wrong-import-position 5 | 6 | from django import forms 7 | 8 | 9 | class TestForm(forms.Form): 10 | class Meta: 11 | pass 12 | 13 | some_field = forms.CharField() 14 | 15 | def clean(self): 16 | print(self.cleaned_data) 17 | print(self.fields) 18 | print(self.error_class) 19 | 20 | 21 | class TestModelForm(forms.ModelForm): 22 | class Meta: 23 | pass 24 | 25 | 26 | class TestFormSubclass(forms.Form): 27 | class Meta: 28 | pass 29 | 30 | 31 | class TestModelFormSubclass(forms.ModelForm): 32 | class Meta: 33 | pass 34 | 35 | 36 | class TestFormWidgetAssignment(forms.Form): 37 | 38 | multi_field = forms.MultipleChoiceField(choices=[('1', 'First'), ('2', 'Second')]) 39 | 40 | class Meta: 41 | widgets = {} 42 | widgets['multi_field'] = forms.CheckboxSelectMultiple 43 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_foreign_key_sets.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about foreign key sets on models 3 | """ 4 | # pylint: disable=missing-docstring 5 | 6 | from django.db import models 7 | 8 | 9 | class SomeModel(models.Model): 10 | name = models.CharField(max_length=20) 11 | 12 | def get_others(self): 13 | return self.othermodel_set.all() 14 | 15 | def get_first(self): 16 | return self.othermodel_set.first() 17 | 18 | def othermodel_update_or_create(self): 19 | return self.othermodel_set.update_or_create() 20 | 21 | 22 | class OtherModel(models.Model): 23 | count = models.IntegerField() 24 | something = models.ForeignKey(SomeModel, on_delete=models.CASCADE) 25 | 26 | 27 | class ThirdModel(models.Model): 28 | whatever = models.ForeignKey(SomeModel, related_name='whatevs', 29 | on_delete=models.CASCADE) 30 | 31 | 32 | def count_whatevers(): 33 | if SomeModel().whatevs.exists(): 34 | return SomeModel().whatevs.count() 35 | return -1 36 | -------------------------------------------------------------------------------- /pylint_django/tests/input/external_model_utils_noerror_override_manager.py: -------------------------------------------------------------------------------- 1 | # Check that when overriding the 'objects' attribite of a model class 2 | # with a manager from the django-model-utils package pylint-django 3 | # does not report not-an-iterator error, see 4 | # https://github.com/PyCQA/pylint-django/issues/117 5 | # pylint: disable=missing-docstring, invalid-name 6 | 7 | from model_utils.managers import InheritanceManager 8 | 9 | from django.db import models 10 | 11 | 12 | class BaseModel(models.Model): 13 | name = models.CharField() 14 | 15 | 16 | class BuggyModel(models.Model): 17 | objects = InheritanceManager() 18 | 19 | name = models.CharField() 20 | 21 | 22 | def function(): 23 | for record in BaseModel.objects.all(): 24 | print(record.name) 25 | 26 | for record in BuggyModel.objects.all(): 27 | print(record.name) 28 | 29 | 30 | class Unit(models.Model): 31 | sim = models.CharField(max_length=64, null=True, blank=True) 32 | objects = InheritanceManager() 33 | 34 | 35 | installed_units = Unit.objects.filter(sim__isnull=False) 36 | ids = [u.sim for u in installed_units] 37 | -------------------------------------------------------------------------------- /pylint_django/tests/input/models/func_noerror_foreign_key_key_cls_unbound_in_same_package.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not crash with ForeignKey string reference pointing to model 3 | in module of models package. See 4 | https://github.com/PyCQA/pylint-django/issues/232 5 | 6 | Note: the no-member disable is here b/c pylint-django doesn't know how to 7 | load models.author.Author. When pylint-django tries to load models referenced 8 | by a single string it assumes they are found in the same module it is inspecting. 9 | Hence it can't find the Author class here so it tells us it doesn't have an 10 | 'id' attribute. Also see: 11 | https://github.com/PyCQA/pylint-django/issues/232#issuecomment-495242695 12 | """ 13 | # pylint: disable=missing-docstring, no-member 14 | from django.db import models 15 | 16 | 17 | class FairyTail(models.Model): 18 | # fails with "UnboundLocalError: local variable 'key_cls' referenced before assignment" 19 | # when 'Author' model comes from same models package 20 | author = models.ForeignKey(to='Author', null=True, on_delete=models.CASCADE) 21 | 22 | def get_author_name(self): 23 | return self.author.id # disable via no-member 24 | -------------------------------------------------------------------------------- /pylint_django/tests/input/migrations/0002_new_column.py: -------------------------------------------------------------------------------- 1 | """ 2 | Migration file which adds a new column with a default value. 3 | This should trigger a warning because adding new columns with 4 | default value on a large table leads to DB performance issues. 5 | 6 | See: 7 | https://github.com/PyCQA/pylint-django/issues/118 and 8 | https://docs.djangoproject.com/en/2.0/topics/migrations/#postgresql 9 | 10 | > ... adding columns with default values will cause a full rewrite of 11 | > the table, for a time proportional to its size. 12 | > For this reason, it’s recommended you always create new columns with 13 | > null=True, as this way they will be added immediately. 14 | """ 15 | # pylint: disable=missing-docstring, invalid-name 16 | from datetime import timedelta 17 | from django.db import migrations, models 18 | 19 | 20 | class Migration(migrations.Migration): 21 | 22 | dependencies = [ 23 | ('input', '0001_noerror_initial'), 24 | ] 25 | 26 | operations = [ 27 | # add a timedelta field 28 | migrations.AddField( # [new-db-field-with-default] 29 | model_name='testrun', 30 | name='estimated_time', 31 | field=models.DurationField(default=timedelta(0)), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /pylint_django/utils.py: -------------------------------------------------------------------------------- 1 | """Utils.""" 2 | import sys 3 | import astroid 4 | 5 | from astroid.bases import Instance 6 | from astroid.exceptions import InferenceError 7 | from astroid.nodes import ClassDef 8 | 9 | from pylint_django.compat import Uninferable 10 | 11 | PY3 = sys.version_info >= (3, 0) # TODO: pylint_django doesn't support Py2 any more 12 | 13 | 14 | def node_is_subclass(cls, *subclass_names): 15 | """Checks if cls node has parent with subclass_name.""" 16 | if not isinstance(cls, (ClassDef, Instance)): 17 | return False 18 | 19 | if cls.bases == Uninferable: 20 | return False 21 | for base_cls in cls.bases: 22 | try: 23 | for inf in base_cls.inferred(): 24 | if inf.qname() in subclass_names: 25 | return True 26 | if inf != cls and node_is_subclass(inf, *subclass_names): 27 | # check up the hierarchy in case we are a subclass of 28 | # a subclass of a subclass ... 29 | return True 30 | except InferenceError: 31 | continue 32 | 33 | return False 34 | 35 | 36 | def is_migrations_module(node): 37 | if not isinstance(node, astroid.Module): 38 | return False 39 | 40 | return 'migrations' in node.path[0] and not node.path[0].endswith('__init__.py') 41 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_managers_return_querysets.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about Manager methods returning lists 3 | when they in fact return QuerySets 4 | """ 5 | 6 | from django.db import models 7 | 8 | 9 | class SomeModelQuerySet(models.QuerySet): 10 | """ A missing docstring """ 11 | @staticmethod 12 | def public_method(): 13 | """ A missing docstring """ 14 | return 1 15 | 16 | 17 | class SomeModelManager(models.Manager): 18 | """A missing docstring""" 19 | 20 | @staticmethod 21 | def public_method(): 22 | """ A missing docstring """ 23 | return 1 24 | 25 | @staticmethod 26 | def another_public_method(): 27 | """ A missing docstring """ 28 | return 1 29 | 30 | 31 | class SomeModel(models.Model): 32 | """ A missing docstring """ 33 | 34 | name = models.CharField(max_length=20) 35 | datetimestamp = models.DateTimeField() 36 | 37 | objects = SomeModelManager() 38 | 39 | 40 | class OtherModel(models.Model): 41 | """ A missing docstring """ 42 | 43 | name = models.CharField(max_length=20) 44 | somemodel = models.ForeignKey(SomeModel, on_delete=models.CASCADE, null=False) 45 | 46 | 47 | def call_some_querysets(): 48 | """ A missing docstring """ 49 | 50 | not_a_list = SomeModel.objects.filter() 51 | not_a_list.values_list('id', flat=True) 52 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_unused_arguments.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint still complains about unused-arguments for other 3 | arguments if a function/method contains an argument named `request`. 4 | """ 5 | # pylint: disable=missing-docstring 6 | 7 | from django.http import JsonResponse 8 | from django.views import View 9 | 10 | # Pylint generates the warning `redefined-outer-name` if an argument name shadows 11 | # a variable name from an outer scope. But if that argument name is ignored this 12 | # warning will not be generated. 13 | # Therefore define request here to cover this behaviour in this test case. 14 | 15 | request = None # pylint: disable=invalid-name 16 | 17 | 18 | def user_detail(request, user_id): # [unused-argument] 19 | # nothing is done with user_id 20 | return JsonResponse({'username': 'steve'}) 21 | 22 | 23 | class UserView(View): 24 | def get(self, request, user_id): # [unused-argument] 25 | # nothing is done with user_id 26 | return JsonResponse({'username': 'steve'}) 27 | 28 | 29 | # The following views are already covered in other test cases. 30 | # They are included here for completeness sake. 31 | 32 | def welcome_view(request): 33 | # just don't use `request' b/c we could have Django views 34 | # which never use it! 35 | return JsonResponse({'message': 'welcome'}) 36 | 37 | 38 | class CBV(View): 39 | def get(self, request): 40 | return JsonResponse({'message': 'hello world'}) 41 | -------------------------------------------------------------------------------- /pylint_django/checkers/django_installed.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from pylint.checkers import BaseChecker 3 | from pylint.checkers.utils import check_messages 4 | from pylint_django.__pkginfo__ import BASE_ID 5 | 6 | 7 | class DjangoInstalledChecker(BaseChecker): 8 | name = 'django-installed-checker' 9 | 10 | msgs = { 11 | 'F%s01' % BASE_ID: ("Django is not available on the PYTHONPATH", 12 | 'django-not-available', 13 | "Django could not be imported by the pylint-django plugin, so most Django related " 14 | "improvements to pylint will fail."), 15 | 16 | 'W%s99' % BASE_ID: ('Placeholder message to prevent disabling of checker', 17 | 'django-not-available-placeholder', 18 | 'PyLint does not recognise checkers as being enabled unless they have at least' 19 | ' one message which is not fatal...') 20 | } 21 | 22 | @check_messages('django-not-available') 23 | def open(self): 24 | try: 25 | __import__('django') 26 | except ImportError: 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('pylint_django') 30 | self.add_message('django-not-available') 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """ 3 | Setup module for Pylint plugin for Django. 4 | """ 5 | from setuptools import setup, find_packages 6 | 7 | LONG_DESCRIPTION = open('README.rst').read() + "\n" + open('CHANGELOG.rst').read() 8 | 9 | setup( 10 | name='pylint-django', 11 | url='https://github.com/PyCQA/pylint-django', 12 | author='landscape.io', 13 | author_email='code@landscape.io', 14 | description='A Pylint plugin to help Pylint understand the Django web framework', 15 | long_description=LONG_DESCRIPTION, 16 | version='2.4.2', 17 | packages=find_packages(), 18 | include_package_data=True, 19 | install_requires=[ 20 | 'pylint-plugin-utils>=0.5', 21 | 'pylint>=2.0', 22 | ], 23 | extras_require={ 24 | 'with_django': ['Django'], 25 | 'for_tests': ['django_tables2', 'factory-boy', 'coverage', 'pytest', 'Faker<=5.0.1'], 26 | }, 27 | license='GPLv2', 28 | classifiers=[ 29 | 'Environment :: Console', 30 | 'Intended Audience :: Developers', 31 | 'Operating System :: Unix', 32 | 'Topic :: Software Development :: Quality Assurance', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 3.6', 35 | 'Programming Language :: Python :: 3.7', 36 | 'Programming Language :: Python :: 3.8', 37 | ], 38 | keywords=['pylint', 'django', 'plugin'], 39 | zip_safe=False, 40 | project_urls={ 41 | 'Changelog': 'https://github.com/PyCQA/pylint-django/blob/master/CHANGELOG.rst', 42 | }, 43 | ) 44 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_classviews.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about attributes and methods 3 | when using Class-based Views 4 | """ 5 | # pylint: disable=missing-docstring 6 | 7 | from django.db import models 8 | from django.http import JsonResponse 9 | from django.views.generic import DetailView 10 | from django.views.generic import TemplateView 11 | from django.views.generic import View 12 | from django.views.generic.edit import CreateView 13 | 14 | 15 | class BoringView(TemplateView): 16 | # ensure that args, kwargs and request are not thrown up as errors 17 | def get_context_data(self, **kwargs): 18 | return { 19 | 'request': self.request, 20 | 'args': self.args, 21 | 'kwargs': self.kwargs 22 | } 23 | 24 | 25 | class JsonView(View): 26 | def post(self, request): 27 | # do something with objects but don't use 28 | # self or request 29 | return JsonResponse({'rc': 0, 'response': 'ok'}) 30 | 31 | 32 | class Book(models.Model): 33 | name = models.CharField(max_length=100) 34 | good = models.BooleanField(default=False) 35 | 36 | 37 | class GetBook(DetailView): 38 | model = Book 39 | template_name = 'books/get.html' 40 | http_method_names = ['get'] 41 | 42 | 43 | class CreateBook(CreateView): 44 | model = Book 45 | template_name = 'books/new.html' 46 | 47 | def get_context_data(self, **kwargs): 48 | context = super().get_context_data(**kwargs) 49 | context['page_title'] = 'New book' 50 | return context 51 | -------------------------------------------------------------------------------- /pylint_django/checkers/auth_user.py: -------------------------------------------------------------------------------- 1 | from pylint import interfaces 2 | from pylint import checkers 3 | from pylint.checkers import utils 4 | 5 | from pylint_django.__pkginfo__ import BASE_ID 6 | 7 | 8 | class AuthUserChecker(checkers.BaseChecker): 9 | __implements__ = (interfaces.IAstroidChecker,) 10 | 11 | name = 'auth-user-checker' 12 | 13 | msgs = {'E%d41' % BASE_ID: ("Hard-coded 'auth.User'", 14 | 'hard-coded-auth-user', 15 | "Don't hard-code the auth.User model. " 16 | "Use settings.AUTH_USER_MODEL instead!"), 17 | 'E%d42' % BASE_ID: ("User model imported from django.contrib.auth.models", 18 | 'imported-auth-user', 19 | "Don't import django.contrib.auth.models.User model. " 20 | "Use django.contrib.auth.get_user_model() instead!")} 21 | 22 | @utils.check_messages('hard-coded-auth-user') 23 | def visit_const(self, node): 24 | # for now we don't check if the parent is a ForeignKey field 25 | # because the user model should not be hard-coded anywhere 26 | if node.value == 'auth.User': 27 | self.add_message('hard-coded-auth-user', node=node) 28 | 29 | @utils.check_messages('imported-auth-user') 30 | def visit_importfrom(self, node): 31 | if node.modname == 'django.contrib.auth.models': 32 | for imported_names in node.names: 33 | if imported_names[0] in ['*', 'User']: 34 | self.add_message('imported-auth-user', node=node) 35 | break 36 | -------------------------------------------------------------------------------- /pylint_django/plugin.py: -------------------------------------------------------------------------------- 1 | """Common Django module.""" 2 | from pylint.checkers.base import NameChecker 3 | from pylint_plugin_utils import get_checker 4 | 5 | from pylint_django.checkers import register_checkers 6 | 7 | # we want to import the transforms to make sure they get added to the astroid manager, 8 | # however we don't actually access them directly, so we'll disable the warning 9 | from pylint_django import transforms # noqa, pylint: disable=unused-import 10 | from pylint_django import compat 11 | 12 | 13 | def load_configuration(linter): 14 | """ 15 | Amend existing checker config. 16 | """ 17 | name_checker = get_checker(linter, NameChecker) 18 | name_checker.config.good_names += ('qs', 'urlpatterns', 'register', 'app_name', 19 | 'handler400', 'handler403', 'handler404', 'handler500') 20 | 21 | # we don't care about South migrations 22 | linter.config.black_list += ('migrations', 'south_migrations') 23 | 24 | 25 | def register(linter): 26 | """ 27 | Registering additional checkers. 28 | """ 29 | # add all of the checkers 30 | register_checkers(linter) 31 | 32 | # register any checking fiddlers 33 | try: 34 | # pylint: disable=import-outside-toplevel 35 | from pylint_django.augmentations import apply_augmentations 36 | apply_augmentations(linter) 37 | except ImportError: 38 | # probably trying to execute pylint_django when Django isn't installed 39 | # in this case the django-not-installed checker will kick-in 40 | pass 41 | 42 | if not compat.LOAD_CONFIGURATION_SUPPORTED: 43 | load_configuration(linter) 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 4 | - 3.6 5 | - 3.7 6 | - 3.8 7 | env: 8 | # note: latest versions first b/c the top-most is included in new 9 | # build stages if not specified 10 | - DJANGO=3.0 11 | - DJANGO=2.2 12 | stages: 13 | - django_not_installed 14 | - django_is_installed 15 | - test 16 | - build_and_package_sanity 17 | matrix: 18 | include: 19 | - { stage: django_not_installed, python: 3.6, env: TOXENV=django_not_installed } 20 | - { stage: django_is_installed, python: 3.6, env: TOXENV=django_is_installed } 21 | - { stage: test, python: 3.8, env: DJANGO=3.1 } 22 | - { stage: test, python: 3.6, env: DJANGO=3.0 } 23 | - { stage: test, python: 3.6, env: DJANGO=2.0 } 24 | - { stage: test, python: 3.6, env: DJANGO=1.11 } 25 | - { stage: test, python: 3.6, env: DJANGO=master } 26 | - { stage: test, python: 3.6, env: TOXENV=flake8 } 27 | - { stage: test, python: 3.6, env: TOXENV=pylint } 28 | - { stage: test, python: 3.6, env: TOXENV=readme } 29 | - { stage: build_and_package_sanity, python: 3.6, env: SANITY_CHECK=1 } 30 | 31 | before_install: 32 | - git clone --depth 1 https://github.com/PyCQA/pylint.git --branch 2.4 --single-branch ~/pylint 33 | 34 | install: 35 | - pip install tox-travis 36 | - pip install -e .[for_tests] 37 | script: 38 | - | 39 | if [ -z "$SANITY_CHECK" ]; then 40 | tox 41 | else 42 | ./scripts/build.sh 43 | fi 44 | 45 | after_success: 46 | - | 47 | if [ -z "$SANITY_CHECK" ] && [ -z "$TOXENV" ]; then 48 | pip install coveralls 49 | coveralls 50 | fi 51 | 52 | notifications: 53 | email: 54 | on_failure: change 55 | on_success: never 56 | -------------------------------------------------------------------------------- /pylint_django/tests/input/external_factory_boy_noerror.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test to validate that pylint_django doesn't produce 3 | Instance of 'SubFactory' has no 'pk' member (no-member) warnings 4 | """ 5 | # pylint: disable=attribute-defined-outside-init, missing-docstring, too-few-public-methods 6 | import factory 7 | 8 | from django import test 9 | from django.db import models 10 | 11 | 12 | class Author(models.Model): 13 | name = models.CharField() 14 | 15 | 16 | class Book(models.Model): 17 | title = models.CharField() 18 | author = models.ForeignKey(Author, related_name='books', on_delete=models.CASCADE) 19 | 20 | 21 | class AuthorFactory(factory.django.DjangoModelFactory): 22 | class Meta: 23 | model = 'Author' 24 | 25 | name = factory.Sequence(lambda n: 'Author %d' % n) 26 | 27 | 28 | class BookFactory(factory.django.DjangoModelFactory): 29 | class Meta: 30 | model = 'Book' 31 | 32 | title = factory.Sequence(lambda n: 'Book %d' % n) 33 | author = factory.SubFactory(AuthorFactory) 34 | reviewer = factory.LazyFunction(Author.objects.first()) 35 | 36 | 37 | class BookTestCase(test.LiveServerTestCase): 38 | serialized_rollback = True 39 | 40 | def _fixture_setup(self): 41 | super()._fixture_setup() 42 | self.book = BookFactory() 43 | _author = AuthorFactory() 44 | _first_book = _author.books.first() 45 | self.assertIsNotNone(_first_book) 46 | 47 | def test_author_is_not_none(self): 48 | self.assertGreater(self.book.pk, 0) 49 | self.assertGreater(self.book.author.pk, 0) 50 | 51 | self.assertIsNotNone(self.book.title) 52 | self.assertIsNotNone(self.book.author.name) 53 | 54 | def test_reviewer_is_not_none(self): 55 | self.assertGreater(self.book.reviewer.pk, 0) 56 | -------------------------------------------------------------------------------- /pylint_django/checkers/forms.py: -------------------------------------------------------------------------------- 1 | """Models.""" 2 | from astroid.nodes import Assign, AssignName, ClassDef 3 | 4 | from pylint.interfaces import IAstroidChecker 5 | from pylint.checkers.utils import check_messages 6 | from pylint.checkers import BaseChecker 7 | 8 | from pylint_django.__pkginfo__ import BASE_ID 9 | from pylint_django.utils import node_is_subclass 10 | 11 | 12 | def _get_child_meta(node): 13 | for child in node.get_children(): 14 | if isinstance(child, ClassDef) and child.name == 'Meta': 15 | return child 16 | return None 17 | 18 | 19 | class FormChecker(BaseChecker): 20 | """Django model checker.""" 21 | __implements__ = IAstroidChecker 22 | 23 | name = 'django-form-checker' 24 | msgs = { 25 | 'W%d04' % BASE_ID: ("Use explicit fields instead of exclude in ModelForm", 26 | 'modelform-uses-exclude', 27 | "Prevents accidentally allowing users to set fields, " 28 | "especially when adding new fields to a Model") 29 | } 30 | 31 | @check_messages('modelform-uses-exclude') 32 | def visit_classdef(self, node): 33 | """Class visitor.""" 34 | if not node_is_subclass(node, 'django.forms.models.ModelForm', '.ModelForm'): 35 | # we only care about forms 36 | return 37 | 38 | meta = _get_child_meta(node) 39 | 40 | if not meta: 41 | return 42 | 43 | for child in meta.get_children(): 44 | if (not isinstance(child, Assign) 45 | or not isinstance(child.targets[0], AssignName)): 46 | continue 47 | 48 | if child.targets[0].name == 'exclude': 49 | self.add_message('W%s04' % BASE_ID, node=child) 50 | break 51 | -------------------------------------------------------------------------------- /pylint_django/transforms/__init__.py: -------------------------------------------------------------------------------- 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 | """ 11 | import os 12 | import re 13 | 14 | import astroid 15 | 16 | from pylint_django.transforms import fields 17 | 18 | fields.add_transforms(astroid.MANAGER) 19 | 20 | 21 | def _add_transform(package_name): 22 | def fake_module_builder(): 23 | """ 24 | Build a fake module to use within transformations. 25 | @package_name is a parameter from the outher scope b/c according to 26 | the docs this can't receive any parameters. 27 | http://pylint.pycqa.org/projects/astroid/en/latest/extending.html?highlight=MANAGER#module-extender-transforms 28 | """ 29 | transforms_dir = os.path.join(os.path.dirname(__file__), 'transforms') 30 | fake_module_path = os.path.join(transforms_dir, '%s.py' % re.sub(r'\.', '_', package_name)) 31 | 32 | with open(fake_module_path) as modulefile: 33 | fake_module = modulefile.read() 34 | 35 | return astroid.builder.AstroidBuilder(astroid.MANAGER).string_build(fake_module) 36 | 37 | astroid.register_module_extender(astroid.MANAGER, package_name, fake_module_builder) 38 | 39 | 40 | _add_transform('django.utils.translation') 41 | # register transform for FileField/ImageField, see #60 42 | _add_transform('django.db.models.fields.files') 43 | -------------------------------------------------------------------------------- /pylint_django/tests/input/external_psycopg2_noerror_postgres_fields.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain Postgres model fields. 3 | """ 4 | # pylint: disable=C0111,W5101 5 | from __future__ import print_function 6 | 7 | from django.contrib.postgres import fields 8 | from django.db import models 9 | 10 | 11 | class PostgresFieldsModel(models.Model): 12 | arrayfield = fields.ArrayField(models.CharField()) 13 | hstorefield = fields.HStoreField() 14 | jsonfield = fields.JSONField() 15 | rangefield = fields.RangeField() 16 | integerrangefield = fields.IntegerRangeField() 17 | bigintegerrangefield = fields.BigIntegerRangeField() 18 | floatrangefield = fields.FloatRangeField() 19 | datetimerangefield = fields.DateTimeRangeField() 20 | daterangefield = fields.DateRangeField() 21 | 22 | def arrayfield_tests(self): 23 | sorted_array = self.arrayfield.sort() 24 | print(sorted_array) 25 | 26 | def dictfield_tests(self): 27 | print(self.hstorefield.keys()) 28 | print(self.hstorefield.values()) 29 | print(self.hstorefield.update({'foo': 'bar'})) 30 | 31 | print(self.jsonfield.keys()) 32 | print(self.jsonfield.values()) 33 | print(self.jsonfield.update({'foo': 'bar'})) 34 | 35 | def rangefield_tests(self): 36 | print(self.rangefield.lower) 37 | print(self.rangefield.upper) 38 | 39 | print(self.integerrangefield.lower) 40 | print(self.integerrangefield.upper) 41 | 42 | print(self.bigintegerrangefield.lower) 43 | print(self.bigintegerrangefield.upper) 44 | 45 | print(self.floatrangefield.lower) 46 | print(self.floatrangefield.upper) 47 | 48 | print(self.datetimerangefield.lower) 49 | print(self.datetimerangefield.upper) 50 | 51 | print(self.daterangefield.lower) 52 | print(self.daterangefield.upper) 53 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_manytomanyfield.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about various 3 | methods on many-to-many relationships 4 | """ 5 | # pylint: disable=missing-docstring 6 | from django.db import models 7 | from django.contrib.auth.models import AbstractUser, Permission 8 | 9 | 10 | class Book(models.Model): 11 | name = models.CharField(max_length=100) 12 | good = models.BooleanField(default=False) 13 | 14 | 15 | class Author(models.Model): 16 | name = models.CharField(max_length=100) 17 | wrote = models.ManyToManyField(Book, verbose_name="Book", 18 | related_name='books') 19 | 20 | def get_good_books(self): 21 | return self.wrote.filter(good=True) 22 | 23 | def is_author_of(self, book): 24 | return book in list(self.wrote.all()) 25 | 26 | def wrote_how_many(self): 27 | return self.wrote.count() 28 | 29 | 30 | # Custom permissions for CustomUser 31 | USER_PERMS = ['change_customuser', 'add_customuser'] 32 | 33 | 34 | class CustomUser(AbstractUser): # pylint: disable=model-no-explicit-unicode 35 | class Meta: 36 | verbose_name = 'CustomUser' 37 | verbose_name_plural = 'CustomUsers' 38 | app_label = "users" 39 | 40 | def grant_permissions(self): 41 | ''' Example adding permissions to User ''' 42 | self.user_permissions.clear() 43 | for perm in USER_PERMS: 44 | perm = Permission.objects.get(codename=perm) 45 | self.user_permissions.add(perm) 46 | return self.user_permissions 47 | 48 | def add_permission(self, permission): 49 | self.user_permissions.add(permission) 50 | 51 | def remove_permission(self, permission): 52 | self.user_permissions.remove(permission) 53 | 54 | def set_permissions(self, permissions): 55 | self.user_permissions.set(permissions) 56 | 57 | def save(self, *args, **kwargs): 58 | ''' Saving while granting new permissions ''' 59 | self.is_staff = True 60 | super().save() 61 | self.grant_permissions() 62 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # This configuration file is for Tox. https://tox.readthedocs.io/ 2 | # It houses configuration sections for common Python tools, too. 3 | 4 | [tox] 5 | envlist = 6 | django_not_installed 7 | django_is_installed 8 | flake8 9 | pylint 10 | readme 11 | py{36}-django{111,20,-master} 12 | py{36,37}-django22 13 | py{36,37,38}-django{30,31} 14 | 15 | [testenv] 16 | commands = 17 | django_not_installed: bash -c \'pylint --load-plugins=pylint_django setup.py | grep django-not-available\' 18 | django_is_installed: pylint --load-plugins=pylint_django --disable=E5110 setup.py 19 | flake8: flake8 20 | pylint: pylint --rcfile=tox.ini -d missing-docstring,too-many-branches,too-many-return-statements,too-many-ancestors,fixme --ignore=tests pylint_django setup 21 | readme: bash -c \'python setup.py -q sdist && twine check dist/*\' 22 | py{36}-django{111,20,-master}: coverage run pylint_django/tests/test_func.py -v 23 | py{36,37}-django22: coverage run pylint_django/tests/test_func.py -v 24 | py{36,37,38}-django{30,31}: coverage run pylint_django/tests/test_func.py -v 25 | clean: find . -type f -name '*.pyc' -delete 26 | clean: find . -type d -name __pycache__ -delete 27 | clean: rm -rf build/ .cache/ dist/ .eggs/ pylint_django.egg-info/ .tox/ 28 | deps = 29 | django_is_installed: Django 30 | flake8: flake8 31 | pylint: pylint 32 | pylint: Django 33 | readme: twine 34 | django111: Django>=1.11,<2.0 35 | django20: Django>=2.0,<2.1 36 | django21: Django>=2.1,<2.2 37 | django22: Django>=2.2,<3.0 38 | django30: Django>=3.0,<3.1 39 | django31: Django>=3.1,<3.2 40 | django-master: Django 41 | django-master: git+https://github.com/pycqa/astroid@master 42 | django-master: git+https://github.com/pycqa/pylint@master 43 | setenv = 44 | PIP_DISABLE_PIP_VERSION_CHECK = 1 45 | PYTHONPATH = . 46 | whitelist_externals = 47 | django_not_installed: bash 48 | readme: bash 49 | py{36}-django{111,20,-master}: coverage 50 | py{36,37}-django22: coverage 51 | py{36,37,38}-django{30,31}: coverage 52 | clean: find 53 | clean: rm 54 | 55 | [travis:env] 56 | DJANGO = 57 | 1.11: django111 58 | 2.0: django20 59 | 2.1: django21 60 | 2.2: django22 61 | 3.0: django30 62 | 3.1: django31 63 | master: django-master 64 | 65 | [flake8] 66 | max-line-length = 120 67 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_foreignkeys.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about various 3 | methods on Django model fields. 4 | """ 5 | # pylint: disable=missing-docstring,wrong-import-position 6 | from django.db import models 7 | from django.db.models import ForeignKey, OneToOneField 8 | 9 | 10 | class Genre(models.Model): 11 | name = models.CharField(max_length=100) 12 | 13 | 14 | class Author(models.Model): 15 | author_name = models.CharField(max_length=100) 16 | 17 | 18 | class ISBN(models.Model): 19 | value = models.CharField(max_length=100) 20 | 21 | 22 | class Book(models.Model): 23 | book_name = models.CharField(max_length=100) 24 | # Check this works with and without `to` keyword 25 | author = models.ForeignKey(to='Author', on_delete=models.CASCADE) 26 | isbn = models.OneToOneField(to=ISBN, on_delete=models.CASCADE) 27 | genre = models.ForeignKey(Genre, on_delete=models.CASCADE) 28 | 29 | def get_isbn(self): 30 | return self.isbn.value 31 | 32 | def get_author_name(self): 33 | return self.author.author_name 34 | 35 | 36 | class Fruit(models.Model): 37 | fruit_name = models.CharField(max_length=20) 38 | 39 | 40 | class Seed(models.Model): 41 | fruit = ForeignKey(to=Fruit, on_delete=models.CASCADE) 42 | 43 | def get_fruit_name(self): 44 | return self.fruit.fruit_name 45 | 46 | 47 | class User(models.Model): 48 | username = models.CharField(max_length=32) 49 | 50 | 51 | class UserProfile(models.Model): 52 | user = OneToOneField(User, on_delete=models.CASCADE) 53 | 54 | def get_username(self): 55 | return self.user.username 56 | 57 | 58 | class Human(models.Model): 59 | child = ForeignKey('self', on_delete=models.SET_NULL, null=True) 60 | parent = ForeignKey(to='self', on_delete=models.SET_NULL, null=True) 61 | 62 | def get_grandchild(self): 63 | return self.child.child 64 | 65 | def get_grandparent(self): 66 | return self.parent.parent 67 | 68 | 69 | class UserPreferences(models.Model): 70 | """ 71 | Used for testing FK which refers to another model by 72 | string, not model class, see 73 | https://github.com/PyCQA/pylint-django/issues/35 74 | """ 75 | user = ForeignKey('User', on_delete=models.CASCADE) 76 | 77 | 78 | class UserAddress(models.Model): 79 | user = OneToOneField(to='User', on_delete=models.CASCADE) 80 | line_1 = models.CharField(max_length=100) 81 | line_2 = models.CharField(max_length=100) 82 | city = models.CharField(max_length=100) 83 | postal_code = models.CharField(max_length=100) 84 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_form_fields.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about various 3 | methods on Django form forms. 4 | """ 5 | # pylint: disable=missing-docstring,R0904 6 | from __future__ import print_function 7 | from datetime import datetime, date 8 | from django import forms 9 | from django.contrib.auth.forms import UserCreationForm 10 | 11 | 12 | class ManyFieldsForm(forms.Form): 13 | 14 | booleanfield = forms.BooleanField() 15 | charfield = forms.CharField(max_length=40, null=True) 16 | datetimefield = forms.DateTimeField(auto_now_add=True) 17 | datefield = forms.DateField(auto_now_add=True) 18 | decimalfield = forms.DecimalField(max_digits=5, decimal_places=2) 19 | durationfield = forms.DurationField() 20 | emailfield = forms.EmailField() 21 | filefield = forms.FileField(name='test_file', upload_to='test') 22 | filepathfield = forms.FilePathField(path='/some/path') 23 | floatfield = forms.FloatField() 24 | genericipaddressfield = forms.GenericIPAddressField() 25 | imagefield = forms.ImageField(name='test_image', upload_to='test') 26 | intfield = forms.IntegerField(null=True) 27 | nullbooleanfield = forms.NullBooleanField() 28 | slugfield = forms.SlugField() 29 | timefield = forms.TimeField() 30 | urlfield = forms.URLField() 31 | 32 | def boolean_field_tests(self): 33 | print(self.booleanfield | True) 34 | print(self.nullbooleanfield | True) 35 | 36 | def string_field_tests(self): 37 | print(self.charfield.strip()) 38 | print(self.charfield.upper()) 39 | print(self.charfield.replace('x', 'y')) 40 | 41 | print(self.filepathfield.strip()) 42 | print(self.filepathfield.upper()) 43 | print(self.filepathfield.replace('x', 'y')) 44 | 45 | print(self.emailfield.strip()) 46 | print(self.emailfield.upper()) 47 | print(self.emailfield.replace('x', 'y')) 48 | 49 | def datetimefield_tests(self): 50 | now = datetime.now() 51 | print(now - self.datetimefield) 52 | print(self.datetimefield.ctime()) 53 | 54 | def datefield_tests(self): 55 | now = date.today() 56 | print(now - self.datefield) 57 | print(self.datefield.isoformat()) 58 | 59 | def decimalfield_tests(self): 60 | print(self.decimalfield.adjusted()) 61 | 62 | def durationfield_tests(self): 63 | now = datetime.now() 64 | print(now - self.durationfield) 65 | print(self.durationfield.total_seconds()) 66 | 67 | def filefield_tests(self): 68 | print(self.filefield) 69 | print(self.imagefield) 70 | 71 | def numberfield_tests(self): 72 | print(self.intfield + 5) 73 | 74 | 75 | _ = UserCreationForm.declared_fields 76 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Build packages for distribution on PyPI 4 | # and execute some sanity scripts on them 5 | # 6 | # note: must be executed from the root directory of the project 7 | 8 | # first clean up the local environment 9 | echo "..... Clean up first" 10 | find . -type f -name '*.pyc' -delete 11 | find . -type d -name __pycache__ -delete 12 | find . -type d -name '*.egg-info' | xargs rm -rf 13 | rm -rf build/ .cache/ dist/ .eggs/ .tox/ 14 | 15 | # check rst formatting of README/CHANGELOG before building the package 16 | echo "..... Check rst formatting for PyPI" 17 | tox -e readme 18 | 19 | # then build the packages 20 | echo "..... Building PyPI packages" 21 | set -e 22 | python setup.py sdist >/dev/null 23 | python setup.py bdist_wheel >/dev/null 24 | set +e 25 | 26 | # then run some sanity tests 27 | echo "..... Searching for .pyc files inside the built packages" 28 | matched_files=`tar -tvf dist/*.tar.gz | grep -c "\.pyc"` 29 | if [ "$matched_files" -gt "0" ]; then 30 | echo "ERROR: .pyc files found in .tar.gz package" 31 | exit 1 32 | fi 33 | matched_files=`unzip -t dist/*.whl | grep -c "\.pyc"` 34 | if [ "$matched_files" -gt "0" ]; then 35 | echo "ERROR: .pyc files found in wheel package" 36 | exit 1 37 | fi 38 | 39 | echo "..... Trying to verify that all source files are present" 40 | # remove pylint_django/*.egg-info/ generated during build 41 | find . -type d -name '*.egg-info' | xargs rm -rf 42 | 43 | source_files=`find ./pylint_django/ -type f | sed 's|./||'` 44 | 45 | # verify for .tar.gz package 46 | package_files=`tar -tvf dist/*.tar.gz` 47 | for src_file in $source_files; do 48 | echo "$package_files" | grep $src_file >/dev/null 49 | if [ "$?" -ne 0 ]; then 50 | echo "ERROR: $src_file not found inside tar.gz package" 51 | exit 1 52 | fi 53 | done 54 | 55 | # verify for wheel package 56 | package_files=`unzip -t dist/*.whl` 57 | for src_file in $source_files; do 58 | echo "$package_files" | grep $src_file >/dev/null 59 | if [ "$?" -ne 0 ]; then 60 | echo "ERROR: $src_file not found inside wheel package" 61 | exit 1 62 | fi 63 | done 64 | 65 | # exit on error from now on 66 | set -e 67 | 68 | echo "..... Trying to install the new tarball inside a virtualenv" 69 | # note: installs with the optional dependency to verify this is still working 70 | virtualenv .venv/test-tarball 71 | source .venv/test-tarball/bin/activate 72 | pip install -f dist/ pylint_django[with_django] 73 | pip freeze | grep Django 74 | deactivate 75 | rm -rf .venv/ 76 | 77 | echo "..... Trying to install the new wheel inside a virtualenv" 78 | virtualenv .venv/test-wheel 79 | source .venv/test-wheel/bin/activate 80 | pip install pylint-plugin-utils # because it does not provide a wheel package 81 | pip install --only-binary :all: -f dist/ pylint_django 82 | deactivate 83 | rm -rf .venv/ 84 | 85 | echo "..... PASS" 86 | -------------------------------------------------------------------------------- /pylint_django/checkers/json_response.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Alexander Todorov 2 | 3 | # Licensed under the GPL 2.0: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html 4 | # For details: https://github.com/PyCQA/pylint-django/blob/master/LICENSE 5 | """ 6 | Various suggestions about JSON http responses 7 | """ 8 | 9 | import astroid 10 | 11 | from pylint import interfaces, checkers 12 | from pylint.checkers import utils 13 | from pylint_django.__pkginfo__ import BASE_ID 14 | 15 | 16 | class JsonResponseChecker(checkers.BaseChecker): 17 | """ 18 | Looks for some common patterns when returning http responses containing 19 | JSON data! 20 | """ 21 | 22 | __implements__ = (interfaces.IAstroidChecker,) 23 | 24 | # configuration section name 25 | name = 'json-response-checker' 26 | msgs = {'R%s01' % BASE_ID: ("Instead of HttpResponse(json.dumps(data)) use JsonResponse(data)", 27 | 'http-response-with-json-dumps', 28 | 'Used when json.dumps() is used as an argument to HttpResponse().'), 29 | 'R%s02' % BASE_ID: ("Instead of HttpResponse(content_type='application/json') use JsonResponse()", 30 | 'http-response-with-content-type-json', 31 | 'Used when HttpResponse() is returning application/json.'), 32 | 'R%s03' % BASE_ID: ("Redundant content_type parameter for JsonResponse()", 33 | 'redundant-content-type-for-json-response', 34 | 'Used when JsonResponse() contains content_type parameter. ' 35 | 'This is either redundant or the content_type is not JSON ' 36 | 'which is probably an error.')} 37 | 38 | @utils.check_messages('http-response-with-json-dumps', 39 | 'http-response-with-content-type-json', 40 | 'redundant-content-type-for-json-response') 41 | def visit_call(self, node): 42 | if (node.func.as_string().endswith('HttpResponse') and node.args 43 | and isinstance(node.args[0], astroid.Call) 44 | and node.args[0].func.as_string() == 'json.dumps'): 45 | self.add_message('http-response-with-json-dumps', node=node) 46 | 47 | if node.func.as_string().endswith('HttpResponse') and node.keywords: 48 | for keyword in node.keywords: 49 | if (keyword.arg == 'content_type' 50 | and keyword.value.as_string().lower().find('application/json') > -1): 51 | self.add_message('http-response-with-content-type-json', node=node) 52 | break 53 | 54 | if node.func.as_string().endswith('JsonResponse') and node.keywords: 55 | for keyword in node.keywords: 56 | if keyword.arg == 'content_type': 57 | self.add_message('redundant-content-type-for-json-response', node=node) 58 | break 59 | -------------------------------------------------------------------------------- /pylint_django/tests/input/func_noerror_model_fields.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checks that Pylint does not complain about various 3 | methods on Django model fields. 4 | """ 5 | # pylint: disable=missing-docstring 6 | from __future__ import print_function 7 | from datetime import datetime, date 8 | from decimal import Decimal 9 | from django.db import models 10 | 11 | 12 | class LotsOfFieldsModel(models.Model): 13 | 14 | bigintegerfield = models.BigIntegerField() 15 | booleanfield = models.BooleanField(default=True) 16 | charfield = models.CharField(max_length=40, null=True) 17 | commaseparatedintegerfield = models.CommaSeparatedIntegerField() 18 | datetimefield = models.DateTimeField(auto_now_add=True) 19 | datefield = models.DateField(auto_now_add=True) 20 | decimalfield = models.DecimalField(max_digits=5, decimal_places=2) 21 | durationfield = models.DurationField() 22 | emailfield = models.EmailField() 23 | filefield = models.FileField(name='test_file', upload_to='test') 24 | filepathfield = models.FilePathField() 25 | floatfield = models.FloatField() 26 | genericipaddressfield = models.GenericIPAddressField() 27 | imagefield = models.ImageField(name='test_image', upload_to='test') 28 | ipaddressfield = models.IPAddressField() 29 | intfield = models.IntegerField(null=True) 30 | nullbooleanfield = models.NullBooleanField() 31 | positiveintegerfield = models.PositiveIntegerField() 32 | positivesmallintegerfield = models.PositiveSmallIntegerField() 33 | slugfield = models.SlugField() 34 | smallintegerfield = models.SmallIntegerField() 35 | textfield = models.TextField() 36 | timefield = models.TimeField() 37 | urlfield = models.URLField() 38 | 39 | def __str__(self): 40 | return self.id 41 | 42 | def boolean_field_tests(self): 43 | print(self.booleanfield | True) 44 | print(self.nullbooleanfield | True) 45 | 46 | def string_field_tests(self): 47 | print(self.charfield.strip()) 48 | print(self.charfield.upper()) 49 | print(self.charfield.replace('x', 'y')) 50 | 51 | print(self.filepathfield.strip()) 52 | print(self.filepathfield.upper()) 53 | print(self.filepathfield.replace('x', 'y')) 54 | 55 | print(self.emailfield.strip()) 56 | print(self.emailfield.upper()) 57 | print(self.emailfield.replace('x', 'y')) 58 | 59 | print(self.textfield.strip()) 60 | print(self.textfield.upper()) 61 | print(self.textfield.replace('x', 'y')) 62 | 63 | def datetimefield_tests(self): 64 | now = datetime.now() 65 | print(now - self.datetimefield) 66 | print(self.datetimefield.ctime()) 67 | 68 | def datefield_tests(self): 69 | now = date.today() 70 | print(now - self.datefield) 71 | print(self.datefield.isoformat()) 72 | 73 | def decimalfield_tests(self): 74 | print(self.decimalfield.compare(Decimal('1.4'))) 75 | 76 | def durationfield_tests(self): 77 | now = datetime.now() 78 | print(now - self.durationfield) 79 | print(self.durationfield.total_seconds()) 80 | 81 | def filefield_tests(self): 82 | self.filefield.save('/dev/null', 'TEST') 83 | print(self.filefield.file) 84 | self.imagefield.save('/dev/null', 'TEST') 85 | print(self.imagefield.file) 86 | 87 | def numberfield_tests(self): 88 | print(self.intfield + 5) 89 | print(self.bigintegerfield + 4) 90 | print(self.smallintegerfield + 3) 91 | print(self.positiveintegerfield + 2) 92 | print(self.positivesmallintegerfield + 1) 93 | -------------------------------------------------------------------------------- /pylint_django/transforms/fields.py: -------------------------------------------------------------------------------- 1 | from astroid import ( 2 | MANAGER, scoped_nodes, nodes, inference_tip, 3 | AstroidImportError 4 | ) 5 | 6 | from pylint_django import utils 7 | 8 | 9 | _STR_FIELDS = ('CharField', 'SlugField', 'URLField', 'TextField', 'EmailField', 10 | 'CommaSeparatedIntegerField', 'FilePathField', 'GenericIPAddressField', 11 | 'IPAddressField', 'RegexField', 'SlugField') 12 | _INT_FIELDS = ('IntegerField', 'SmallIntegerField', 'BigIntegerField', 13 | 'PositiveIntegerField', 'PositiveSmallIntegerField') 14 | _BOOL_FIELDS = ('BooleanField', 'NullBooleanField') 15 | _RANGE_FIELDS = ('RangeField', 'IntegerRangeField', 'BigIntegerRangeField', 16 | 'FloatRangeField', 'DateTimeRangeField', 'DateRangeField') 17 | 18 | 19 | def is_model_field(cls): 20 | return cls.qname().startswith('django.db.models.fields') or \ 21 | cls.qname().startswith('django.contrib.postgres.fields') 22 | 23 | 24 | def is_form_field(cls): 25 | return cls.qname().startswith('django.forms.fields') 26 | 27 | 28 | def is_model_or_form_field(cls): 29 | return is_model_field(cls) or is_form_field(cls) 30 | 31 | 32 | def apply_type_shim(cls, _context=None): # noqa 33 | 34 | if cls.name in _STR_FIELDS: 35 | base_nodes = scoped_nodes.builtin_lookup('str') 36 | elif cls.name in _INT_FIELDS: 37 | base_nodes = scoped_nodes.builtin_lookup('int') 38 | elif cls.name in _BOOL_FIELDS: 39 | base_nodes = scoped_nodes.builtin_lookup('bool') 40 | elif cls.name == 'FloatField': 41 | base_nodes = scoped_nodes.builtin_lookup('float') 42 | elif cls.name == 'DecimalField': 43 | try: 44 | base_nodes = MANAGER.ast_from_module_name('_decimal').lookup('Decimal') 45 | except AstroidImportError: 46 | base_nodes = MANAGER.ast_from_module_name('_pydecimal').lookup('Decimal') 47 | elif cls.name in ('SplitDateTimeField', 'DateTimeField'): 48 | base_nodes = MANAGER.ast_from_module_name('datetime').lookup('datetime') 49 | elif cls.name == 'TimeField': 50 | base_nodes = MANAGER.ast_from_module_name('datetime').lookup('time') 51 | elif cls.name == 'DateField': 52 | base_nodes = MANAGER.ast_from_module_name('datetime').lookup('date') 53 | elif cls.name == 'DurationField': 54 | base_nodes = MANAGER.ast_from_module_name('datetime').lookup('timedelta') 55 | elif cls.name == 'UUIDField': 56 | base_nodes = MANAGER.ast_from_module_name('uuid').lookup('UUID') 57 | elif cls.name == 'ManyToManyField': 58 | base_nodes = MANAGER.ast_from_module_name('django.db.models.query').lookup('QuerySet') 59 | elif cls.name in ('ImageField', 'FileField'): 60 | base_nodes = MANAGER.ast_from_module_name('django.core.files.base').lookup('File') 61 | elif cls.name == 'ArrayField': 62 | base_nodes = scoped_nodes.builtin_lookup('list') 63 | elif cls.name in ('HStoreField', 'JSONField'): 64 | base_nodes = scoped_nodes.builtin_lookup('dict') 65 | elif cls.name in _RANGE_FIELDS: 66 | base_nodes = MANAGER.ast_from_module_name('psycopg2._range').lookup('Range') 67 | else: 68 | return iter([cls]) 69 | 70 | # XXX: for some reason, with python3, this particular line triggers a 71 | # check in the StdlibChecker for deprecated methods; one of these nodes 72 | # is an ImportFrom which has no qname() method, causing the checker 73 | # to die... 74 | if utils.PY3: 75 | base_nodes = [n for n in base_nodes[1] if not isinstance(n, nodes.ImportFrom)] 76 | else: 77 | base_nodes = list(base_nodes[1]) 78 | 79 | return iter([cls] + base_nodes) 80 | 81 | 82 | def add_transforms(manager): 83 | manager.register_transform(nodes.ClassDef, inference_tip(apply_type_shim), is_model_or_form_field) 84 | -------------------------------------------------------------------------------- /pylint_django/tests/test_func.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | import sys 4 | import pytest 5 | 6 | import pylint 7 | 8 | 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pylint_django.tests.settings') 10 | 11 | try: 12 | # pylint 2.5: test_functional has been moved to pylint.testutils 13 | from pylint.testutils import FunctionalTestFile, LintModuleTest 14 | 15 | if "test" not in csv.list_dialects(): 16 | class test_dialect(csv.excel): 17 | delimiter = ":" 18 | lineterminator = "\n" 19 | 20 | csv.register_dialect("test", test_dialect) 21 | except (ImportError, AttributeError): 22 | # specify directly the directory containing test_functional.py 23 | test_functional_dir = os.getenv('PYLINT_TEST_FUNCTIONAL_DIR', '') 24 | 25 | # otherwise look for in in ~/pylint/tests - pylint 2.4 26 | # this is the pylint git checkout dir, not the pylint module dir 27 | if not os.path.isdir(test_functional_dir): 28 | test_functional_dir = os.path.join(os.getenv('HOME', '/home/travis'), 'pylint', 'tests') 29 | 30 | # or site-packages/pylint/test/ - pylint before 2.4 31 | if not os.path.isdir(test_functional_dir): 32 | test_functional_dir = os.path.join(os.path.dirname(pylint.__file__), 'test') 33 | 34 | sys.path.append(test_functional_dir) 35 | 36 | from test_functional import FunctionalTestFile, LintModuleTest 37 | 38 | 39 | # alter sys.path again because the tests now live as a subdirectory 40 | # of pylint_django 41 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) 42 | # so we can find migrations 43 | sys.path.append(os.path.join(os.path.dirname(__file__), 'input')) 44 | 45 | 46 | class PylintDjangoLintModuleTest(LintModuleTest): 47 | """ 48 | Only used so that we can load this plugin into the linter! 49 | """ 50 | def __init__(self, test_file): 51 | super(PylintDjangoLintModuleTest, self).__init__(test_file) 52 | self._linter.load_plugin_modules(['pylint_django']) 53 | self._linter.load_plugin_configuration() 54 | 55 | 56 | class PylintDjangoMigrationsTest(PylintDjangoLintModuleTest): 57 | """ 58 | Only used so that we can load 59 | pylint_django.checkers.migrations into the linter! 60 | """ 61 | def __init__(self, test_file): 62 | super().__init__(test_file) 63 | self._linter.load_plugin_modules(['pylint_django.checkers.migrations']) 64 | self._linter.load_plugin_configuration() 65 | 66 | 67 | def get_tests(input_dir='input', sort=False): 68 | def _file_name(test): 69 | return test.base 70 | 71 | HERE = os.path.dirname(os.path.abspath(__file__)) 72 | input_dir = os.path.join(HERE, input_dir) 73 | 74 | suite = [] 75 | for fname in os.listdir(input_dir): 76 | if fname != '__init__.py' and fname.endswith('.py'): 77 | suite.append(FunctionalTestFile(input_dir, fname)) 78 | 79 | # when testing the migrations plugin we need to sort by input file name 80 | # because the plugin reports the errors in close() which appends them to the 81 | # report for the last file in the list 82 | if sort: 83 | suite.sort(key=_file_name) 84 | 85 | return suite 86 | 87 | 88 | TESTS = get_tests() 89 | TESTS.extend(get_tests('input/models')) 90 | TESTS_NAMES = [t.base for t in TESTS] 91 | 92 | 93 | @pytest.mark.parametrize("test_file", TESTS, ids=TESTS_NAMES) 94 | def test_everything(test_file): 95 | # copied from pylint.tests.test_functional.test_functional 96 | LintTest = PylintDjangoLintModuleTest(test_file) 97 | LintTest.setUp() 98 | LintTest._runTest() 99 | 100 | 101 | # NOTE: define tests for the migrations checker! 102 | MIGRATIONS_TESTS = get_tests('input/migrations', True) 103 | MIGRATIONS_TESTS_NAMES = [t.base for t in MIGRATIONS_TESTS] 104 | 105 | 106 | @pytest.mark.parametrize("test_file", MIGRATIONS_TESTS, ids=MIGRATIONS_TESTS_NAMES) 107 | def test_migrations_plugin(test_file): 108 | LintTest = PylintDjangoMigrationsTest(test_file) 109 | LintTest.setUp() 110 | LintTest._runTest() 111 | 112 | 113 | if __name__ == '__main__': 114 | sys.exit(pytest.main(sys.argv)) 115 | -------------------------------------------------------------------------------- /pylint_django/checkers/models.py: -------------------------------------------------------------------------------- 1 | """Models.""" 2 | from astroid import Const 3 | from astroid.nodes import Assign 4 | from astroid.nodes import ClassDef, FunctionDef, AssignName 5 | 6 | from pylint.interfaces import IAstroidChecker 7 | from pylint.checkers.utils import check_messages 8 | from pylint.checkers import BaseChecker 9 | 10 | from pylint_django.__pkginfo__ import BASE_ID 11 | from pylint_django.utils import node_is_subclass, PY3 12 | 13 | 14 | MESSAGES = { 15 | 'E%d01' % BASE_ID: ("__unicode__ on a model must be callable (%s)", 16 | 'model-unicode-not-callable', 17 | "Django models require a callable __unicode__ method"), 18 | 'W%d01' % BASE_ID: ("No __unicode__ method on model (%s)", 19 | 'model-missing-unicode', 20 | "Django models should implement a __unicode__ method for string representation"), 21 | 'W%d02' % BASE_ID: ("Found __unicode__ method on model (%s). Python3 uses __str__.", 22 | 'model-has-unicode', 23 | "Django models should not implement a __unicode__ " 24 | "method for string representation when using Python3"), 25 | 'W%d03' % BASE_ID: ("Model does not explicitly define __unicode__ (%s)", 26 | 'model-no-explicit-unicode', 27 | "Django models should implement a __unicode__ method for string representation. " 28 | "A parent class of this model does, but ideally all models should be explicit.") 29 | } 30 | 31 | 32 | def _is_meta_with_abstract(node): 33 | if isinstance(node, ClassDef) and node.name == 'Meta': 34 | for meta_child in node.get_children(): 35 | if not isinstance(meta_child, Assign): 36 | continue 37 | if not meta_child.targets[0].name == 'abstract': 38 | continue 39 | if not isinstance(meta_child.value, Const): 40 | continue 41 | # TODO: handle tuple assignment? 42 | # eg: 43 | # abstract, something_else = True, 1 44 | if meta_child.value.value: 45 | # this class is abstract 46 | return True 47 | return False 48 | 49 | 50 | def _has_python_2_unicode_compatible_decorator(node): 51 | if node.decorators is None: 52 | return False 53 | 54 | for decorator in node.decorators.nodes: 55 | if getattr(decorator, 'name', None) == 'python_2_unicode_compatible': 56 | return True 57 | 58 | return False 59 | 60 | 61 | def _is_unicode_or_str_in_python_2_compatibility(method): 62 | if method.name == '__unicode__': 63 | return True 64 | 65 | if method.name == '__str__' and _has_python_2_unicode_compatible_decorator(method.parent): 66 | return True 67 | 68 | return False 69 | 70 | 71 | class ModelChecker(BaseChecker): 72 | """Django model checker.""" 73 | __implements__ = IAstroidChecker 74 | 75 | name = 'django-model-checker' 76 | msgs = MESSAGES 77 | 78 | @check_messages('model-missing-unicode') 79 | def visit_classdef(self, node): 80 | """Class visitor.""" 81 | if not node_is_subclass(node, 'django.db.models.base.Model', '.Model'): 82 | # we only care about models 83 | return 84 | 85 | for child in node.get_children(): 86 | if _is_meta_with_abstract(child): 87 | return 88 | 89 | if isinstance(child, Assign): 90 | grandchildren = list(child.get_children()) 91 | 92 | if not isinstance(grandchildren[0], AssignName): 93 | continue 94 | 95 | name = grandchildren[0].name 96 | if name != '__unicode__': 97 | continue 98 | 99 | grandchild = grandchildren[1] 100 | assigned = grandchild.inferred()[0] 101 | 102 | if assigned.callable(): 103 | return 104 | 105 | self.add_message('E%s01' % BASE_ID, args=node.name, node=node) 106 | return 107 | 108 | if isinstance(child, FunctionDef) and child.name == '__unicode__': 109 | if PY3: 110 | self.add_message('W%s02' % BASE_ID, args=node.name, node=node) 111 | return 112 | 113 | # if we get here, then we have no __unicode__ method directly on the class itself 114 | 115 | # a different warning is emitted if a parent declares __unicode__ 116 | for method in node.methods(): 117 | if method.parent != node and _is_unicode_or_str_in_python_2_compatibility(method): 118 | # this happens if a parent declares the unicode method but 119 | # this node does not 120 | self.add_message('W%s03' % BASE_ID, args=node.name, node=node) 121 | return 122 | 123 | # if the Django compatibility decorator is used then we don't emit a warning 124 | # see https://github.com/PyCQA/pylint-django/issues/10 125 | if _has_python_2_unicode_compatible_decorator(node): 126 | return 127 | 128 | if PY3: 129 | return 130 | 131 | self.add_message('W%s01' % BASE_ID, args=node.name, node=node) 132 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pylint-django 2 | ============= 3 | 4 | .. image:: https://travis-ci.org/PyCQA/pylint-django.svg?branch=master 5 | :target: https://travis-ci.org/PyCQA/pylint-django 6 | 7 | .. image:: https://landscape.io/github/landscapeio/pylint-django/master/landscape.png 8 | :target: https://landscape.io/github/landscapeio/pylint-django 9 | 10 | .. image:: https://coveralls.io/repos/PyCQA/pylint-django/badge.svg 11 | :target: https://coveralls.io/r/PyCQA/pylint-django 12 | 13 | .. image:: https://img.shields.io/pypi/v/pylint-django.svg 14 | :target: https://pypi.python.org/pypi/pylint-django 15 | 16 | 17 | About 18 | ----- 19 | 20 | ``pylint-django`` is a `Pylint `__ plugin for improving code 21 | analysis when analysing code using Django. It is also used by the 22 | `Prospector `__ tool. 23 | 24 | 25 | Installation 26 | ------------ 27 | 28 | To install:: 29 | 30 | pip install pylint-django 31 | 32 | 33 | **WARNING:** ``pylint-django`` will not install ``Django`` by default because 34 | this causes more trouble than good, 35 | `see discussion `__. If you wish 36 | to automatically install the latest version of ``Django`` then:: 37 | 38 | pip install pylint-django[with_django] 39 | 40 | otherwise sort out your testing environment and please **DO NOT** report issues 41 | about missing Django! 42 | 43 | 44 | Usage 45 | ----- 46 | 47 | 48 | Ensure ``pylint-django`` is installed and on your path. In order to access some 49 | of the internal Django features to improve pylint inspections, you should also 50 | provide a Django settings module appropriate to your project. This can be done 51 | either with an environment variable:: 52 | 53 | DJANGO_SETTINGS_MODULE=your.app.settings pylint --load-plugins pylint_django [..other options..] 54 | 55 | Alternatively, this can be passed in as a commandline flag:: 56 | 57 | pylint --load-plugins pylint_django --django-settings-module=your.app.settings [..other options..] 58 | 59 | If you do not configure Django, default settings will be used but this will not include, for 60 | example, which applications to include in `INSTALLED_APPS` and so the linting and type inference 61 | will be less accurate. It is recommended to specify a settings module. 62 | 63 | Prospector 64 | ---------- 65 | 66 | If you have ``prospector`` installed, then ``pylint-django`` will already be 67 | installed as a dependency, and will be activated automatically if Django is 68 | detected:: 69 | 70 | prospector [..other options..] 71 | 72 | 73 | Features 74 | -------- 75 | 76 | * Prevents warnings about Django-generated attributes such as 77 | ``Model.objects`` or ``Views.request``. 78 | * Prevents warnings when using ``ForeignKey`` attributes ("Instance of 79 | ForeignKey has no member"). 80 | * Fixes pylint's knowledge of the types of Model and Form field attributes 81 | * Validates ``Model.__unicode__`` methods. 82 | * ``Meta`` informational classes on forms and models do not generate errors. 83 | * Flags dangerous use of the exclude attribute in ModelForm.Meta. 84 | * Uses Django's internal machinery to try and resolve models referenced as 85 | strings in ForeignKey fields. That relies on ``django.setup()`` which needs 86 | the appropriate project settings defined! 87 | 88 | 89 | Additional plugins 90 | ------------------ 91 | 92 | ``pylint_django.checkers.migrations`` looks for migrations which: 93 | 94 | - add new model fields and these fields have a default value. According to 95 | `Django docs `_ 96 | this may have performance penalties especially on large tables. The prefered way 97 | is to add a new DB column with ``null=True`` because it will be created instantly 98 | and then possibly populate the table with the desired default values. 99 | Only the last migration from a sub-directory will be examined; 100 | - are ``migrations.RunPython()`` without a reverse callable - these will result in 101 | non reversible data migrations; 102 | 103 | 104 | This plugin is disabled by default! To enable it:: 105 | 106 | pylint --load-plugins pylint_django --load-plugins pylint_django.checkers.migrations 107 | 108 | 109 | Contributing 110 | ------------ 111 | 112 | Please feel free to add your name to the ``CONTRIBUTORS.rst`` file if you want to 113 | be credited when pull requests get merged. You can also add to the 114 | ``CHANGELOG.rst`` file if you wish, although we'll also do that when merging. 115 | 116 | 117 | Tests 118 | ----- 119 | 120 | The structure of the test package follows that from pylint itself. 121 | 122 | It is fairly simple: create a module starting with ``func_`` followed by 123 | a test name, and insert into it some code. The tests will run pylint 124 | against these modules. If the idea is that no messages now occur, then 125 | that is fine, just check to see if it works by running ``scripts/test.sh``. 126 | 127 | Any command line argument passed to ``scripts/test.sh`` will be passed to the internal invocation of ``pytest``. 128 | For example if you want to debug the tests you can execute ``scripts/test.sh --capture=no``. 129 | A specific test case can be run by filtering based on the file name of the test case ``./scripts/test.sh -k 'func_noerror_views'``. 130 | 131 | Ideally, add some pylint error suppression messages to the file to prevent 132 | spurious warnings, since these are all tiny little modules not designed to 133 | do anything so there's no need to be perfect. 134 | 135 | It is possible to make tests with expected error output, for example, if 136 | adding a new message or simply accepting that pylint is supposed to warn. 137 | A ``test_file_name.txt`` file contains a list of expected error messages in the 138 | format 139 | ``error-type:line number:class name or empty:1st line of detailed error text:confidence or empty``. 140 | 141 | 142 | License 143 | ------- 144 | 145 | ``pylint-django`` is available under the GPLv2 license. 146 | -------------------------------------------------------------------------------- /pylint_django/transforms/foreignkey.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | 3 | from astroid import ( 4 | MANAGER, nodes, InferenceError, inference_tip, 5 | UseInferenceDefault 6 | ) 7 | from astroid.nodes import ClassDef, Attribute 8 | 9 | from pylint_django.utils import node_is_subclass 10 | 11 | 12 | def is_foreignkey_in_class(node): 13 | # is this of the form field = models.ForeignKey 14 | if not isinstance(node.parent, nodes.Assign): 15 | return False 16 | if not isinstance(node.parent.parent, ClassDef): 17 | return False 18 | 19 | if isinstance(node.func, Attribute): 20 | attr = node.func.attrname 21 | elif isinstance(node.func, nodes.Name): 22 | attr = node.func.name 23 | else: 24 | return False 25 | return attr in ('OneToOneField', 'ForeignKey') 26 | 27 | 28 | def _get_model_class_defs_from_module(module, model_name, module_name): 29 | class_defs = [] 30 | for module_node in module.lookup(model_name)[1]: 31 | if (isinstance(module_node, nodes.ClassDef) 32 | and node_is_subclass(module_node, 'django.db.models.base.Model')): 33 | class_defs.append(module_node) 34 | elif isinstance(module_node, nodes.ImportFrom): 35 | imported_module = module_node.do_import_module() 36 | class_defs.extend( 37 | _get_model_class_defs_from_module( 38 | imported_module, model_name, module_name 39 | ) 40 | ) 41 | return class_defs 42 | 43 | 44 | def _module_name_from_django_model_resolution(model_name, module_name): 45 | import django # pylint: disable=import-outside-toplevel 46 | django.setup() 47 | from django.apps import apps # pylint: disable=import-outside-toplevel 48 | 49 | app = apps.get_app_config(module_name) 50 | model = app.get_model(model_name) 51 | 52 | return model.__module__ 53 | 54 | 55 | def infer_key_classes(node, context=None): 56 | from django.core.exceptions import ImproperlyConfigured # pylint: disable=import-outside-toplevel 57 | 58 | keyword_args = [] 59 | if node.keywords: 60 | keyword_args = [kw.value for kw in node.keywords if kw.arg == 'to'] 61 | all_args = chain(node.args, keyword_args) 62 | 63 | for arg in all_args: 64 | # typically the class of the foreign key will 65 | # be the first argument, so we'll go from left to right 66 | if isinstance(arg, (nodes.Name, nodes.Attribute)): 67 | try: 68 | key_cls = None 69 | for inferred in arg.infer(context=context): 70 | key_cls = inferred 71 | break 72 | except InferenceError: 73 | continue 74 | else: 75 | if key_cls is not None: 76 | break 77 | elif isinstance(arg, nodes.Const): 78 | try: 79 | # can be 'self' , 'Model' or 'app.Model' 80 | if arg.value == 'self': 81 | module_name = '' 82 | # for relations with `to` first parent be Keyword(arg='to') 83 | # and we need to go deeper in parent tree to get model name 84 | if isinstance(arg.parent, nodes.Keyword) and arg.parent.arg == 'to': 85 | model_name = arg.parent.parent.parent.parent.name 86 | else: 87 | model_name = arg.parent.parent.parent.name 88 | else: 89 | module_name, _, model_name = arg.value.rpartition('.') 90 | except AttributeError: 91 | break 92 | 93 | # when ForeignKey is specified only by class name we assume that 94 | # this class must be found in the current module 95 | if not module_name: 96 | current_module = node.frame() 97 | while not isinstance(current_module, nodes.Module): 98 | current_module = current_module.parent.frame() 99 | 100 | module_name = current_module.name 101 | elif not module_name.endswith('models'): 102 | # otherwise Django allows specifying an app name first, e.g. 103 | # ForeignKey('auth.User') 104 | try: 105 | module_name = _module_name_from_django_model_resolution(model_name, module_name) 106 | except LookupError: 107 | # If Django's model resolution fails we try to convert that to 108 | # 'auth.models', 'User' which works nicely with the `endswith()` 109 | # comparison below 110 | module_name += '.models' 111 | except ImproperlyConfigured as exep: 112 | raise RuntimeError("DJANGO_SETTINGS_MODULE required for resolving ForeignKey " 113 | "string references, see Usage section in README at " 114 | "https://pypi.org/project/pylint-django/!") from exep 115 | 116 | # ensure that module is loaded in astroid_cache, for cases when models is a package 117 | if module_name not in MANAGER.astroid_cache: 118 | MANAGER.ast_from_module_name(module_name) 119 | 120 | # create list from dict_values, because it may be modified in a loop 121 | for module in list(MANAGER.astroid_cache.values()): 122 | # only load model classes from modules which match the module in 123 | # which *we think* they are defined. This will prevent infering 124 | # other models of the same name which are found elsewhere! 125 | if model_name in module.locals and module.name.endswith(module_name): 126 | class_defs = _get_model_class_defs_from_module( 127 | module, model_name, module_name 128 | ) 129 | 130 | if class_defs: 131 | return iter([class_defs[0].instantiate_class()]) 132 | else: 133 | raise UseInferenceDefault 134 | return iter([key_cls.instantiate_class()]) 135 | 136 | 137 | def add_transform(manager): 138 | manager.register_transform(nodes.Call, inference_tip(infer_key_classes), 139 | is_foreignkey_in_class) 140 | -------------------------------------------------------------------------------- /pylint_django/checkers/foreign_key_strings.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import astroid 4 | from pylint.checkers import BaseChecker 5 | from pylint.checkers.utils import check_messages 6 | from pylint.interfaces import IAstroidChecker 7 | 8 | from pylint_django.__pkginfo__ import BASE_ID 9 | from pylint_django.transforms import foreignkey 10 | 11 | 12 | class ForeignKeyStringsChecker(BaseChecker): 13 | """ 14 | Adds transforms to be able to do type inference for model ForeignKeyField 15 | properties which use a string to name the foreign relationship. This uses 16 | Django's model name resolution and this checker wraps the setup to ensure 17 | Django is able to configure itself before attempting to use the lookups. 18 | """ 19 | 20 | _LONG_MESSAGE = """Finding foreign-key relationships from strings in pylint-django requires configuring Django. 21 | This can be done via the DJANGO_SETTINGS_MODULE environment variable or the pylint option django-settings-module, eg: 22 | 23 | `pylint --load-plugins=pylint_django --django-settings-module=myproject.settings` 24 | 25 | . This can also be set as an option in a .pylintrc configuration file. 26 | 27 | Some basic default settings were used, however this will lead to less accurate linting. 28 | Consider passing in an explicit Django configuration file to match your project to improve accuracy.""" 29 | 30 | __implements__ = (IAstroidChecker,) 31 | 32 | name = "Django foreign keys referenced by strings" 33 | 34 | options = ( 35 | ( 36 | "django-settings-module", 37 | { 38 | "default": None, 39 | "type": "string", 40 | "metavar": "", 41 | "help": "A module containing Django settings to be used while linting.", 42 | }, 43 | ), 44 | ) 45 | 46 | msgs = { 47 | "E%s10" 48 | % BASE_ID: ( 49 | "Django was not configured. For more information run" 50 | "pylint --load-plugins=pylint_django --help-msg=django-not-configured", 51 | "django-not-configured", 52 | _LONG_MESSAGE, 53 | ), 54 | "F%s10" 55 | % BASE_ID: ( 56 | "Provided Django settings %s could not be loaded", 57 | "django-settings-module-not-found", 58 | "The provided Django settings module %s was not found on the path", 59 | ), 60 | } 61 | 62 | def __init__(self, *args, **kwargs): 63 | super().__init__(*args, **kwargs) 64 | self._raise_warning = False 65 | 66 | def open(self): 67 | # This is a bit of a hacky workaround. pylint-django does not *require* that 68 | # Django is configured explicitly, and will use some basic defaults in that 69 | # case. However, as this is a WARNING not a FATAL, the error must be raised 70 | # with an AST node - only F and R messages are scope exempt (see 71 | # https://github.com/PyCQA/pylint/blob/master/pylint/constants.py#L24) 72 | 73 | # However, testing to see if Django is configured happens in `open()` 74 | # before any modules are inspected, as Django needs to be configured with 75 | # defaults before the foreignkey checker can work properly. At this point, 76 | # there are no nodes. 77 | 78 | # Therefore, during the initialisation in `open()`, if django was configured 79 | # using defaults by pylint-django, it cannot raise the warning yet and 80 | # must wait until some module is inspected to be able to raise... so that 81 | # state is stashed in this property. 82 | 83 | from django.core.exceptions import ( # pylint: disable=import-outside-toplevel 84 | ImproperlyConfigured, 85 | ) 86 | 87 | try: 88 | import django # pylint: disable=import-outside-toplevel 89 | 90 | django.setup() 91 | from django.apps import apps # noqa pylint: disable=import-outside-toplevel,unused-import 92 | except ImproperlyConfigured: 93 | # this means that Django wasn't able to configure itself using some defaults 94 | # provided (likely in a DJANGO_SETTINGS_MODULE environment variable) 95 | # so see if the user has specified a pylint option 96 | if self.config.django_settings_module is None: 97 | # we will warn the user that they haven't actually configured Django themselves 98 | self._raise_warning = True 99 | # but use django defaults then... 100 | from django.conf import ( # pylint: disable=import-outside-toplevel 101 | settings, 102 | ) 103 | settings.configure() 104 | django.setup() 105 | else: 106 | # see if we can load the provided settings module 107 | try: 108 | from django.conf import ( # pylint: disable=import-outside-toplevel 109 | settings, 110 | Settings, 111 | ) 112 | 113 | settings.configure(Settings(self.config.django_settings_module)) 114 | django.setup() 115 | except ImportError: 116 | # we could not find the provided settings module... 117 | # at least here it is a fatal error so we can just raise this immediately 118 | self.add_message( 119 | "django-settings-module-not-found", 120 | args=self.config.django_settings_module, 121 | ) 122 | # however we'll trundle on with basic settings 123 | from django.conf import ( # pylint: disable=import-outside-toplevel 124 | settings, 125 | ) 126 | 127 | settings.configure() 128 | django.setup() 129 | 130 | # now we can add the trasforms specifc to this checker 131 | foreignkey.add_transform(astroid.MANAGER) 132 | 133 | # TODO: this is a bit messy having so many inline imports but in order to avoid 134 | # duplicating the django_installed checker, it'll do for now. In the future, merging 135 | # those two checkers together might make sense. 136 | 137 | @check_messages("django-not-configured") 138 | def visit_module(self, node): 139 | if self._raise_warning: 140 | # just add it to the first node we see... which isn't nice but not sure what else to do 141 | self.add_message("django-not-configured", node=node) 142 | self._raise_warning = False # only raise it once... 143 | -------------------------------------------------------------------------------- /pylint_django/checkers/migrations.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018, 2020 Alexander Todorov 2 | # Copyright (c) 2020 Bryan Mutai 3 | 4 | # Licensed under the GPL 2.0: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html 5 | # For details: https://github.com/PyCQA/pylint-django/blob/master/LICENSE 6 | """ 7 | Various suggestions around migrations. Disabled by default! Enable with 8 | pylint --load-plugins=pylint_django.checkers.migrations 9 | """ 10 | 11 | import astroid 12 | 13 | from pylint import interfaces 14 | from pylint import checkers 15 | from pylint.checkers import utils 16 | 17 | from pylint_plugin_utils import suppress_message 18 | 19 | from pylint_django.__pkginfo__ import BASE_ID 20 | from pylint_django import compat 21 | from pylint_django.utils import is_migrations_module 22 | 23 | 24 | def _is_addfield_with_default(call): 25 | if not isinstance(call.func, astroid.Attribute): 26 | return False 27 | 28 | if not call.func.attrname == 'AddField': 29 | return False 30 | 31 | for keyword in call.keywords: 32 | # looking for AddField(..., field=XXX(..., default=Y, ...), ...) 33 | if keyword.arg == 'field' and isinstance(keyword.value, astroid.Call): 34 | # loop over XXX's keywords 35 | # NOTE: not checking if XXX is an actual field type because there could 36 | # be many types we're not aware of. Also the migration will probably break 37 | # if XXX doesn't instantiate a field object! 38 | for field_keyword in keyword.value.keywords: 39 | if field_keyword.arg == 'default': 40 | return True 41 | 42 | return False 43 | 44 | 45 | class NewDbFieldWithDefaultChecker(checkers.BaseChecker): 46 | """ 47 | Looks for migrations which add new model fields and these fields have a 48 | default value. According to Django docs this may have performance penalties 49 | especially on large tables: 50 | https://docs.djangoproject.com/en/2.0/topics/migrations/#postgresql 51 | 52 | The prefered way is to add a new DB column with null=True because it will 53 | be created instantly and then possibly populate the table with the 54 | desired default values. 55 | """ 56 | 57 | __implements__ = (interfaces.IAstroidChecker,) 58 | 59 | # configuration section name 60 | name = 'new-db-field-with-default' 61 | msgs = {'W%s98' % BASE_ID: ("%s AddField with default value", 62 | 'new-db-field-with-default', 63 | 'Used when Pylint detects migrations adding new ' 64 | 'fields with a default value.')} 65 | 66 | _migration_modules = [] 67 | _possible_offences = {} 68 | 69 | def visit_module(self, node): 70 | if is_migrations_module(node): 71 | self._migration_modules.append(node) 72 | 73 | def visit_call(self, node): 74 | try: 75 | module = node.frame().parent 76 | except: # noqa: E722, pylint: disable=bare-except 77 | return 78 | 79 | if not is_migrations_module(module): 80 | return 81 | 82 | if _is_addfield_with_default(node): 83 | if module not in self._possible_offences: 84 | self._possible_offences[module] = [] 85 | 86 | if node not in self._possible_offences[module]: 87 | self._possible_offences[module].append(node) 88 | 89 | @utils.check_messages('new-db-field-with-default') 90 | def close(self): 91 | def _path(node): 92 | return node.path 93 | 94 | # sort all migrations by name in reverse order b/c 95 | # we need only the latest ones 96 | self._migration_modules.sort(key=_path, reverse=True) 97 | 98 | # filter out the last migration modules under each distinct 99 | # migrations directory, iow leave only the latest migrations 100 | # for each application 101 | last_name_space = '' 102 | latest_migrations = [] 103 | for module in self._migration_modules: 104 | name_space = module.path[0].split('migrations')[0] 105 | if name_space != last_name_space: 106 | last_name_space = name_space 107 | latest_migrations.append(module) 108 | 109 | for module, nodes in self._possible_offences.items(): 110 | if module in latest_migrations: 111 | for node in nodes: 112 | self.add_message('new-db-field-with-default', args=module.name, node=node) 113 | 114 | 115 | class MissingBackwardsMigrationChecker(checkers.BaseChecker): 116 | __implements__ = (interfaces.IAstroidChecker,) 117 | 118 | name = 'missing-backwards-migration-callable' 119 | 120 | msgs = {'W%s97' % BASE_ID: ('Always include backwards migration callable', 121 | 'missing-backwards-migration-callable', 122 | 'Always include a backwards/reverse callable counterpart' 123 | ' so that the migration is not irreversable.')} 124 | 125 | @utils.check_messages('missing-backwards-migration-callable') 126 | def visit_call(self, node): 127 | try: 128 | module = node.frame().parent 129 | except: # noqa: E722, pylint: disable=bare-except 130 | return 131 | 132 | if not is_migrations_module(module): 133 | return 134 | 135 | if node.func.as_string().endswith('RunPython') and len(node.args) < 2: 136 | if node.keywords: 137 | for keyword in node.keywords: 138 | if keyword.arg == 'reverse_code': 139 | return 140 | self.add_message('missing-backwards-migration-callable', 141 | node=node) 142 | else: 143 | self.add_message('missing-backwards-migration-callable', 144 | node=node) 145 | 146 | 147 | def is_in_migrations(node): 148 | """ 149 | RunPython() migrations receive forward/backwards functions with signature: 150 | 151 | def func(apps, schema_editor): 152 | 153 | which could be unused. This augmentation will suppress all 'unused-argument' 154 | messages coming from functions in migration modules. 155 | """ 156 | return is_migrations_module(node.parent) 157 | 158 | 159 | def load_configuration(linter): # TODO this is redundant and can be removed 160 | # don't blacklist migrations for this checker 161 | new_black_list = list(linter.config.black_list) 162 | if 'migrations' in new_black_list: 163 | new_black_list.remove('migrations') 164 | linter.config.black_list = new_black_list 165 | 166 | 167 | def register(linter): 168 | """Required method to auto register this checker.""" 169 | linter.register_checker(NewDbFieldWithDefaultChecker(linter)) 170 | linter.register_checker(MissingBackwardsMigrationChecker(linter)) 171 | if not compat.LOAD_CONFIGURATION_SUPPORTED: 172 | load_configuration(linter) 173 | 174 | # apply augmentations for migration checkers 175 | # Unused arguments for migrations 176 | suppress_message(linter, checkers.variables.VariablesChecker.leave_functiondef, 177 | 'unused-argument', is_in_migrations) 178 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | Pylint plugin for improving code analysis for when using Django 294 | Copyright (C) 2013 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Version 2.4.2 (08 Jan 2021) 5 | --------------------------- 6 | 7 | - Moved `Faker` dependencies to test-only `#304 `_ 8 | 9 | 10 | Version 2.4.1 (07 Jan 2021) 11 | --------------------------- 12 | 13 | - Relaxed Faker package versioning requirement for ``Faker`` (Robin Chow) 14 | 15 | 16 | Version 2.4.0 (06 Jan 2021) 17 | --------------------------- 18 | 19 | - Allowed configuration of the Django settings module to be used via a 20 | commandline argument `#286 `_ 21 | - If Django settings are not specified via a commandline argument or environment 22 | variable, an error is issued but defaults are loaded from Django, removing the 23 | fatal error behaviour. `#277 `_ 24 | and `#243 `_ 25 | - Fixed tests to work with pylint>2.6 26 | - Fixed ``AttributeError: 'Subscript' object has no attribute 'name'`` error. 27 | Closes `#284 `_ (@uy-rrodriguez) 28 | - Pin Faker version to Prevent Asteroid Crash (James Pulec) 29 | - Remove Python 3.5 Support (EOL since Sept 2020 and Faker requires 3.6 anyway) 30 | (James Pulec) 31 | - Fixed reverse manager ``update_or_create`` calls (James Pulec) 32 | 33 | 34 | Version 2.3.0 (05 Aug 2020) 35 | --------------------------- 36 | 37 | - Improvement in error message for ``missing-backwards-migration-callable`` 38 | (Bryan Mutai) 39 | - Start testing with Django 3.1 on Python 3.8 40 | - Better error message when Django is not configured. Closes 41 | `#277 `_ 42 | 43 | 44 | Version 2.2.0 (22 Jul 2020) 45 | --------------------------- 46 | 47 | - Rely on Django to resolve string references in ForeignKey fields. Refs 48 | `#243 `_ (Alejandro Angulo) 49 | - Suppress ``unused-argument`` for functions in migration modules. Fix 50 | `#267 `_ 51 | - New checker for hard-coded ``auth.User``. Fix 52 | `#244 `_ 53 | 54 | 55 | Version 2.1.0 (12 Jul 2020) 56 | --------------------------- 57 | 58 | - New checker to warn about missing backwards data migrations (Bryan Mutai) 59 | - Rename ``pylint_django.checkers.db_performance`` to 60 | ``pylint_django.checkers.migrations`` 61 | - Add URL to project Changelog for PyPI (Ville Skyttä) 62 | - Fix failing test suite b/c of missing CSV dialect. Fix 63 | `#268 `_ 64 | (Alejandro Angulo) 65 | 66 | 67 | Version 2.0.15 (14 Apr 2020) 68 | ---------------------------- 69 | 70 | - Do not report error for ``gettext_lazy`` (Antonin MOREL) 71 | 72 | 73 | Version 2.0.14 (25 Feb 2020) 74 | ---------------------------- 75 | 76 | - Add support for Django 3.0 and Python 3.8 (Wayne Lambert) 77 | - Support ASGI. Fix 78 | `#258 `_ (Sander Maijers) 79 | 80 | 81 | Version 2.0.13 (23 Nov 2019), HackBulgaria edition 82 | -------------------------------------------------- 83 | 84 | - Suppress ``too-many-ancestors`` for class-based generic views 85 | - Add ``handler400``, ``handler403``, ``handler404`` to good_names. Fix 86 | `#248 `_ 87 | 88 | 89 | Version 2.0.12 (04 Nov 2019) 90 | ---------------------------- 91 | 92 | - Fix too broad suppression of ``unused-argument`` warnings for functions and 93 | methods where the first argument is named ``request``. Now issues warnings 94 | for the rest of the arguments if they are unused. Fix 95 | `#249 `_ (Pascal Urban) 96 | - Pass arguments of ``scripts/test.sh`` to ``test_func/pytest`` to ease 97 | development (Pascal Urban) 98 | - Document behavior when ForeignKey fields are referenced as strings. Fix 99 | `#241 `_ 100 | 101 | 102 | Version 2.0.11 (10 July 2019) 103 | ----------------------------- 104 | 105 | - Use ``functools.wrap`` to preserve ``leave_module`` info (Mohit Solanki) 106 | 107 | 108 | Version 2.0.10 (07 July 2019), Novi Sad edition 109 | ----------------------------------------------- 110 | 111 | - Suppress ``no-member`` for ``ManyToManyField``. Fix 112 | `#192 `_ and 113 | `#237 `_ (Pierre Chiquet) 114 | 115 | - Fix ``UnboundLocalError`` with ``ForeignKey(to=)``. Fix 116 | `#232 `_ (Sardorbek Imomaliev) 117 | 118 | 119 | Version 2.0.9 (26 April 2019) 120 | ----------------------------- 121 | 122 | - Fix ``UnboundLocalError: local variable 'key_cls' referenced before assignment`` 123 | for cases when models is a python package, the ``to`` argument is a string 124 | that is used in this pattern ``app.Model`` and also there is some other 125 | ``bool`` const like ``null=True`` right after ``to``. (Sardorbek Imomaliev) 126 | - Don't crash if ForeignKey field doesn't have keyword arguments Fix 127 | `#230 `_ 128 | 129 | 130 | Version 2.0.8 (18 April 2019) 131 | ----------------------------- 132 | 133 | - Support recursive (self) ForeignKey relations. Fix 134 | `#208 `_ (Daniil Kharkov) 135 | 136 | 137 | Version 2.0.7 (16 April 2019) 138 | ----------------------------- 139 | 140 | - Fixed ``AstroidImportError`` for ``DecimalField``. Fix 141 | `#221 `_ (Daniil Kharkov) 142 | - Add ``load_configuration()`` in ``pylint_django/__init__.py``. Fix #222 143 | `#222 `_ 144 | - Support ForeignKey relations with ``to`` keyword. Fix 145 | `#223 `_ (Daniil Kharkov) 146 | 147 | 148 | Version 2.0.6 (27 Feb 2019) 149 | --------------------------- 150 | 151 | - Updating dependency version of pylint-plugin-utils as pylint 2.3 release 152 | was not compatible `#220 `_ 153 | - Improvements to tox.ini: 154 | `#217 `_ 155 | and `#216 `_ (@aerostitch) 156 | - Add support for new load_configuration hook of pylint 157 | `#214 `_ (@matusvalo) 158 | - 'urlpatterns' no longer reported as an invalid constant name 159 | 160 | 161 | Version 2.0.5 (17 Dec 2018) 162 | --------------------------- 163 | 164 | Bumping the version number because there's been a mix-up between 165 | GitHub tags and the versions pushed to PyPI for 2.0.3 and 2.0.4. 166 | 167 | Please use 2.0.5 which includes the changes mentioned below! 168 | 169 | 170 | Version 2.0.4 (do not use) 171 | -------------------------- 172 | 173 | - Avoid traceback with concurrent execution. Fix 174 | `#197 `_ 175 | - Suppress ``no-member`` errors for ``LazyFunction`` in factories 176 | - Suppress ``no-member`` errors for ``RelatedManager`` fields 177 | - Clean up compatibility code: 178 | `PR #207 `_ 179 | 180 | 181 | Version 2.0.3 (do not use) 182 | -------------------------- 183 | 184 | - Fixing compatability between ranges of astroid (2.0.4 -> 2.1) and 185 | pylint (2.1.1 -> 2.2). 186 | `#201 `_ and 187 | `#202 `_ 188 | 189 | Version 2.0.2 (26 Aug 2018) 190 | --------------------------- 191 | 192 | - Suppress false-positive no-self-argument in factory.post_generation. Fix 193 | `#190 `_ (Federico Bond) 194 | 195 | 196 | Version 2.0.1 (20 Aug 2018) 197 | --------------------------- 198 | 199 | - Enable testing with Django 2.1 200 | - Add test for Model.objects.get_or_create(). Close 201 | `#156 `__ 202 | - Add test for objects.exclude(). Close 203 | `#177 `__ 204 | - Fix Instance of 'Model' has no 'id' member (no-member), 205 | fix Class 'UserCreationForm' has no 'declared_fields' member. Close 206 | `#184 `__ 207 | - Fix for Instance of 'ManyToManyField' has no 'add' member. Close 208 | `#163 `__ 209 | - Add test & fix for unused arguments on class based views 210 | 211 | 212 | Version 2.0 (25 July 2018) 213 | -------------------------- 214 | 215 | - Requires pylint >= 2.0 which doesn't support Python 2 anymore! 216 | - Add modelform-uses-unicode check to flag dangerous use of the exclude 217 | attribute in ModelForm.Meta (Federico Bond). 218 | 219 | 220 | Version 0.11.1 (25 May 2018), the DjangoCon Heidelberg edition 221 | -------------------------------------------------------------- 222 | 223 | - Enable test case for ``urlpatterns`` variable which was previously disabled 224 | - Disable ``unused-argument`` message for the ``request`` argument passed to 225 | view functions. Fix 226 | `#155 `__ 227 | - Add transformations for ``model_utils`` managers instead of special-casing them. 228 | Fix 229 | `#160 `__ 230 | 231 | 232 | Version 0.11 (18 April 2018), the TestCon Moscow edition 233 | -------------------------------------------------------- 234 | 235 | - New ``JsonResponseChecker`` that looks for common anti-patterns with 236 | http responses returning JSON. This includes:: 237 | 238 | HttpResponse(json.dumps(data)) 239 | 240 | HttpResponse(data, content_type='application/json') 241 | 242 | JsonResponse(data, content_type=...) 243 | 244 | 245 | Version 0.10.0 (10 April 2018) 246 | ------------------------------ 247 | 248 | - Remove the compatibility layer for older astroid versions 249 | - Make flake8 happy. Fix 250 | `#102 `__ 251 | - Fix: compatibility with Python < 3.6 caused by ``ModuleNotFoundError`` 252 | not available on older versions of Python (Juan Rial) 253 | - Show README and CHANGELOG on PyPI. Fix 254 | `#122 `__ 255 | - Fix explicit unicode check with ``python_2_unicode_compatible`` base models 256 | (Federico Bond) 257 | - Suppress ``not-an-iterable`` message for 'objects'. Fix 258 | `#117 `__ 259 | - Teach pylint_django that ``objects.all()`` is subscriptable. Fix 260 | `#144 `__ 261 | - Suppress ``invalid-name`` for ``wsgi.application``. Fix 262 | `#77 `__ 263 | - Add test for ``WSGIRequest.context``. Closes 264 | `#78 `__ 265 | - Register transforms for ``FileField``. Fix 266 | `#60 `__ 267 | - New checker ``pylint_django.checkers.db_performance``. 268 | Enables checking of migrations and reports when there's an 269 | ``AddField`` operation with a default value which may slow down applying 270 | migrations on large tables. This may also lead to production tables 271 | being locked while migrations are being applied. Fix 272 | `#118 `__ 273 | - Suppress ``no-member`` for ``factory.SubFactory`` objects. 274 | Useful when model factories use ``factory.SubFactory()`` for foreign 275 | key relations. 276 | 277 | 278 | Version 0.9.4 (12 March 2018) 279 | ----------------------------- 280 | 281 | - Add an optional dependency on Django 282 | - Fix the ``DjangoInstalledChecker`` so it can actually warn when 283 | Django isn't available 284 | - Fix `#136 `__ by 285 | adding automated build and sanity test scripts 286 | 287 | Version 0.9.3 (removed from PyPI) 288 | --------------------------------- 289 | 290 | - Fix `#133 `__ and 291 | `#134 `__ by 292 | including package data when building wheel and tar.gz packages for 293 | PyPI (Joseph Herlant) 294 | 295 | Version 0.9.2 (broken) 296 | ---------------------- 297 | 298 | - Fix `#129 `__ - 299 | Move tests under ``site-packages/pylint_django`` (Mr. Senko) 300 | - Fix `#96 `__ - List 301 | Django as a dependency (Mr. Senko) 302 | 303 | Version 0.9.1 (26 Feb 2018) 304 | --------------------------- 305 | 306 | - Fix `#123 `__ - 307 | Update links after the move to PyCQA (Mr. Senko) 308 | - Add test for Meta class from django\_tables2 (Mr. Senko) 309 | - Fix flake8 complaints (Peter Bittner) 310 | - Add missing .txt and .rc test files to MANIFEST.in (Joseph Herlant) 311 | 312 | Version 0.9 (25 Jan 2018) 313 | ------------------------- 314 | 315 | - Fix `#120 `__ - 316 | TypeError: 'NamesConsumer' object does not support indexing (Simone 317 | Basso) 318 | - Fix `#110 `__ and 319 | `#35 `__ - resolve 320 | ForeignKey models specified as strings instead of class names (Mr. 321 | Senko) 322 | 323 | Version 0.8.0 (20 Jan 2018) 324 | --------------------------- 325 | 326 | - This is the last version to support Python 2. Issues a deprecation 327 | warning! 328 | - `#109 `__, adding 329 | 'urlpatterns', 'register', 'app\_name' to good names. Obsoletes 330 | `#111 `__, fixes 331 | `#108 `__ (Vinay 332 | Pai) 333 | - Add 'handler500' to good names (Mr. Senko) 334 | - `#103 `__: Support 335 | factory\_boy's DjangoModelFactory Meta class (Konstantinos 336 | Koukopoulos) 337 | - `#100 `__: Fix 338 | E1101:Instance of '**proxy**\ ' has no 'format' member' when using 339 | .format() on a ugettext\_lazy translation. Fixes 340 | `#80 `__ 341 | (canarduck) 342 | - `#99 `__: Add tests 343 | and transforms for DurationField, fixes 344 | `#95 `__ (James M. 345 | Allen) 346 | - `#92 `__: Add json 347 | field to WSGIRequest proxy (sjk4sc) 348 | - `#84 `__: Add support 349 | for django.contrib.postgres.fields and UUIDField (Villiers Strauss) 350 | - Stop testing with older Django versions. Currently testing with 351 | Django 1.11.x and 2.0 352 | - Stop testing on Python 2, no functional changes in the source code 353 | though 354 | - Update tests and require latest version of pylint (>=1.8), fixes 355 | `#53 `__, 356 | `#97 `__ 357 | - `#81 `__ Fix 358 | 'duplicate-except' false negative for except blocks which catch the 359 | ``DoesNotExist`` exception. 360 | 361 | Version 0.7.4 362 | ------------- 363 | 364 | - `#88 `__ Fixed builds 365 | with Django 1.10 (thanks to 366 | `federicobond `__) 367 | - `#91 `__ Fixed race 368 | condition when running with pylint parallel execution mode (thanks to 369 | `jeremycarroll `__) 370 | - `#64 `__ "Meta is 371 | old style class" now suppressed on BaseSerializer too (thanks to 372 | `unklphil `__) 373 | - `#70 `__ Updating to 374 | handle newer pylint/astroid versions (thanks to 375 | `iXce `__) 376 | 377 | Version 0.7.2 378 | ------------- 379 | 380 | - `#76 `__ Better 381 | handling of mongoengine querysetmanager 382 | - `#73 `__ 383 | `#72 `__ Make package 384 | zip safe to help fix some path problems 385 | - `#68 `__ Suppressed 386 | invalid constant warning for "app\_name" in urls.py 387 | - `#67 `__ Fix 388 | view.args and view.kwargs 389 | - `#66 `__ accessing 390 | \_meta no longer causes a protected-access warning as this is a 391 | public API as of Django 1.8 392 | - `#65 `__ Add support 393 | of mongoengine module. 394 | - `#59 `__ Silence 395 | old-style-class for widget Meta 396 | 397 | Version 0.7.1 398 | ------------- 399 | 400 | - `#52 `__ - Fixed 401 | stupid mistake when using versioninfo 402 | 403 | Version 0.7 404 | ----------- 405 | 406 | - `#51 `__ - Fixed 407 | compatibility with pylint 1.5 / astroid 1.4.1 408 | 409 | Version 0.6.1 410 | ------------- 411 | 412 | - `#43 `__ - Foreign 413 | key ID access (``somefk_id``) does not raise an 'attribute not found' 414 | warning 415 | - `#31 `__ - Support 416 | for custom model managers (thanks 417 | `smirolo `__) 418 | - `#48 `__ - Added 419 | support for django-restframework (thanks 420 | `mbertolacci `__) 421 | 422 | Version 0.6 423 | ----------- 424 | 425 | - Pylint 1.4 dropped support for Python 2.6, therefore a constraint is 426 | added that pylint-django will only work with Python2.6 if pylint<=1.3 427 | is installed 428 | - `#40 `__ - pylint 429 | 1.4 warned about View and Model classes not having enough public 430 | methods; this is suppressed 431 | - `#37 `__ - fixed an 432 | infinite loop when using astroid 1.3.3+ 433 | - `#36 `__ - no 434 | longer warning about lack of ``__unicode__`` method on abstract model 435 | classes 436 | - `PR #34 `__ - prevent 437 | warning about use of ``super()`` on ModelManager classes 438 | 439 | Version 0.5.5 440 | ------------- 441 | 442 | - `PR #27 `__ - better 443 | ``ForeignKey`` transforms, which now work when of the form 444 | ``othermodule.ModelClass``. This also fixes a problem where an 445 | inferred type would be ``_Yes`` and pylint would fail 446 | - `PR #28 `__ - better 447 | knowledge of ``ManyToManyField`` classes 448 | 449 | Version 0.5.4 450 | ------------- 451 | 452 | - Improved resiliance to inference failure when Django types cannot be 453 | inferred (which can happen if Django is not on the system path 454 | 455 | Version 0.5.3 456 | ------------- 457 | 458 | - `Issue #25 `__ 459 | Fixing cases where a module defines ``get`` as a method 460 | 461 | Version 0.5.2 462 | ------------- 463 | 464 | - Fixed a problem where type inference could get into an infinite loop 465 | 466 | Version 0.5.1 467 | ------------- 468 | 469 | - Removed usage of a Django object, as importing it caused Django to 470 | try to configure itself and thus throw an ImproperlyConfigured 471 | exception. 472 | 473 | Version 0.5 474 | ----------- 475 | 476 | - `Issue #7 `__ 477 | Improved handling of Django model fields 478 | - `Issue #10 `__ No 479 | warning about missing **unicode** if the Django python3/2 480 | compatability tools are used 481 | - `Issue #11 `__ 482 | Improved handling of Django form fields 483 | - `Issue #12 `__ 484 | Improved handling of Django ImageField and FileField objects 485 | - `Issue #14 `__ 486 | Models which do not define **unicode** but whose parents do now have 487 | a new error (W5103) instead of incorrectly warning about no 488 | **unicode** being present. 489 | - `Issue #21 `__ 490 | ``ForeignKey`` and ``OneToOneField`` fields on models are replaced 491 | with instance of the type they refer to in the AST, which allows 492 | pylint to generate correct warnings about attributes they may or may 493 | not have. 494 | 495 | Version 0.3 496 | ----------- 497 | 498 | - Python3 is now supported 499 | - ``__unicode__`` warning on models does not appear in Python3 500 | 501 | Version 0.2 502 | ----------- 503 | 504 | - Pylint now recognises ``BaseForm`` as an ancestor of ``Form`` and 505 | subclasses 506 | - Improved ``Form`` support 507 | - `Issue #2 `__ - a 508 | subclass of a ``Model`` or ``Form`` also has warnings about a 509 | ``Meta`` class suppressed. 510 | - `Issue #3 `__ - 511 | ``Form`` and ``ModelForm`` subclasses no longer warn about ``Meta`` 512 | classes. 513 | -------------------------------------------------------------------------------- /pylint_django/augmentations/__init__.py: -------------------------------------------------------------------------------- 1 | """Augmentations.""" 2 | # pylint: disable=invalid-name 3 | import functools 4 | import itertools 5 | 6 | from astroid import InferenceError 7 | from astroid.objects import Super 8 | from astroid.nodes import ClassDef, ImportFrom, Attribute 9 | from astroid.scoped_nodes import ClassDef as ScopedClass, Module 10 | 11 | from pylint.checkers.base import DocStringChecker, NameChecker 12 | from pylint.checkers.design_analysis import MisdesignChecker 13 | from pylint.checkers.classes import ClassChecker 14 | from pylint.checkers.newstyle import NewStyleConflictChecker 15 | from pylint.checkers.variables import VariablesChecker 16 | from pylint.checkers.typecheck import TypeChecker 17 | from pylint.checkers.variables import ScopeConsumer 18 | 19 | from pylint_plugin_utils import augment_visit, suppress_message 20 | 21 | from django import VERSION as django_version 22 | from django.views.generic.base import View, RedirectView, ContextMixin 23 | from django.views.generic.dates import DateMixin, DayMixin, MonthMixin, WeekMixin, YearMixin 24 | from django.views.generic.detail import SingleObjectMixin, SingleObjectTemplateResponseMixin, TemplateResponseMixin 25 | from django.views.generic.edit import DeletionMixin, FormMixin, ModelFormMixin 26 | from django.views.generic.list import MultipleObjectMixin, MultipleObjectTemplateResponseMixin 27 | from django.utils import termcolors 28 | 29 | from pylint_django.utils import node_is_subclass, PY3 30 | 31 | 32 | # Note: it would have been nice to import the Manager object from Django and 33 | # get its attributes that way - and this used to be the method - but unfortunately 34 | # there's no guarantee that Django is properly configured at that stage, and importing 35 | # anything from the django.db package causes an ImproperlyConfigured exception. 36 | # Therefore we'll fall back on a hard-coded list of attributes which won't be as accurate, 37 | # but this is not 100% accurate anyway. 38 | MANAGER_ATTRS = { 39 | 'none', 40 | 'all', 41 | 'count', 42 | 'dates', 43 | 'distinct', 44 | 'extra', 45 | 'get', 46 | 'get_or_create', 47 | 'update_or_create', 48 | 'get_queryset', 49 | 'create', 50 | 'bulk_create', 51 | 'filter', 52 | 'aggregate', 53 | 'annotate', 54 | 'complex_filter', 55 | 'exclude', 56 | 'in_bulk', 57 | 'iterator', 58 | 'latest', 59 | 'order_by', 60 | 'select_for_update', 61 | 'select_related', 62 | 'prefetch_related', 63 | 'values', 64 | 'values_list', 65 | 'update', 66 | 'reverse', 67 | 'defer', 68 | 'only', 69 | 'using', 70 | 'exists', 71 | } 72 | 73 | 74 | QS_ATTRS = { 75 | 'filter', 76 | 'exclude', 77 | 'annotate', 78 | 'order_by', 79 | 'reverse', 80 | 'distinct', 81 | 'values', 82 | 'values_list', 83 | 'dates', 84 | 'datetimes', 85 | 'none', 86 | 'all', 87 | 'select_related', 88 | 'prefetch_related', 89 | 'extra', 90 | 'defer', 91 | 'only', 92 | 'using', 93 | 'select_for_update', 94 | 'raw', 95 | 'get', 96 | 'create', 97 | 'get_or_create', 98 | 'update_or_create', 99 | 'bulk_create', 100 | 'count', 101 | 'in_bulk', 102 | 'iterator', 103 | 'latest', 104 | 'earliest', 105 | 'first', 106 | 'last', 107 | 'aggregate', 108 | 'exists', 109 | 'update', 110 | 'delete', 111 | 'as_manager', 112 | 'expression', 113 | 'output_field', 114 | } 115 | 116 | 117 | MODELADMIN_ATTRS = { 118 | # options 119 | 'actions', 120 | 'actions_on_top', 121 | 'actions_on_bottom', 122 | 'actions_selection_counter', 123 | 'date_hierarchy', 124 | 'empty_value_display', 125 | 'exclude', 126 | 'fields', 127 | 'fieldsets', 128 | 'filter_horizontal', 129 | 'filter_vertical', 130 | 'form', 131 | 'formfield_overrides', 132 | 'inlines', 133 | 'list_display', 134 | 'list_display_links', 135 | 'list_editable', 136 | 'list_filter', 137 | 'list_max_show_all', 138 | 'list_per_page', 139 | 'list_select_related', 140 | 'ordering', 141 | 'paginator', 142 | 'prepopulated_fields', 143 | 'preserve_filters', 144 | 'radio_fields', 145 | 'raw_id_fields', 146 | 'readonly_fields', 147 | 'save_as', 148 | 'save_on_top', 149 | 'search_fields', 150 | 'show_full_result_count', 151 | 'view_on_site', 152 | # template options 153 | 'add_form_template', 154 | 'change_form_template', 155 | 'change_list_template', 156 | 'delete_confirmation_template', 157 | 'delete_selected_confirmation_template', 158 | 'object_history_template', 159 | } 160 | 161 | 162 | MODEL_ATTRS = { 163 | 'id', 164 | 'DoesNotExist', 165 | 'MultipleObjectsReturned', 166 | '_base_manager', 167 | '_default_manager', 168 | '_meta', 169 | 'delete', 170 | 'get_next_by_date', 171 | 'get_previous_by_date', 172 | 'objects', 173 | 'save', 174 | } 175 | 176 | 177 | FIELD_ATTRS = { 178 | 'null', 179 | 'blank', 180 | 'choices', 181 | 'db_column', 182 | 'db_index', 183 | 'db_tablespace', 184 | 'default', 185 | 'editable', 186 | 'error_messages', 187 | 'help_text', 188 | 'primary_key', 189 | 'unique', 190 | 'unique_for_date', 191 | 'unique_for_month', 192 | 'unique_for_year', 193 | 'verbose_name', 194 | 'validators', 195 | } 196 | 197 | 198 | CHAR_FIELD_ATTRS = { 199 | 'max_length', 200 | } 201 | 202 | 203 | DATE_FIELD_ATTRS = { 204 | 'auto_now', 205 | 'auto_now_add', 206 | } 207 | 208 | 209 | DECIMAL_FIELD_ATTRS = { 210 | 'max_digits', 211 | 'decimal_places', 212 | } 213 | 214 | 215 | FILE_FIELD_ATTRS = { 216 | 'upload_to', 217 | 'storage', 218 | } 219 | 220 | 221 | IMAGE_FIELD_ATTRS = { 222 | 'height_field', 223 | 'width_field', 224 | } 225 | 226 | 227 | IP_FIELD_ATTRS = { 228 | 'protocol', 229 | 'unpack_ipv4', 230 | } 231 | 232 | 233 | SLUG_FIELD_ATTRS = { 234 | 'allow_unicode', 235 | } 236 | 237 | 238 | FOREIGNKEY_FIELD_ATTRS = { 239 | 'limit_choices_to', 240 | 'related_name', 241 | 'related_query_name', 242 | 'to_field', 243 | 'db_constraint', 244 | 'swappable', 245 | } 246 | 247 | 248 | MANYTOMANY_FIELD_ATTRS = { 249 | 'add', 250 | 'clear', 251 | 'related_name', 252 | 'related_query_name', 253 | 'remove', 254 | 'set', 255 | 'limit_choices_to', 256 | 'symmetrical', 257 | 'through', 258 | 'through_fields', 259 | 'db_table', 260 | 'db_constraint', 261 | 'swappable', 262 | } 263 | 264 | 265 | ONETOONE_FIELD_ATTRS = { 266 | 'parent_link', 267 | } 268 | 269 | 270 | STYLE_ATTRS = set(itertools.chain.from_iterable(termcolors.PALETTES.values())) 271 | 272 | 273 | VIEW_ATTRS = { 274 | ( 275 | ( 276 | '{}.{}'.format(cls.__module__, cls.__name__), 277 | '.{}'.format(cls.__name__) 278 | ), 279 | tuple(cls.__dict__.keys()) 280 | ) for cls in ( 281 | View, RedirectView, ContextMixin, 282 | DateMixin, DayMixin, MonthMixin, WeekMixin, YearMixin, 283 | SingleObjectMixin, SingleObjectTemplateResponseMixin, TemplateResponseMixin, 284 | DeletionMixin, FormMixin, ModelFormMixin, 285 | MultipleObjectMixin, MultipleObjectTemplateResponseMixin, 286 | ) 287 | } 288 | 289 | 290 | FORM_ATTRS = { 291 | 'declared_fields', 292 | } 293 | 294 | 295 | def ignore_import_warnings_for_related_fields(orig_method, self, node): 296 | """ 297 | Replaces the leave_module method on the VariablesChecker class to 298 | prevent unused-import warnings which are caused by the ForeignKey 299 | and OneToOneField transformations. By replacing the nodes in the 300 | AST with their type rather than the django field, imports of the 301 | form 'from django.db.models import OneToOneField' raise an unused-import 302 | warning 303 | """ 304 | consumer = self._to_consume[0] # pylint: disable=W0212 305 | # we can disable this warning ('Access to a protected member _to_consume of a client class') 306 | # as it's not actually a client class, but rather, this method is being monkey patched 307 | # onto the class and so the access is valid 308 | 309 | new_things = {} 310 | 311 | iterat = consumer.to_consume.items if PY3 else consumer.to_consume.iteritems 312 | for name, stmts in iterat(): 313 | if isinstance(stmts[0], ImportFrom): 314 | if any([n[0] in ('ForeignKey', 'OneToOneField') for n in stmts[0].names]): 315 | continue 316 | new_things[name] = stmts 317 | 318 | consumer._atomic = ScopeConsumer(new_things, consumer.consumed, consumer.scope_type) # pylint: disable=W0212 319 | self._to_consume = [consumer] # pylint: disable=W0212 320 | 321 | return orig_method(self, node) 322 | 323 | 324 | def foreign_key_sets(chain, node): 325 | """ 326 | When a Django model has a ForeignKey to another model, the target 327 | of the foreign key gets a '_set' attribute for accessing 328 | a queryset of the model owning the foreign key - eg: 329 | 330 | class ModelA(models.Model): 331 | pass 332 | 333 | class ModelB(models.Model): 334 | a = models.ForeignKey(ModelA) 335 | 336 | Now, ModelA instances will have a modelb_set attribute. 337 | 338 | It's also possible to explicitly name the relationship using the related_name argument 339 | to the ForeignKey constructor. As it's impossible to know this without inspecting all 340 | models before processing, we'll instead do a "best guess" approach and see if the attribute 341 | being accessed goes on to be used as a queryset. This is via 'duck typing': if the method 342 | called on the attribute being accessed is something we might find in a queryset, we'll 343 | warn. 344 | """ 345 | quack = False 346 | 347 | if node.attrname in MANAGER_ATTRS or node.attrname.endswith('_set'): 348 | # if this is a X_set method, that's a pretty strong signal that this is the default 349 | # Django name, rather than one set by related_name 350 | quack = True 351 | else: 352 | # we will 353 | if isinstance(node.parent, Attribute): 354 | func_name = getattr(node.parent, 'attrname', None) 355 | if func_name in MANAGER_ATTRS: 356 | quack = True 357 | 358 | if quack: 359 | children = list(node.get_children()) 360 | for child in children: 361 | try: 362 | inferred_cls = child.inferred() 363 | except InferenceError: 364 | pass 365 | else: 366 | for cls in inferred_cls: 367 | if (node_is_subclass(cls, 368 | 'django.db.models.manager.Manager', 369 | 'django.db.models.base.Model', 370 | '.Model', 371 | 'django.db.models.fields.related.ForeignObject')): 372 | # This means that we are looking at a subclass of models.Model 373 | # and something is trying to access a _set attribute. 374 | # Since this could exist, we will return so as not to raise an 375 | # error. 376 | return 377 | chain() 378 | 379 | 380 | def foreign_key_ids(chain, node): 381 | if node.attrname.endswith('_id'): 382 | return 383 | chain() 384 | 385 | 386 | def is_model_admin_subclass(node): 387 | """Checks that node is derivative of ModelAdmin class.""" 388 | if node.name[-5:] != 'Admin' or isinstance(node.parent, ClassDef): 389 | return False 390 | 391 | return node_is_subclass(node, 'django.contrib.admin.options.ModelAdmin') 392 | 393 | 394 | def is_model_media_subclass(node): 395 | """Checks that node is derivative of Media class.""" 396 | if node.name != 'Media' or not isinstance(node.parent, ClassDef): 397 | return False 398 | 399 | parents = ('django.contrib.admin.options.ModelAdmin', 400 | 'django.forms.widgets.Media', 401 | 'django.db.models.base.Model', 402 | '.Model', # for the transformed version used in this plugin 403 | 'django.forms.forms.Form', 404 | '.Form', 405 | 'django.forms.widgets.Widget', 406 | '.Widget', 407 | 'django.forms.models.ModelForm', 408 | '.ModelForm') 409 | return node_is_subclass(node.parent, *parents) 410 | 411 | 412 | def is_model_meta_subclass(node): 413 | """Checks that node is derivative of Meta class.""" 414 | if node.name != 'Meta' or not isinstance(node.parent, ClassDef): 415 | return False 416 | 417 | parents = ('.Model', # for the transformed version used here 418 | 'django.db.models.base.Model', 419 | '.Form', 420 | 'django.forms.forms.Form', 421 | '.ModelForm', 422 | 'django.forms.models.ModelForm', 423 | 'rest_framework.serializers.BaseSerializer', 424 | 'rest_framework.generics.GenericAPIView', 425 | 'rest_framework.viewsets.ReadOnlyModelViewSet', 426 | 'rest_framework.viewsets.ModelViewSet', 427 | 'django_filters.filterset.FilterSet', 428 | 'factory.django.DjangoModelFactory',) 429 | return node_is_subclass(node.parent, *parents) 430 | 431 | 432 | def is_model_factory(node): 433 | """Checks that node is derivative of DjangoModelFactory or SubFactory class.""" 434 | try: 435 | parent_classes = node.expr.inferred() 436 | except: # noqa: E722, pylint: disable=bare-except 437 | return False 438 | 439 | parents = ('factory.declarations.LazyFunction', 440 | 'factory.declarations.SubFactory', 441 | 'factory.django.DjangoModelFactory') 442 | 443 | for parent_class in parent_classes: 444 | try: 445 | if parent_class.qname() in parents: 446 | return True 447 | 448 | if node_is_subclass(parent_class, *parents): 449 | return True 450 | except AttributeError: 451 | continue 452 | 453 | return False 454 | 455 | 456 | def is_factory_post_generation_method(node): 457 | if not node.decorators: 458 | return False 459 | 460 | for decorator in node.decorators.get_children(): 461 | try: 462 | inferred = decorator.inferred() 463 | except InferenceError: 464 | continue 465 | 466 | for target in inferred: 467 | if target.qname() == 'factory.helpers.post_generation': 468 | return True 469 | 470 | return False 471 | 472 | 473 | def is_model_mpttmeta_subclass(node): 474 | """Checks that node is derivative of MPTTMeta class.""" 475 | if node.name != 'MPTTMeta' or not isinstance(node.parent, ClassDef): 476 | return False 477 | 478 | parents = ('django.db.models.base.Model', 479 | '.Model', # for the transformed version used in this plugin 480 | 'django.forms.forms.Form', 481 | '.Form', 482 | 'django.forms.models.ModelForm', 483 | '.ModelForm') 484 | return node_is_subclass(node.parent, *parents) 485 | 486 | 487 | def _attribute_is_magic(node, attrs, parents): 488 | """Checks that node is an attribute used inside one of allowed parents""" 489 | if node.attrname not in attrs: 490 | return False 491 | if not node.last_child(): 492 | return False 493 | 494 | try: 495 | for cls in node.last_child().inferred(): 496 | if isinstance(cls, Super): 497 | cls = cls._self_class # pylint: disable=protected-access 498 | if node_is_subclass(cls, *parents) or cls.qname() in parents: 499 | return True 500 | except InferenceError: 501 | pass 502 | return False 503 | 504 | 505 | def is_style_attribute(node): 506 | parents = ('django.core.management.color.Style', ) 507 | return _attribute_is_magic(node, STYLE_ATTRS, parents) 508 | 509 | 510 | def is_manager_attribute(node): 511 | """Checks that node is attribute of Manager or QuerySet class.""" 512 | parents = ('django.db.models.manager.Manager', 513 | '.Manager', 514 | 'factory.base.BaseFactory.build', 515 | 'django.db.models.query.QuerySet', 516 | '.QuerySet') 517 | return _attribute_is_magic(node, MANAGER_ATTRS.union(QS_ATTRS), parents) 518 | 519 | 520 | def is_admin_attribute(node): 521 | """Checks that node is attribute of BaseModelAdmin.""" 522 | parents = ('django.contrib.admin.options.BaseModelAdmin', 523 | '.BaseModelAdmin') 524 | return _attribute_is_magic(node, MODELADMIN_ATTRS, parents) 525 | 526 | 527 | def is_model_attribute(node): 528 | """Checks that node is attribute of Model.""" 529 | parents = ('django.db.models.base.Model', 530 | '.Model') 531 | return _attribute_is_magic(node, MODEL_ATTRS, parents) 532 | 533 | 534 | def is_field_attribute(node): 535 | """Checks that node is attribute of Field.""" 536 | parents = ('django.db.models.fields.Field', 537 | '.Field') 538 | return _attribute_is_magic(node, FIELD_ATTRS, parents) 539 | 540 | 541 | def is_charfield_attribute(node): 542 | """Checks that node is attribute of CharField.""" 543 | parents = ('django.db.models.fields.CharField', 544 | '.CharField') 545 | return _attribute_is_magic(node, CHAR_FIELD_ATTRS, parents) 546 | 547 | 548 | def is_datefield_attribute(node): 549 | """Checks that node is attribute of DateField.""" 550 | parents = ('django.db.models.fields.DateField', 551 | '.DateField') 552 | return _attribute_is_magic(node, DATE_FIELD_ATTRS, parents) 553 | 554 | 555 | def is_decimalfield_attribute(node): 556 | """Checks that node is attribute of DecimalField.""" 557 | parents = ('django.db.models.fields.DecimalField', 558 | '.DecimalField') 559 | return _attribute_is_magic(node, DECIMAL_FIELD_ATTRS, parents) 560 | 561 | 562 | def is_filefield_attribute(node): 563 | """Checks that node is attribute of FileField.""" 564 | parents = ('django.db.models.fields.files.FileField', 565 | '.FileField') 566 | return _attribute_is_magic(node, FILE_FIELD_ATTRS, parents) 567 | 568 | 569 | def is_imagefield_attribute(node): 570 | """Checks that node is attribute of ImageField.""" 571 | parents = ('django.db.models.fields.files.ImageField', 572 | '.ImageField') 573 | return _attribute_is_magic(node, IMAGE_FIELD_ATTRS, parents) 574 | 575 | 576 | def is_ipfield_attribute(node): 577 | """Checks that node is attribute of GenericIPAddressField.""" 578 | parents = ('django.db.models.fields.GenericIPAddressField', 579 | '.GenericIPAddressField') 580 | return _attribute_is_magic(node, IP_FIELD_ATTRS, parents) 581 | 582 | 583 | def is_slugfield_attribute(node): 584 | """Checks that node is attribute of SlugField.""" 585 | parents = ('django.db.models.fields.SlugField', 586 | '.SlugField') 587 | return _attribute_is_magic(node, SLUG_FIELD_ATTRS, parents) 588 | 589 | 590 | def is_foreignkeyfield_attribute(node): 591 | """Checks that node is attribute of ForeignKey.""" 592 | parents = ('django.db.models.fields.related.ForeignKey', 593 | '.ForeignKey') 594 | return _attribute_is_magic(node, FOREIGNKEY_FIELD_ATTRS, parents) 595 | 596 | 597 | def is_manytomanyfield_attribute(node): 598 | """Checks that node is attribute of ManyToManyField.""" 599 | parents = ('django.db.models.fields.related.ManyToManyField', 600 | '.ManyToManyField') 601 | return _attribute_is_magic(node, MANYTOMANY_FIELD_ATTRS, parents) 602 | 603 | 604 | def is_onetoonefield_attribute(node): 605 | """Checks that node is attribute of OneToOneField.""" 606 | parents = ('django.db.models.fields.related.OneToOneField', 607 | '.OneToOneField') 608 | return _attribute_is_magic(node, ONETOONE_FIELD_ATTRS, parents) 609 | 610 | 611 | def is_form_attribute(node): 612 | """Checks that node is attribute of Form.""" 613 | parents = ('django.forms.forms.Form', 'django.forms.models.ModelForm') 614 | return _attribute_is_magic(node, FORM_ATTRS, parents) 615 | 616 | 617 | def is_model_test_case_subclass(node): 618 | """Checks that node is derivative of TestCase class.""" 619 | if not node.name.endswith('Test') and not isinstance(node.parent, ClassDef): 620 | return False 621 | 622 | return node_is_subclass(node, 'django.test.testcases.TestCase') 623 | 624 | 625 | def generic_is_view_attribute(parents, attrs): 626 | """Generates is_X_attribute function for given parents and attrs.""" 627 | def is_attribute(node): 628 | return _attribute_is_magic(node, attrs, parents) 629 | return is_attribute 630 | 631 | 632 | def is_model_view_subclass_method_shouldnt_be_function(node): 633 | """Checks that node is get or post method of the View class.""" 634 | if node.name not in ('get', 'post'): 635 | return False 636 | 637 | parent = node.parent 638 | while parent and not isinstance(parent, ScopedClass): 639 | parent = parent.parent 640 | 641 | subclass = ('django.views.View', 642 | 'django.views.generic.View', 643 | 'django.views.generic.base.View',) 644 | 645 | return parent is not None and node_is_subclass(parent, *subclass) 646 | 647 | 648 | def ignore_unused_argument_warnings_for_request(orig_method, self, stmt, name): 649 | """ 650 | Ignore unused-argument warnings for function arguments named "request". 651 | 652 | The signature of Django view functions require the request argument but it is okay if the request is not used. 653 | This function should be used as a wrapper for the `VariablesChecker._is_name_ignored` method. 654 | """ 655 | if name == 'request': 656 | return True 657 | 658 | return orig_method(self, stmt, name) 659 | 660 | 661 | def is_model_field_display_method(node): 662 | """Accept model's fields with get_*_display names.""" 663 | if not node.attrname.endswith('_display'): 664 | return False 665 | if not node.attrname.startswith('get_'): 666 | return False 667 | 668 | if node.last_child(): 669 | # TODO: could validate the names of the fields on the model rather than 670 | # blindly accepting get_*_display 671 | try: 672 | for cls in node.last_child().inferred(): 673 | if node_is_subclass(cls, 'django.db.models.base.Model', '.Model'): 674 | return True 675 | except InferenceError: 676 | return False 677 | return False 678 | 679 | 680 | def is_model_media_valid_attributes(node): 681 | """Suppress warnings for valid attributes of Media class.""" 682 | if node.name not in ('js', ): 683 | return False 684 | 685 | parent = node.parent 686 | while parent and not isinstance(parent, ScopedClass): 687 | parent = parent.parent 688 | 689 | if parent is None or parent.name != "Media": 690 | return False 691 | 692 | return True 693 | 694 | 695 | def is_templatetags_module_valid_constant(node): 696 | """Suppress warnings for valid constants in templatetags module.""" 697 | if node.name not in ('register', ): 698 | return False 699 | 700 | parent = node.parent 701 | while not isinstance(parent, Module): 702 | parent = parent.parent 703 | 704 | if "templatetags." not in parent.name: 705 | return False 706 | 707 | return True 708 | 709 | 710 | def is_urls_module_valid_constant(node): 711 | """Suppress warnings for valid constants in urls module.""" 712 | if node.name not in ('urlpatterns', 'app_name'): 713 | return False 714 | 715 | parent = node.parent 716 | while not isinstance(parent, Module): 717 | parent = parent.parent 718 | 719 | if not parent.name.endswith('urls'): 720 | return False 721 | 722 | return True 723 | 724 | 725 | def allow_meta_protected_access(node): 726 | if django_version >= (1, 8): 727 | return node.attrname == '_meta' 728 | 729 | return False 730 | 731 | 732 | def is_class(class_name): 733 | """Shortcut for node_is_subclass.""" 734 | return lambda node: node_is_subclass(node, class_name) 735 | 736 | 737 | def wrap(orig_method, with_method): 738 | @functools.wraps(orig_method) 739 | def wrap_func(*args, **kwargs): 740 | return with_method(orig_method, *args, **kwargs) 741 | return wrap_func 742 | 743 | 744 | def is_wsgi_application(node): 745 | frame = node.frame() 746 | return node.name == 'application' and isinstance(frame, Module) and \ 747 | (frame.name == 'asgi' or frame.path[0].endswith('asgi.py') or frame.file.endswith('asgi.py') or 748 | frame.name == 'wsgi' or frame.path[0].endswith('wsgi.py') or frame.file.endswith('wsgi.py')) 749 | 750 | 751 | # Compat helpers 752 | def pylint_newstyle_classdef_compat(linter, warning_name, augment): 753 | if not hasattr(NewStyleConflictChecker, 'visit_classdef'): 754 | return 755 | suppress_message(linter, getattr(NewStyleConflictChecker, 'visit_classdef'), warning_name, augment) 756 | 757 | 758 | def apply_wrapped_augmentations(): 759 | """ 760 | Apply augmentation and supression rules through monkey patching of pylint. 761 | """ 762 | # NOTE: The monkey patching is done with wrap and needs to be done in a thread safe manner to support the 763 | # parallel option of pylint (-j). 764 | # This is achieved by comparing __name__ of the monkey patched object to the original value and only patch it if 765 | # these are equal. 766 | 767 | # Unused argument 'request' (get, post) 768 | current_is_name_ignored = VariablesChecker._is_name_ignored # pylint: disable=protected-access 769 | if current_is_name_ignored.__name__ == '_is_name_ignored': 770 | # pylint: disable=protected-access 771 | VariablesChecker._is_name_ignored = wrap(current_is_name_ignored, ignore_unused_argument_warnings_for_request) 772 | 773 | # ForeignKey and OneToOneField 774 | current_leave_module = VariablesChecker.leave_module 775 | if current_leave_module.__name__ == 'leave_module': 776 | # current_leave_module is not wrapped 777 | # Two threads may hit the next assignment concurrently, but the result is the same 778 | VariablesChecker.leave_module = wrap(current_leave_module, ignore_import_warnings_for_related_fields) 779 | # VariablesChecker.leave_module is now wrapped 780 | # else VariablesChecker.leave_module is already wrapped 781 | 782 | 783 | # augment things 784 | def apply_augmentations(linter): 785 | """Apply augmentation and suppression rules.""" 786 | augment_visit(linter, TypeChecker.visit_attribute, foreign_key_sets) 787 | augment_visit(linter, TypeChecker.visit_attribute, foreign_key_ids) 788 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_model_field_display_method) 789 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_style_attribute) 790 | suppress_message(linter, NameChecker.visit_assignname, 'invalid-name', is_urls_module_valid_constant) 791 | 792 | # supress errors when accessing magical class attributes 793 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_manager_attribute) 794 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_admin_attribute) 795 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_model_attribute) 796 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_field_attribute) 797 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_charfield_attribute) 798 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_datefield_attribute) 799 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_decimalfield_attribute) 800 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_filefield_attribute) 801 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_imagefield_attribute) 802 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_ipfield_attribute) 803 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_slugfield_attribute) 804 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_foreignkeyfield_attribute) 805 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_manytomanyfield_attribute) 806 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_onetoonefield_attribute) 807 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_form_attribute) 808 | 809 | for parents, attrs in VIEW_ATTRS: 810 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', generic_is_view_attribute(parents, attrs)) 811 | 812 | # formviews have too many ancestors, there's nothing the user of the library can do about that 813 | suppress_message(linter, MisdesignChecker.visit_classdef, 'too-many-ancestors', 814 | is_class('django.views.generic.edit.FormView')) 815 | 816 | # class-based generic views just have a longer inheritance chain 817 | suppress_message(linter, MisdesignChecker.visit_classdef, 'too-many-ancestors', 818 | is_class('django.views.generic.detail.BaseDetailView')) 819 | suppress_message(linter, MisdesignChecker.visit_classdef, 'too-many-ancestors', 820 | is_class('django.views.generic.edit.ProcessFormView')) 821 | 822 | # model forms have no __init__ method anywhere in their bases 823 | suppress_message(linter, ClassChecker.visit_classdef, 'W0232', is_class('django.forms.models.ModelForm')) 824 | 825 | # Meta 826 | suppress_message(linter, DocStringChecker.visit_classdef, 'missing-docstring', is_model_meta_subclass) 827 | pylint_newstyle_classdef_compat(linter, 'old-style-class', is_model_meta_subclass) 828 | suppress_message(linter, ClassChecker.visit_classdef, 'no-init', is_model_meta_subclass) 829 | suppress_message(linter, MisdesignChecker.leave_classdef, 'too-few-public-methods', is_model_meta_subclass) 830 | suppress_message(linter, ClassChecker.visit_attribute, 'protected-access', allow_meta_protected_access) 831 | 832 | # Media 833 | suppress_message(linter, NameChecker.visit_assignname, 'C0103', is_model_media_valid_attributes) 834 | suppress_message(linter, DocStringChecker.visit_classdef, 'missing-docstring', is_model_media_subclass) 835 | pylint_newstyle_classdef_compat(linter, 'old-style-class', is_model_media_subclass) 836 | suppress_message(linter, ClassChecker.visit_classdef, 'no-init', is_model_media_subclass) 837 | suppress_message(linter, MisdesignChecker.leave_classdef, 'too-few-public-methods', is_model_media_subclass) 838 | 839 | # Admin 840 | # Too many public methods (40+/20) 841 | # TODO: Count public methods of django.contrib.admin.options.ModelAdmin and increase 842 | # MisdesignChecker.config.max_public_methods to this value to count only user' methods. 843 | # nb_public_methods = 0 844 | # for method in node.methods(): 845 | # if not method.name.startswith('_'): 846 | # nb_public_methods += 1 847 | suppress_message(linter, MisdesignChecker.leave_classdef, 'R0904', is_model_admin_subclass) 848 | 849 | # Tests 850 | suppress_message(linter, MisdesignChecker.leave_classdef, 'R0904', is_model_test_case_subclass) 851 | 852 | # View 853 | # Method could be a function (get, post) 854 | suppress_message(linter, ClassChecker.leave_functiondef, 'no-self-use', 855 | is_model_view_subclass_method_shouldnt_be_function) 856 | 857 | # django-mptt 858 | suppress_message(linter, DocStringChecker.visit_classdef, 'missing-docstring', is_model_mpttmeta_subclass) 859 | pylint_newstyle_classdef_compat(linter, 'old-style-class', is_model_mpttmeta_subclass) 860 | suppress_message(linter, ClassChecker.visit_classdef, 'W0232', is_model_mpttmeta_subclass) 861 | suppress_message(linter, MisdesignChecker.leave_classdef, 'too-few-public-methods', is_model_mpttmeta_subclass) 862 | 863 | # factory_boy's DjangoModelFactory 864 | suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_model_factory) 865 | suppress_message(linter, ClassChecker.visit_functiondef, 'no-self-argument', is_factory_post_generation_method) 866 | 867 | # wsgi.py 868 | suppress_message(linter, NameChecker.visit_assignname, 'invalid-name', is_wsgi_application) 869 | 870 | apply_wrapped_augmentations() 871 | --------------------------------------------------------------------------------