├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── django_linter ├── __init__.py ├── __pkginfo__.py ├── checkers │ ├── __init__.py │ ├── forms.py │ ├── layout.py │ ├── misc.py │ ├── models.py │ ├── settings.py │ └── views.py ├── main.py ├── suppressers │ ├── __init__.py │ └── suppress.py └── transformers │ ├── __init__.py │ ├── factories.py │ ├── models.py │ └── testing.py ├── generate_readme.py ├── requirements-test.txt ├── setup.cfg ├── setup.py └── tests ├── input ├── __init__.py ├── func_forms_form_field_redefinition.py ├── func_misc_print_used_py34.py ├── func_misc_print_used_py_27.py ├── func_models_float_money_field.py ├── func_models_get_absolute_url_without_reverse.py ├── func_models_model_field_redefinition.py ├── func_models_naive_datetime_used.py ├── func_models_nullable_text_field.py ├── func_models_related_field_name_with_id.py ├── func_models_unicode_method_absent.py ├── func_models_unicode_method_return.py ├── func_settings_improper_settings_import.py ├── func_suppressers_good_names.py ├── func_transformers_factories.py ├── func_transformers_testing.py ├── func_views_fetching_db_objects_len.py ├── func_views_is_authenticated_not_called.py ├── func_views_objects_get_without_doesnotexist.py ├── func_views_raw_get_post_access.py └── models.py ├── messages ├── func_forms_form_field_redefinition.txt ├── func_misc_print_used_py34.txt ├── func_misc_print_used_py_27.txt ├── func_models_float_money_field.txt ├── func_models_get_absolute_url_without_reverse.txt ├── func_models_model_field_redefinition.txt ├── func_models_naive_datetime_used.txt ├── func_models_nullable_text_field.txt ├── func_models_related_field_name_with_id.txt ├── func_models_unicode_method_absent.txt ├── func_models_unicode_method_return.txt ├── func_settings_improper_settings_import.txt ├── func_suppressers_good_names.txt ├── func_transformers_factories.txt ├── func_transformers_testing.txt ├── func_views_fetching_db_objects_len.txt ├── func_views_is_authenticated_not_called.txt ├── func_views_objects_get_without_doesnotexist.txt └── func_views_raw_get_post_access.txt ├── settings ├── __init__.py ├── empty │ ├── __init__.py │ └── settings.py └── required │ ├── __init__.py │ └── settings.py ├── test.py └── test_settings.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.sublime-project 3 | *.sublime-workspace 4 | dist 5 | build 6 | *.egg-info 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.5" 5 | env: 6 | - DJANGO_VERSION=1.9 7 | - DJANGO_VERSION=1.8.7 8 | install: 9 | - pip install -q pylint Django==$DJANGO_VERSION djangorestframework factory-boy 10 | script: python -m unittest discover -s tests 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Timofey Trukhanov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE django_linter 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django linter 2 | ============= 3 | 4 | .. image:: https://travis-ci.org/geerk/django_linter.svg?branch=master 5 | :target: https://travis-ci.org/geerk/django_linter 6 | 7 | This is a simple extension for pylint that aims to check some common mistakes in django projects. 8 | 9 | Contributions are welcome. 10 | 11 | Installation 12 | ------------ 13 | 14 | :: 15 | 16 | pip install django_linter 17 | 18 | Usage 19 | ----- 20 | 21 | It can be used as a plugin or standalone script. To use it as a plugin it should be installed first, then run with pylint: 22 | 23 | :: 24 | 25 | pylint --load-plugins=django_linter TARGET 26 | 27 | To use it as a standalone script: 28 | 29 | 30 | :: 31 | 32 | usage: django-linter [-h] TARGET [TARGET ...] 33 | 34 | Simple extension for pylint to check django projects for common mistakes. 35 | 36 | positional arguments: 37 | TARGET python package or module 38 | 39 | optional arguments: 40 | -h, --help show this help message and exit 41 | 42 | Implemented checks 43 | ------------------ 44 | 45 | **Settings:** 46 | 47 | - E5221 (required-setting-missed): Used when required setting missed in settings file. 48 | - E5222 (empty-setting): Used when setting is empty value. 49 | - W5221 (improper-settings-import): Used when settings is not imported from django.conf 50 | 51 | **Models:** 52 | 53 | - W5241 (nullable-text-field): Used when text field has null=True. 54 | - W5242 (float-money-field): Used when money related field uses FloatField. 55 | - W5243 (naive-datetime-used): Used when there is datetime.now is used. 56 | - W5244 (related-field-named-with-id): Used when related field is named with _id suffix 57 | - W5245 (unicode-method-absent): Used when model has no unicode method. 58 | - W5246 (unicode-method-return): Used when unicode method does not return unicode. 59 | - W5247 (model-field-redefinition): Used when there are more than one model field with the same name. 60 | - W5248 (get-absolute-url-without-reverse): Used when get_absolute_url method is defined without using reverse function. 61 | 62 | **Forms:** 63 | 64 | - W5211 (form-field-redefinition): Used when there are more than one form field with the same name. 65 | 66 | **Views:** 67 | 68 | - W5231 (is-authenticated-not-called): Used when is_authenticated method is not called 69 | - W5232 (objects-get-without-doesnotexist): Used when Model.objects.get is used without enclosing it in try-except block to catch DoesNotExist exception. 70 | - W5233 (fetching-db-objects-len): Used when there is db query that fetches objects from database only to check the number of returned objects. 71 | - W5234 (raw-get-post-access): Used when request.GET or request.POST dicts is accessed directly, it is better to use forms. 72 | 73 | **Layout:** 74 | 75 | - W5201 (forms-layout): Used when form class definition is not in forms module. 76 | - W5202 (admin-layout): Used when admin class definition is not in admin module. 77 | 78 | **Misc:** 79 | 80 | - W5251 (print-used): Used when there is print statement or function 81 | 82 | Implemented suppressers 83 | ----------------------- 84 | 85 | - "Meta" classes 86 | - urlpatterns 87 | - logger 88 | 89 | Implemented transformers 90 | ------------------------ 91 | 92 | **Models** 93 | 94 | - "id" field 95 | - "objects" manager 96 | - "DoesNotExist" exception 97 | - "MultipleObjectsReturned" exception 98 | 99 | **Testing** 100 | 101 | - test responses (django and DRF) 102 | 103 | **Factories** 104 | 105 | - factory-boy's factories (factory should return django model, but not always possible to infer model class) 106 | 107 | -------------------------------------------------------------------------------- /django_linter/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | from . import checkers, suppressers, transformers 5 | 6 | 7 | def register(linter): 8 | checkers.register(linter) 9 | suppressers.register(linter) 10 | transformers.register(linter) 11 | -------------------------------------------------------------------------------- /django_linter/__pkginfo__.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | BASE_ID = 52 5 | -------------------------------------------------------------------------------- /django_linter/checkers/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | from . import settings, models, misc, layout, forms, views 5 | 6 | 7 | def register(linter): 8 | linter.register_checker(settings.SettingsShecker(linter)) 9 | linter.register_checker(models.ModelsChecker(linter)) 10 | linter.register_checker(misc.MiscChecker(linter)) 11 | linter.register_checker(layout.LayoutChecker(linter)) 12 | linter.register_checker(forms.FormsChecker(linter)) 13 | linter.register_checker(views.ViewsChecker(linter)) 14 | -------------------------------------------------------------------------------- /django_linter/checkers/forms.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | from pylint.checkers import BaseChecker 5 | from pylint.interfaces import IAstroidChecker 6 | from pylint.checkers.utils import safe_infer 7 | from astroid import YES, AssName 8 | 9 | from ..__pkginfo__ import BASE_ID 10 | 11 | 12 | class FormsChecker(BaseChecker): 13 | __implements__ = IAstroidChecker 14 | 15 | name = 'forms' 16 | msgs = { 17 | 'W%s11' % BASE_ID: ( 18 | 'Form field redefinition: %s.%s', 19 | 'form-field-redefinition', 20 | 'Used when there are more than one form field with the same name.'), 21 | } 22 | 23 | _is_form_class = False 24 | 25 | def visit_classdef(self, node): 26 | self._is_form_class = bool( 27 | node.is_subtype_of('django.forms.forms.BaseForm')) 28 | if self._is_form_class: 29 | self._form_field_names = set() 30 | self._form_name = node.name 31 | 32 | def leave_classdef(self, node): 33 | self._is_form_class = False 34 | 35 | def visit_callfunc(self, node): 36 | if self._is_form_class: 37 | ass_name = next(node.parent.get_children()) 38 | if isinstance(ass_name, AssName): 39 | field_name = ass_name.name 40 | val = safe_infer(node) 41 | if val is not None and val is not YES: 42 | if val.is_subtype_of('django.forms.fields.Field'): 43 | if field_name in self._form_field_names: 44 | self.add_message( 45 | 'form-field-redefinition', node=ass_name, 46 | args=(self._form_name, field_name)) 47 | else: 48 | self._form_field_names.add(field_name) 49 | -------------------------------------------------------------------------------- /django_linter/checkers/layout.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | from pylint.checkers import BaseChecker 5 | from pylint.interfaces import IAstroidChecker 6 | 7 | from ..__pkginfo__ import BASE_ID 8 | 9 | 10 | class LayoutChecker(BaseChecker): 11 | __implements__ = IAstroidChecker 12 | 13 | name = 'layout' 14 | msgs = { 15 | 'W%s01' % BASE_ID: ( 16 | 'Form %s not in forms module', 17 | 'forms-layout', 18 | 'Used when form class definition is not in forms module.'), 19 | 'W%s02' % BASE_ID: ( 20 | 'Admin class %s not in admin module', 21 | 'admin-layout', 22 | 'Used when admin class definition is not in admin module.'), 23 | } 24 | 25 | def leave_class(self, node): 26 | if node.is_subtype_of('django.forms.forms.BaseForm'): 27 | if not ('forms' in node.root().file): 28 | self.add_message('forms-layout', node=node, args=(node.name,)) 29 | elif node.is_subtype_of('django.contrib.admin.options.ModelAdmin'): 30 | if not ('admin' in node.root().file): 31 | self.add_message('admin-layout', node=node, args=(node.name,)) 32 | -------------------------------------------------------------------------------- /django_linter/checkers/misc.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | from pylint.checkers import BaseChecker 5 | from pylint.interfaces import IAstroidChecker 6 | from astroid import Name 7 | 8 | from ..__pkginfo__ import BASE_ID 9 | 10 | 11 | class MiscChecker(BaseChecker): 12 | __implements__ = IAstroidChecker 13 | 14 | name = 'misc' 15 | msgs = { 16 | 'W%s51' % BASE_ID: ( 17 | 'Print is used (consider using logger instead)', 18 | 'print-used', 19 | 'Used when there is print statement or function'), 20 | } 21 | 22 | def visit_callfunc(self, node): 23 | if isinstance(node.func, Name) and node.func.name == 'print': 24 | self.add_message('print-used', node=node) 25 | 26 | def visit_print(self, node): 27 | self.add_message('print-used', node=node) 28 | -------------------------------------------------------------------------------- /django_linter/checkers/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | from pylint.checkers import BaseChecker 5 | from pylint.interfaces import IAstroidChecker 6 | from pylint.checkers.utils import safe_infer 7 | from astroid import YES, AssName, Keyword, Return, Getattr, Instance, Name 8 | 9 | from ..__pkginfo__ import BASE_ID 10 | 11 | 12 | class ModelsChecker(BaseChecker): 13 | __implements__ = IAstroidChecker 14 | 15 | name = 'models' 16 | msgs = { 17 | 'W%s41' % BASE_ID: ( 18 | 'Text field is nullable: %s', 19 | 'nullable-text-field', 20 | 'Used when text field has null=True.'), 21 | 'W%s42' % BASE_ID: ( 22 | 'Money related field is float: %s', 23 | 'float-money-field', 24 | 'Used when money related field uses FloatField.'), 25 | 'W%s43' % BASE_ID: ( 26 | 'Possible use of naive datetime, consider using "auto_now"', 27 | 'naive-datetime-used', 28 | 'Used when there is datetime.now is used.'), 29 | 'W%s44' % BASE_ID: ( 30 | 'Related field is named with _id suffix', 31 | 'related-field-named-with-id', 32 | 'Used when related field is named with _id suffix'), 33 | 'W%s45' % BASE_ID: ( 34 | 'Unicode method is absent in model "%s"', 35 | 'unicode-method-absent', 36 | 'Used when model has no unicode method.'), 37 | 'W%s46' % BASE_ID: ( 38 | 'Unicode method should always return unicode', 39 | 'unicode-method-return', 40 | 'Used when unicode method does not return unicode.'), 41 | 'W%s47' % BASE_ID: ( 42 | 'Model field redefinition: %s.%s', 43 | 'model-field-redefinition', 44 | 'Used when there are more than one model field with ' 45 | 'the same name.'), 46 | 'W%s48' % BASE_ID: ( 47 | 'get_absolute_url defined without using reverse (%s)', 48 | 'get-absolute-url-without-reverse', 49 | 'Used when get_absolute_url method is defined without using ' 50 | 'reverse function.'), 51 | } 52 | 53 | _is_model_class = False 54 | _has_unicode_method = False 55 | _is_get_absolute_url = False 56 | _is_reverse_used_in_get_absolute_url = False 57 | _text_fields = {'CharField', 'TextField', 'SlugField'} 58 | 59 | @staticmethod 60 | def _is_money_field(field_name): 61 | return 'price' in field_name 62 | 63 | @classmethod 64 | def _is_text_field(cls, klass): 65 | return any(klass.is_subtype_of('django.db.models.fields.' + text_field) 66 | for text_field in cls._text_fields) 67 | 68 | @classmethod 69 | def _is_text_class(cls, klass): 70 | return (klass.is_subtype_of('__builtin__.unicode') 71 | or cls._is_text_field(klass)) 72 | 73 | def visit_classdef(self, node): 74 | self._is_model_class = bool( 75 | node.is_subtype_of('django.db.models.base.Model')) 76 | if self._is_model_class: 77 | self._model_field_names = set() 78 | self._model_name = node.name 79 | 80 | def leave_classdef(self, node): 81 | if self._is_model_class and not self._has_unicode_method: 82 | self.add_message('unicode-method-absent', args=node.name, node=node) 83 | 84 | self._is_model_class = False 85 | self._has_unicode_method = False 86 | 87 | def visit_functiondef(self, node): 88 | if self._is_model_class: 89 | if node.name == '__unicode__': 90 | self._has_unicode_method = True 91 | for stmt in node.body: 92 | if isinstance(stmt, Return): 93 | 94 | val = safe_infer(stmt.value) 95 | if (val and isinstance(val, Instance) and 96 | not self._is_text_class(val._proxied)): 97 | self.add_message('unicode-method-return', node=stmt) 98 | 99 | elif isinstance(stmt.value, Getattr): 100 | getattr_ = stmt.value 101 | if getattr_.expr.name == 'self': 102 | if getattr_.attrname == 'id': 103 | self.add_message( 104 | 'unicode-method-return', node=stmt) 105 | elif node.name == 'get_absolute_url': 106 | self._is_get_absolute_url = True 107 | 108 | def leave_functiondef(self, node): 109 | if (self._is_get_absolute_url and 110 | not self._is_reverse_used_in_get_absolute_url): 111 | self.add_message('get-absolute-url-without-reverse', 112 | node=node, args=(self._model_name,)) 113 | self._is_reverse_used_in_get_absolute_url = False 114 | self._is_get_absolute_url = False 115 | 116 | def visit_call(self, node): 117 | if (self._is_get_absolute_url and isinstance(node.func, Name) and 118 | node.func.name == 'reverse'): 119 | self._is_reverse_used_in_get_absolute_url = True 120 | 121 | if self._is_model_class: 122 | ass_name = next(node.parent.get_children()) 123 | if isinstance(ass_name, AssName): 124 | field_name = ass_name.name 125 | val = safe_infer(node) 126 | if val is not None and val is not YES: 127 | if val.is_subtype_of('django.db.models.fields.Field'): 128 | if field_name in self._model_field_names: 129 | self.add_message( 130 | 'model-field-redefinition', node=ass_name, 131 | args=(self._model_name, field_name)) 132 | else: 133 | self._model_field_names.add(field_name) 134 | 135 | if val.name in self._text_fields: 136 | for arg in node.keywords or []: 137 | if (arg.arg == 'null' and arg.value.value): 138 | self.add_message( 139 | 'nullable-text-field', args=field_name, 140 | node=arg.value) 141 | elif val.name == 'DateTimeField': 142 | for arg in node.keywords or []: 143 | if (arg.arg == 'default' and 144 | 'datetime.now' in arg.value.as_string()): 145 | self.add_message( 146 | 'naive-datetime-used', node=arg.value) 147 | elif val.is_subtype_of( 148 | 'django.db.models.fields.related.RelatedField'): 149 | if field_name.endswith('_id'): 150 | self.add_message('related-field-named-with-id', 151 | node=ass_name) 152 | if self._is_money_field(field_name) and ( 153 | val.name == 'FloatField'): 154 | self.add_message( 155 | 'float-money-field', args=field_name, 156 | node=ass_name) 157 | -------------------------------------------------------------------------------- /django_linter/checkers/settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | from pylint.checkers import BaseChecker 5 | from pylint.checkers.utils import safe_infer 6 | from pylint.interfaces import IAstroidChecker 7 | from astroid import YES, List 8 | 9 | from ..__pkginfo__ import BASE_ID 10 | 11 | 12 | class SettingsShecker(BaseChecker): 13 | __implements__ = IAstroidChecker 14 | 15 | name = 'settings' 16 | msgs = { 17 | 'E%s21' % BASE_ID: ( 18 | 'Required setting "%s" is missed', 19 | 'required-setting-missed', 20 | 'Used when required setting missed in settings file.'), 21 | 'E%s22' % BASE_ID: ( 22 | 'Empty setting "%s"', 23 | 'empty-setting', 24 | 'Used when setting is empty value.'), 25 | 'W%s21' % BASE_ID: ( 26 | 'Improper settings import', 27 | 'improper-settings-import', 28 | 'Used when settings is not imported from django.conf'), 29 | } 30 | 31 | _REQUIRED_SETTINGS = ('STATIC_ROOT', 'ALLOWED_HOSTS') 32 | 33 | @staticmethod 34 | def _is_settings_module(node): 35 | if node.name.rsplit('.', 1)[-1] == 'settings': 36 | return True 37 | return False 38 | 39 | def visit_import(self, node): 40 | if ('settings' in node.as_string() and 41 | 'django.conf' not in node.as_string()): 42 | self.add_message('improper-settings-import', node=node) 43 | 44 | def visit_from(self, node): 45 | if node.modname.rsplit('.', 1)[-1] == 'settings' or ( 46 | 'settings' in dict(node.names) and 47 | 'django.conf' not in node.modname): 48 | self.add_message('improper-settings-import', node=node) 49 | 50 | def leave_module(self, node): 51 | if self._is_settings_module(node): 52 | module_locals = node.locals 53 | for setting_name in self._REQUIRED_SETTINGS: 54 | if setting_name not in module_locals: 55 | self.add_message( 56 | 'required-setting-missed', args=setting_name, node=node) 57 | else: 58 | setting = module_locals[setting_name][-1] 59 | val = safe_infer(setting) 60 | if val is not None and val is not YES: 61 | if isinstance(val, List): 62 | is_empty = not val.elts 63 | else: 64 | is_empty = not val.value 65 | if is_empty: 66 | self.add_message('empty-setting', args=setting_name, 67 | node=setting) 68 | -------------------------------------------------------------------------------- /django_linter/checkers/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | from pylint.checkers import BaseChecker 5 | from pylint.interfaces import IAstroidChecker 6 | from astroid import Name, CallFunc, AssName, Getattr, Tuple, Subscript 7 | 8 | from ..__pkginfo__ import BASE_ID 9 | 10 | 11 | class ViewsChecker(BaseChecker): 12 | __implements__ = IAstroidChecker 13 | 14 | name = 'views' 15 | msgs = { 16 | 'W%s31' % BASE_ID: ( 17 | 'is_authenticated is not called', 18 | 'is-authenticated-not-called', 19 | 'Used when is_authenticated method is not called'), 20 | 'W%s32' % BASE_ID: ( 21 | 'objects.get is used without catching DoesNotExist', 22 | 'objects-get-without-doesnotexist', 23 | 'Used when Model.objects.get is used without enclosing it ' 24 | 'in try-except block to catch DoesNotExist exception.'), 25 | 'W%s33' % BASE_ID: ( 26 | 'Fetching model objects only for getting len', 27 | 'fetching-db-objects-len', 28 | 'Used when there is db query that fetches objects from ' 29 | 'database only to check the number of returned objects.'), 30 | 'W%s34' % BASE_ID: ( 31 | 'Accessing raw GET or POST data, consider using forms', 32 | 'raw-get-post-access', 33 | 'Used when request.GET or request.POST dicts is accessed ' 34 | 'directly, it is better to use forms.'), 35 | } 36 | 37 | _is_view_function = False 38 | _is_view_class = False 39 | _is_inside_try_except = False 40 | _try_except_node = None 41 | _is_len = False 42 | 43 | @staticmethod 44 | def _is_does_not_exist(node): 45 | if (isinstance(node, (Name, Getattr)) and 46 | 'DoesNotExist' in node.as_string()): 47 | return True 48 | return False 49 | 50 | @staticmethod 51 | def _is_getattr_or_name(node, name): 52 | if ((isinstance(node, Name) and node.name == name) or 53 | (isinstance(node, Getattr) and node.attrname == name)): 54 | return True 55 | return False 56 | 57 | def visit_attribute(self, node): 58 | parent = node.parent 59 | expr = node.expr 60 | if self._is_getattr_or_name(expr, 'user'): 61 | if (node.attrname == 'is_authenticated' and 62 | not isinstance(parent, CallFunc)): 63 | self.add_message('is-authenticated-not-called', node=node) 64 | elif self._is_getattr_or_name(expr, 'request'): 65 | if node.attrname in ('GET', 'POST'): 66 | if (isinstance(parent, Subscript) or 67 | isinstance(parent, Getattr) and 68 | parent.attrname == 'get'): 69 | self.add_message('raw-get-post-access', node=node) 70 | elif isinstance(parent, Getattr) and node.attrname == 'objects': 71 | if parent.attrname == 'get': 72 | if self._is_view_function or self._is_view_class: 73 | if not self._is_inside_try_except: 74 | self.add_message( 75 | 'objects-get-without-doesnotexist', node=node) 76 | else: 77 | for h in self._try_except_node.handlers: 78 | if self._is_does_not_exist(h.type): 79 | break 80 | elif isinstance(h.type, Tuple): 81 | _does_not_exist_found = False 82 | for exc_cls in h.type.elts: 83 | if self._is_does_not_exist(exc_cls): 84 | _does_not_exist_found = True 85 | break 86 | if _does_not_exist_found: 87 | break 88 | else: 89 | self.add_message( 90 | 'objects-get-without-doesnotexist', node=node) 91 | elif parent.attrname in ('all', 'filter', 'exclude'): 92 | if self._is_len: 93 | self.add_message('fetching-db-objects-len', node=node) 94 | 95 | def visit_functiondef(self, node): 96 | if 'views' in node.root().file: 97 | args = node.args.args 98 | if (args and isinstance(args[0], AssName) and 99 | args[0].name == 'request'): 100 | self._is_view_function = True 101 | 102 | def leave_functiondef(self, node): 103 | self._is_view_function = False 104 | 105 | def visit_tryexcept(self, node): 106 | self._is_inside_try_except = True 107 | self._try_except_node = node 108 | 109 | def leave_tryexcept(self, node): 110 | self._is_inside_try_except = False 111 | self._try_except_node = None 112 | 113 | def visit_call(self, node): 114 | if isinstance(node.func, Name) and node.func.name == 'len': 115 | self._is_len = True 116 | 117 | def leave_call(self, node): 118 | self._is_len = False 119 | 120 | def visit_classdef(self, node): 121 | if node.is_subtype_of('django.views.generic.base.View'): 122 | self._is_view_class = True 123 | 124 | def leave_classdef(self, node): 125 | self._is_view_class = False 126 | -------------------------------------------------------------------------------- /django_linter/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | from argparse import ArgumentParser 4 | 5 | 6 | from pylint import lint, reporters 7 | 8 | from . import register 9 | 10 | 11 | def main(): 12 | arg_parser = ArgumentParser( 13 | description='Simple extension for pylint to check django projects for ' 14 | 'common mistakes.') 15 | arg_parser.add_argument('targets', metavar='TARGET', nargs='+', 16 | help='python package or module') 17 | 18 | args = arg_parser.parse_args() 19 | 20 | linter = lint.PyLinter() 21 | reporters.initialize(linter) 22 | linter._load_reporter() 23 | register(linter) 24 | 25 | with lint.fix_import_path(args.targets): 26 | linter.check(args.targets) 27 | 28 | return linter.msg_status 29 | 30 | 31 | if __name__ == '__main__': 32 | main() 33 | -------------------------------------------------------------------------------- /django_linter/suppressers/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | from pylint.checkers.classes import ClassChecker 5 | from pylint.checkers.base import DocStringChecker 6 | from pylint.checkers.design_analysis import MisdesignChecker 7 | from pylint.checkers.newstyle import NewStyleConflictChecker 8 | 9 | from .suppress import suppress_msgs, is_meta_class 10 | 11 | 12 | def register(linter): 13 | if 'good-names' in linter._all_options: 14 | linter.global_set_option('good-names', ','.join([ 15 | # default 16 | 'i', 'j', 'k', 'ex', 'Run', '_', 17 | # django specific 18 | 'urlpatterns', 'qs', 'id', 19 | # misc 20 | 'logger', 21 | ])) 22 | 23 | suppress_msgs(DocStringChecker, 'visit_classdef', is_meta_class, 24 | 'missing-docstring') 25 | suppress_msgs(MisdesignChecker, 'leave_classdef', is_meta_class, 26 | 'too-few-public-methods') 27 | suppress_msgs(NewStyleConflictChecker, 'visit_classdef', is_meta_class, 28 | 'old-style-class') 29 | suppress_msgs(ClassChecker, 'visit_classdef', is_meta_class, 'no-init') 30 | -------------------------------------------------------------------------------- /django_linter/suppressers/suppress.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | from pylint.interfaces import UNDEFINED 5 | 6 | 7 | def suppress_msgs(checker, method_name, test_func, *msg_ids): 8 | 9 | def patched_add_message(self, msg_id, line=None, node=None, args=None, 10 | confidence=UNDEFINED): 11 | if not (test_func(node) and msg_id in msg_ids): 12 | try: 13 | checker.linter.add_message(msg_id, line, node, args, confidence) 14 | except AttributeError: # when checker is turned off 15 | pass 16 | 17 | old_method = getattr(checker, method_name) 18 | 19 | def patched_method(*args, **kwargs): 20 | old_add_message = checker.add_message 21 | checker.add_message = patched_add_message 22 | old_method(*args, **kwargs) 23 | checker.add_message = old_add_message 24 | 25 | setattr(checker, method_name, patched_method) 26 | 27 | 28 | def is_meta_class(node): 29 | if 'meta' in node.name.lower(): 30 | return True 31 | return False 32 | -------------------------------------------------------------------------------- /django_linter/transformers/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | from astroid import MANAGER, Class, CallFunc 5 | 6 | from .models import transform_model_class 7 | from .testing import transform_test_response 8 | from .factories import transform_factory_return 9 | 10 | 11 | def register(linter): 12 | MANAGER.register_transform(Class, transform_model_class) 13 | MANAGER.register_transform(Class, transform_test_response) 14 | MANAGER.register_transform(CallFunc, transform_factory_return) 15 | -------------------------------------------------------------------------------- /django_linter/transformers/factories.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | from pylint.checkers.utils import safe_infer 5 | from astroid import MANAGER, Class, Name, Instance, YES 6 | from astroid.builder import AstroidBuilder 7 | 8 | 9 | try: 10 | DjangoModel = AstroidBuilder(MANAGER).string_build(""" 11 | from django.db import models 12 | class Model(models.Model): 13 | id = models.AutoField()""").lookup('Model')[1][0] 14 | except IndexError: 15 | DjangoModel = None 16 | 17 | 18 | def transform_factory_return(node): 19 | if (isinstance(node.func, Name) and 20 | 'factory' in node.func._repr_name().lower()): 21 | val = safe_infer(node.func) 22 | if (isinstance(val, Class) and 23 | val.is_subtype_of('factory.django.DjangoModelFactory')): 24 | try: 25 | model = safe_infer(val.locals['Meta'][0].locals['model'][0]) 26 | except (KeyError, IndexError): 27 | pass 28 | else: 29 | if model is not None and model is not YES: 30 | if isinstance(model, Class): 31 | def infer_call_result(self, caller, context=None): 32 | yield Instance(model) 33 | val.infer_call_result = infer_call_result 34 | elif DjangoModel is not None: 35 | def infer_call_result(self, caller, context=None): 36 | yield Instance(DjangoModel) 37 | val.infer_call_result = infer_call_result 38 | -------------------------------------------------------------------------------- /django_linter/transformers/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | from astroid import MANAGER, Class, Instance, Function, Arguments, Pass 5 | 6 | 7 | def transform_model_class(cls): 8 | if cls.is_subtype_of('django.db.models.base.Model'): 9 | core_exceptions = MANAGER.ast_from_module_name('django.core.exceptions') 10 | # add DoesNotExist exception 11 | DoesNotExist = Class('DoesNotExist', None) 12 | DoesNotExist.bases = core_exceptions.lookup('ObjectDoesNotExist')[1] 13 | cls.locals['DoesNotExist'] = [DoesNotExist] 14 | # add MultipleObjectsReturned exception 15 | MultipleObjectsReturned = Class('MultipleObjectsReturned', None) 16 | MultipleObjectsReturned.bases = core_exceptions.lookup( 17 | 'MultipleObjectsReturned')[1] 18 | cls.locals['MultipleObjectsReturned'] = [MultipleObjectsReturned] 19 | # add objects manager 20 | if 'objects' not in cls.locals: 21 | try: 22 | Manager = MANAGER.ast_from_module_name( 23 | 'django.db.models.manager').lookup('Manager')[1][0] 24 | QuerySet = MANAGER.ast_from_module_name( 25 | 'django.db.models.query').lookup('QuerySet')[1][0] 26 | except IndexError: 27 | pass 28 | else: 29 | if isinstance(Manager.body[0], Pass): 30 | # for django >= 1.7 31 | for func_name, func_list in QuerySet.locals.items(): 32 | if (not func_name.startswith('_') and 33 | func_name not in Manager.locals): 34 | func = func_list[0] 35 | if (isinstance(func, Function) and 36 | 'queryset_only' not in func.instance_attrs): 37 | f = Function(func_name, None) 38 | f.args = Arguments() 39 | Manager.locals[func_name] = [f] 40 | cls.locals['objects'] = [Instance(Manager)] 41 | # add id field 42 | if 'id' not in cls.locals: 43 | try: 44 | AutoField = MANAGER.ast_from_module_name( 45 | 'django.db.models.fields').lookup('AutoField')[1][0] 46 | except IndexError: 47 | pass 48 | else: 49 | cls.locals['id'] = [Instance(AutoField)] 50 | 51 | -------------------------------------------------------------------------------- /django_linter/transformers/testing.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | from astroid import MANAGER 5 | from astroid.builder import AstroidBuilder 6 | 7 | BASE_REQUEST_DEFINITION = """ 8 | from django.http import HttpResponse, HttpRequest 9 | 10 | def request(self, *args, **kwargs): 11 | resp = HttpResponse() 12 | resp.client = self 13 | resp.content = '' 14 | resp.context = {} 15 | resp.request = HttpRequest() 16 | resp.templates = [] 17 | %s 18 | return resp 19 | """ 20 | DJANGO_REQUEST_DEFINITION = BASE_REQUEST_DEFINITION % '' 21 | DRF_REQUEST_DEFINITION = BASE_REQUEST_DEFINITION % 'resp.data = {}' 22 | DJANGO_CLIENT_REQUEST = AstroidBuilder( 23 | MANAGER).string_build(DJANGO_REQUEST_DEFINITION).locals['request'] 24 | DRF_CLIENT_REQUEST = AstroidBuilder( 25 | MANAGER).string_build(DRF_REQUEST_DEFINITION).locals['request'] 26 | HTTP_METHODS = ('get', 'post', 'put', 'head', 'delete', 'options') 27 | 28 | 29 | def transform_test_response(cls): 30 | if cls.is_subtype_of('django.test.client.Client'): 31 | for method in HTTP_METHODS: 32 | cls.locals[method] = DJANGO_CLIENT_REQUEST 33 | elif cls.is_subtype_of('rest_framework.test.APIClient'): 34 | for method in HTTP_METHODS: 35 | cls.locals[method] = DRF_CLIENT_REQUEST 36 | -------------------------------------------------------------------------------- /generate_readme.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | import os 4 | import subprocess 5 | 6 | from django_linter.checkers.settings import SettingsShecker 7 | from django_linter.checkers.models import ModelsChecker 8 | from django_linter.checkers.misc import MiscChecker 9 | from django_linter.checkers.layout import LayoutChecker 10 | from django_linter.checkers.forms import FormsChecker 11 | from django_linter.checkers.views import ViewsChecker 12 | 13 | 14 | def main(): 15 | out = open('README.rst', 'w') 16 | print("""Django linter 17 | ============= 18 | 19 | .. image:: https://travis-ci.org/geerk/django_linter.svg?branch=master 20 | :target: https://travis-ci.org/geerk/django_linter 21 | 22 | This is a simple extension for pylint that aims to check some common mistakes in django projects. 23 | 24 | Contributions are welcome. 25 | 26 | Installation 27 | ------------ 28 | 29 | :: 30 | 31 | pip install django_linter 32 | 33 | Usage 34 | ----- 35 | 36 | It can be used as a plugin or standalone script. To use it as a plugin it should be installed first, then run with pylint: 37 | 38 | :: 39 | 40 | pylint --load-plugins=django_linter TARGET 41 | 42 | To use it as a standalone script: 43 | 44 | """, file=out) 45 | print('::', file=out) 46 | print('', file=out) 47 | usage = os.tmpfile() 48 | p = subprocess.Popen( 49 | ['python', '-m', 'django_linter.main', '-h'], stdout=usage) 50 | p.wait() 51 | usage.seek(0) 52 | for line in usage: 53 | if line != '\n': 54 | out.write(' ' + line.replace('main.py', 'django-linter')) 55 | else: 56 | out.write(line) 57 | print('', file=out) 58 | print('Implemented checks', file=out) 59 | print('------------------', file=out) 60 | print('', file=out) 61 | for checker in (SettingsShecker, ModelsChecker, FormsChecker, ViewsChecker, 62 | LayoutChecker, MiscChecker): 63 | print('**%s:**' % checker.name.title(), file=out) 64 | print('', file=out) 65 | for k in sorted(checker.msgs.viewkeys()): 66 | print('- %s (%s): %s' % (k, checker.msgs[k][1], checker.msgs[k][2]), 67 | file=out) 68 | print('', file=out) 69 | print("""Implemented suppressers 70 | ----------------------- 71 | 72 | - "Meta" classes 73 | - urlpatterns 74 | - logger 75 | 76 | Implemented transformers 77 | ------------------------ 78 | 79 | **Models** 80 | 81 | - "id" field 82 | - "objects" manager 83 | - "DoesNotExist" exception 84 | - "MultipleObjectsReturned" exception 85 | 86 | **Testing** 87 | 88 | - test responses (django and DRF) 89 | 90 | **Factories** 91 | 92 | - factory-boy's factories (factory should return django model, but not always possible to infer model class) 93 | """, file=out) 94 | 95 | 96 | if __name__ == '__main__': 97 | main() 98 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pylint 2 | django 3 | djangorestframework 4 | factory-boy 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | setup( 6 | name='django_linter', 7 | version='0.7', 8 | packages=find_packages(exclude=['tests*']), 9 | description='Linter for django projects', 10 | long_description=open('README.rst').read(), 11 | author='Timofey Trukhanov', 12 | author_email='timofey.trukhanov@gmail.com', 13 | license='MIT', 14 | url='https://github.com/geerk/django_linter', 15 | install_requires=('pylint==1.5.1',), 16 | entry_points={ 17 | 'console_scripts': ['django-linter = django_linter.main:main']}, 18 | classifiers=[ 19 | 'Development Status :: 4 - Beta', 20 | 'Framework :: Django :: 1.8', 21 | 'Framework :: Django :: 1.9', 22 | 'Intended Audience :: Developers', 23 | 'License :: OSI Approved :: MIT License', 24 | 'Programming Language :: Python :: Implementation :: CPython', 25 | 'Programming Language :: Python :: 2', 26 | 'Programming Language :: Python :: 2.7', 27 | 'Programming Language :: Python :: 3', 28 | 'Programming Language :: Python :: 3.5', 29 | 'Operating System :: OS Independent']) 30 | -------------------------------------------------------------------------------- /tests/input/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geerk/django_linter/d5a27004acc9c7caf8663a8fb519060f210c81ec/tests/input/__init__.py -------------------------------------------------------------------------------- /tests/input/func_forms_form_field_redefinition.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check for form field redefinition 3 | """ 4 | from django import forms 5 | 6 | 7 | class Form0(forms.Form): 8 | name = forms.CharField(max_length=22) 9 | age = forms.IntegerField() 10 | name = forms.CharField() 11 | -------------------------------------------------------------------------------- /tests/input/func_misc_print_used_py34.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check using print 3 | """ 4 | print('test') 5 | -------------------------------------------------------------------------------- /tests/input/func_misc_print_used_py_27.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check using print 3 | """ 4 | print 'test' 5 | print('test') 6 | -------------------------------------------------------------------------------- /tests/input/func_models_float_money_field.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check for correct type for money related field 3 | """ 4 | from django.db import models 5 | 6 | 7 | class Product(models.Model): 8 | name = models.CharField(max_length=255) 9 | price = models.FloatField() 10 | 11 | def __unicode__(self): 12 | return self.name 13 | -------------------------------------------------------------------------------- /tests/input/func_models_get_absolute_url_without_reverse.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check for calling reverse in get_absolute_url method 3 | """ 4 | from django.db import models 5 | from django.core.urlresolvers import reverse 6 | 7 | 8 | class Category(models.Model): 9 | name = models.CharField() 10 | 11 | def __unicode__(self): 12 | return self.name 13 | 14 | def get_absolute_url(self): 15 | return reverse('product_detail', args=(self.name,)) 16 | 17 | 18 | class Product(models.Model): 19 | name = models.CharField(max_length=255) 20 | 21 | def __unicode__(self): 22 | return self.name 23 | 24 | def get_absolute_url(self): 25 | return '/products/%s' % self.name 26 | -------------------------------------------------------------------------------- /tests/input/func_models_model_field_redefinition.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check for model field redefinition 3 | """ 4 | from django.db import models 5 | 6 | 7 | class Category(models.Model): 8 | name = models.CharField() 9 | name = models.CharField() 10 | 11 | def __unicode__(self): 12 | return self.name 13 | -------------------------------------------------------------------------------- /tests/input/func_models_naive_datetime_used.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check use of datetime.now instead of auto_now 3 | """ 4 | from datetime import datetime 5 | from django.db import models 6 | 7 | 8 | class Product(models.Model): 9 | name = models.CharField(max_length=255) 10 | modified = models.DateTimeField(default=datetime.now) 11 | 12 | def __unicode__(self): 13 | return self.name 14 | -------------------------------------------------------------------------------- /tests/input/func_models_nullable_text_field.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check for nullable text fields in models 3 | """ 4 | from django.db import models 5 | 6 | 7 | class Product(models.Model): 8 | name = models.CharField(max_length=255) 9 | description = models.TextField(null=True) 10 | 11 | class Meta: 12 | verbose_name = 'product' 13 | 14 | def __unicode__(self): 15 | return self.name 16 | -------------------------------------------------------------------------------- /tests/input/func_models_related_field_name_with_id.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check that related fields do not contain _id prefix 3 | """ 4 | from django.db import models 5 | 6 | 7 | class Category(models.Model): 8 | name = models.CharField() 9 | 10 | def __unicode__(self): 11 | return self.name 12 | 13 | 14 | class Product(models.Model): 15 | name = models.CharField(max_length=255) 16 | category_id = models.ForeignKey(Category) 17 | 18 | def __unicode__(self): 19 | return self.name 20 | -------------------------------------------------------------------------------- /tests/input/func_models_unicode_method_absent.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check for presense of __unicode__ method in models 3 | """ 4 | from django.db import models 5 | 6 | 7 | class Product(models.Model): 8 | name = models.CharField(max_length=255) 9 | -------------------------------------------------------------------------------- /tests/input/func_models_unicode_method_return.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check that __unicode__ method returns unicode 3 | """ 4 | from django.db import models 5 | 6 | 7 | class Category(models.Model): 8 | name = models.CharField() 9 | 10 | def __unicode__(self): 11 | return self.id 12 | 13 | 14 | class Product(models.Model): 15 | name = models.CharField(max_length=255) 16 | price = models.DecimalField() 17 | 18 | def __unicode__(self): 19 | return self.price 20 | -------------------------------------------------------------------------------- /tests/input/func_settings_improper_settings_import.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check settings imports 3 | """ 4 | 5 | from django.conf import settings 6 | import test_project.settings 7 | from test_project import settings 8 | -------------------------------------------------------------------------------- /tests/input/func_suppressers_good_names.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check for django specific variable names 3 | """ 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | urlpatterns = [] 9 | 10 | foobar = 0 # added to check that bad names are checked also 11 | -------------------------------------------------------------------------------- /tests/input/func_transformers_factories.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check transforms for factories 3 | """ 4 | import unittest 5 | import factory 6 | from django.db import models 7 | from .models import Category 8 | 9 | 10 | class Product(models.Model): 11 | name = models.CharField(max_length=33) 12 | 13 | def __unicode__(self): 14 | return self.name 15 | 16 | 17 | class ProductFactory(factory.DjangoModelFactory): 18 | 19 | class Meta: 20 | model = Product 21 | 22 | 23 | 24 | class ProductFactoryTestCase(unittest.TestCase): 25 | 26 | def test_product_factory(self): 27 | product = ProductFactory() 28 | self.assertEqual(product.id, 1) 29 | self.assertEqual(product.name, 'test_name') 30 | self.assertEqual(product.foobar, 'foobar') 31 | 32 | 33 | class CategoryFactory(factory.DjangoModelFactory): 34 | 35 | class Meta: 36 | model = Category 37 | 38 | 39 | class CategoryFactoryTestCase(unittest.TestCase): 40 | 41 | def test_category_factory(self): 42 | category = CategoryFactory() 43 | self.assertEqual(category.id, 1) 44 | self.assertEqual(category.foobar, 'foobar') 45 | -------------------------------------------------------------------------------- /tests/input/func_transformers_testing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check transforms for testing 3 | """ 4 | from django.test import TestCase 5 | from rest_framework.test import APIClient 6 | 7 | 8 | class HomePageTestCase(TestCase): 9 | 10 | def test_home_page(self): 11 | resp = self.client.get('/') 12 | self.assertEqual(resp.status_code, 200) 13 | self.assertContains(resp, 'Home') 14 | 15 | 16 | class HomeAPITestCase(TestCase): 17 | 18 | def setUp(self): 19 | self.client = APIClient() 20 | 21 | def test_home_api(self): 22 | resp = self.client.get('/') 23 | self.assertEqual(resp.data, {'content': 'HOME'}) 24 | -------------------------------------------------------------------------------- /tests/input/func_views_fetching_db_objects_len.py: -------------------------------------------------------------------------------- 1 | """Check for uneffecient query to get number of objects using len""" 2 | from django.shortcuts import render 3 | from django.http import HttpResponseForbidden 4 | 5 | from .models import Product, Category 6 | 7 | 8 | def product_list_view(request): 9 | if request.is_authenticated(): 10 | ctx = {'products': Product.objects.all(), 11 | 'categories_count': len(Category.objects.all())} 12 | return render(request, 'product_list.html', context=ctx) 13 | else: 14 | return HttpResponseForbidden() 15 | -------------------------------------------------------------------------------- /tests/input/func_views_is_authenticated_not_called.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check for calling is_authenticated instead of using it as attribute 3 | """ 4 | from django.views.generic import UpdateView 5 | 6 | 7 | def product_create(request): 8 | if request.user.is_authenticated: 9 | # create user 10 | pass 11 | 12 | 13 | class ProductUpdateView(UpdateView): 14 | 15 | def post(self, *args, **kwargs): 16 | if self.request.user.is_authenticated: 17 | # update user 18 | pass 19 | -------------------------------------------------------------------------------- /tests/input/func_views_objects_get_without_doesnotexist.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check calling objects.get inside try-except 3 | """ 4 | from __future__ import (absolute_import, division, 5 | print_function, unicode_literals) 6 | from django.shortcuts import render 7 | from django.http import HttpResponseForbidden, HttpResponseNotFound 8 | from django.core.exceptions import MultipleObjectsReturned 9 | from django.views.generic import TemplateView 10 | 11 | from .models import Product, Category 12 | 13 | 14 | def product_list_view(request, cat_id): 15 | if request.is_authenticated(): 16 | ctx = {'products': Product.objects.all(), 17 | 'cat_id': cat_id} 18 | return render(request, 'product_list.html', context=ctx) 19 | else: 20 | return HttpResponseForbidden() 21 | 22 | 23 | def product_detail_view(request, slug): 24 | if request.is_authenticated: 25 | try: 26 | product = Product.objects.get(slug=slug) 27 | except MultipleObjectsReturned: 28 | return HttpResponseNotFound() 29 | return render( 30 | request, 'product_detail.html', context={'product': product}) 31 | else: 32 | return HttpResponseForbidden() 33 | 34 | 35 | def category_detail_view(request, **kwargs): 36 | try: 37 | category = Category.objects.get(pk=kwargs['pk']) 38 | except Category.DoesNotExist: 39 | return HttpResponseNotFound() 40 | return render( 41 | request, 'category_detail.html', context={'category': category}) 42 | 43 | 44 | class IndexView(TemplateView): 45 | 46 | def get_context_data(self, **kwargs): 47 | ctx = super(IndexView, self).get_context_data(**kwargs) 48 | ctx['cat'] = Category.objects.get(pk=kwargs['cat_id']) 49 | # check this not raises exception 50 | self._manager = Category.objects # pylint: disable=W0201 51 | return ctx 52 | -------------------------------------------------------------------------------- /tests/input/func_views_raw_get_post_access.py: -------------------------------------------------------------------------------- 1 | """Chec for raw access GET and POST dicts""" 2 | from django.shortcuts import render 3 | from django.http import HttpResponseForbidden 4 | from django.views.generic import TemplateView 5 | 6 | from .models import Product, Category 7 | 8 | 9 | def product_list_view(request): 10 | if request.is_authenticated(): 11 | ctx = {'products': Product.objects.all(), 12 | 'cat_id': request.GET['cat_id']} 13 | return render(request, 'product_list.html', context=ctx) 14 | else: 15 | return HttpResponseForbidden() 16 | 17 | 18 | class IndexView(TemplateView): 19 | 20 | def get_context_data(self, **kwargs): 21 | ctx = super(IndexView, self).get_context_data(**kwargs) 22 | try: 23 | ctx['cat'] = Category.objects.get(pk=self.request.GET.get('cat_id')) 24 | except Category.DoesNotExist: 25 | ctx['cat'] = None 26 | return ctx 27 | -------------------------------------------------------------------------------- /tests/input/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Product(models.Model): 5 | pass 6 | 7 | 8 | class Category(models.Model): 9 | pass 10 | -------------------------------------------------------------------------------- /tests/messages/func_forms_form_field_redefinition.txt: -------------------------------------------------------------------------------- 1 | W: 10:Form0: Form field redefinition: Form0.name 2 | -------------------------------------------------------------------------------- /tests/messages/func_misc_print_used_py34.txt: -------------------------------------------------------------------------------- 1 | W: 4: Print is used (consider using logger instead) 2 | -------------------------------------------------------------------------------- /tests/messages/func_misc_print_used_py_27.txt: -------------------------------------------------------------------------------- 1 | C: 5: Unnecessary parens after 'print' keyword 2 | W: 4: Print is used (consider using logger instead) 3 | W: 5: Print is used (consider using logger instead) 4 | -------------------------------------------------------------------------------- /tests/messages/func_models_float_money_field.txt: -------------------------------------------------------------------------------- 1 | W: 9:Product: Money related field is float: price 2 | -------------------------------------------------------------------------------- /tests/messages/func_models_get_absolute_url_without_reverse.txt: -------------------------------------------------------------------------------- 1 | W: 24:Product.get_absolute_url: get_absolute_url defined without using reverse (Product) 2 | -------------------------------------------------------------------------------- /tests/messages/func_models_model_field_redefinition.txt: -------------------------------------------------------------------------------- 1 | W: 9:Category: Model field redefinition: Category.name 2 | -------------------------------------------------------------------------------- /tests/messages/func_models_naive_datetime_used.txt: -------------------------------------------------------------------------------- 1 | W: 10:Product: Possible use of naive datetime, consider using "auto_now" 2 | -------------------------------------------------------------------------------- /tests/messages/func_models_nullable_text_field.txt: -------------------------------------------------------------------------------- 1 | W: 9:Product: Text field is nullable: description 2 | -------------------------------------------------------------------------------- /tests/messages/func_models_related_field_name_with_id.txt: -------------------------------------------------------------------------------- 1 | W: 16:Product: Related field is named with _id suffix 2 | -------------------------------------------------------------------------------- /tests/messages/func_models_unicode_method_absent.txt: -------------------------------------------------------------------------------- 1 | W: 7:Product: Unicode method is absent in model "Product" 2 | -------------------------------------------------------------------------------- /tests/messages/func_models_unicode_method_return.txt: -------------------------------------------------------------------------------- 1 | W: 11:Category.__unicode__: Unicode method should always return unicode 2 | W: 19:Product.__unicode__: Unicode method should always return unicode 3 | -------------------------------------------------------------------------------- /tests/messages/func_settings_improper_settings_import.txt: -------------------------------------------------------------------------------- 1 | E: 6: Unable to import 'test_project.settings' 2 | E: 7: Unable to import 'test_project' 3 | W: 5: Unused settings imported from django.conf 4 | W: 6: Improper settings import 5 | W: 6: Unused import test_project.settings 6 | W: 7: Improper settings import 7 | W: 7: Reimport 'settings' (imported line 6) 8 | -------------------------------------------------------------------------------- /tests/messages/func_suppressers_good_names.txt: -------------------------------------------------------------------------------- 1 | C: 10: Invalid constant name "foobar" 2 | -------------------------------------------------------------------------------- /tests/messages/func_transformers_factories.txt: -------------------------------------------------------------------------------- 1 | E: 30:ProductFactoryTestCase.test_product_factory: Instance of 'Product' has no 'foobar' member 2 | E: 44:CategoryFactoryTestCase.test_category_factory: Instance of 'Category' has no 'foobar' member 3 | -------------------------------------------------------------------------------- /tests/messages/func_transformers_testing.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geerk/django_linter/d5a27004acc9c7caf8663a8fb519060f210c81ec/tests/messages/func_transformers_testing.txt -------------------------------------------------------------------------------- /tests/messages/func_views_fetching_db_objects_len.txt: -------------------------------------------------------------------------------- 1 | W: 11:product_list_view: Fetching model objects only for getting len 2 | -------------------------------------------------------------------------------- /tests/messages/func_views_is_authenticated_not_called.txt: -------------------------------------------------------------------------------- 1 | W: 8:product_create: is_authenticated is not called 2 | W: 16:ProductUpdateView.post: is_authenticated is not called 3 | -------------------------------------------------------------------------------- /tests/messages/func_views_objects_get_without_doesnotexist.txt: -------------------------------------------------------------------------------- 1 | W: 26:product_detail_view: objects.get is used without catching DoesNotExist 2 | W: 48:IndexView.get_context_data: objects.get is used without catching DoesNotExist 3 | -------------------------------------------------------------------------------- /tests/messages/func_views_raw_get_post_access.txt: -------------------------------------------------------------------------------- 1 | W: 12:product_list_view: Accessing raw GET or POST data, consider using forms 2 | W: 23:IndexView.get_context_data: Accessing raw GET or POST data, consider using forms 3 | -------------------------------------------------------------------------------- /tests/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geerk/django_linter/d5a27004acc9c7caf8663a8fb519060f210c81ec/tests/settings/__init__.py -------------------------------------------------------------------------------- /tests/settings/empty/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geerk/django_linter/d5a27004acc9c7caf8663a8fb519060f210c81ec/tests/settings/empty/__init__.py -------------------------------------------------------------------------------- /tests/settings/empty/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings file to test empty settings 3 | """ 4 | 5 | ALLOWED_HOSTS = [] 6 | STATIC_ROOT = '' 7 | -------------------------------------------------------------------------------- /tests/settings/required/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geerk/django_linter/d5a27004acc9c7caf8663a8fb519060f210c81ec/tests/settings/required/__init__.py -------------------------------------------------------------------------------- /tests/settings/required/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Empty settings to test required settings 3 | """ 4 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | import unittest 4 | import os 5 | 6 | from pylint.testutils import make_tests, LintTestUsingFile, cb_test_gen, linter 7 | 8 | 9 | def suite(): 10 | input_dir = os.path.join(os.path.dirname(__file__), 'input') 11 | msg_dir = os.path.join(os.path.dirname(__file__), 'messages') 12 | linter.load_plugin_modules(['django_linter']) 13 | linter.set_option('disable', 'R0901,C0111') 14 | return unittest.TestSuite([ 15 | unittest.makeSuite(test, suiteClass=unittest.TestSuite) 16 | for test in make_tests( 17 | input_dir, msg_dir, None, [cb_test_gen(LintTestUsingFile)])]) 18 | 19 | 20 | def load_tests(loader, tests, pattern): 21 | return suite() 22 | 23 | 24 | if __name__ == '__main__': 25 | unittest.main(defaultTest='suite') 26 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | import sys 4 | from unittest import TestCase 5 | from os.path import abspath, dirname, join 6 | 7 | from pylint.testutils import linter 8 | 9 | 10 | class SettingsTestCase(TestCase): 11 | 12 | @classmethod 13 | def setUpClass(cls): 14 | DATA = join(dirname(abspath(__file__)), 'settings') 15 | sys.path.insert(1, DATA) 16 | 17 | def setUp(self): 18 | linter.reporter.finalize() 19 | 20 | def test_required_setting_missed(self): 21 | linter.check('settings.required.settings') 22 | got = linter.reporter.finalize().strip() 23 | self.assertEqual( 24 | got, """E: 1: Required setting "ALLOWED_HOSTS" is missed 25 | E: 1: Required setting "STATIC_ROOT" is missed""") 26 | 27 | def test_empty_settings(self): 28 | linter.check('settings.empty.settings') 29 | got = linter.reporter.finalize().strip() 30 | self.assertEqual( 31 | got, '''E: 5: Empty setting "ALLOWED_HOSTS" 32 | E: 6: Empty setting "STATIC_ROOT"''') 33 | --------------------------------------------------------------------------------