diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 00000000..b83317ae --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,87 @@ +name: testing + +on: + push: + branches: master + pull_request: + +jobs: + sanity: + name: sanity / ${{ matrix.toxenv }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [3.6] + toxenv: [django_not_installed, django_is_installed, flake8, pylint, readme] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Execute tests + run: | + pip install tox + pip install -e .[for_tests] + + export TOXENV=${{ matrix.toxenv }} + export PYTHON=${{ matrix.python-version }} + tox + + + test: + name: test / Django@${{ matrix.django-version }} / Python@${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + django-version: [-main, 3.2, 3.1, "3.0", "2.0", 1.11] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Execute tests + run: | + pip install tox + pip install -e .[for_tests] + + export DJANGO=${{ matrix.django-version }} + export PYTHON=${{ matrix.python-version }} + export TOXENV=$(echo py${{ matrix.python-version }}-django${{ matrix.django-version }} | tr -d .) + tox + + - name: Coveralls + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + pip install coveralls + coveralls --service=github + + build_and_package_sanity: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Build + run: | + pip install tox + pip install -e .[for_tests] + + ./scripts/build.sh diff --git a/.gitignore b/.gitignore index abac1ee1..e365f2e6 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ nosetests.xml .project .pydevproject .env +.idea +env.txt +.venv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..a26d288f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +# https://pre-commit.com/ +repos: + - repo: git://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: mixed-line-ending + args: [--fix=lf] + - id: debug-statements + # code formatting + - repo: https://gitlab.com/pycqa/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + args: [ --max-line-length=140 ] + - repo: https://github.com/python/black + rev: 21.9b0 + hooks: + - id: black + args: [--safe, --line-length=140] + - repo: https://github.com/PyCQA/isort + rev: 5.10.1 + hooks: + - id: isort + args: ['--profile', 'black'] diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index dda7b45e..00000000 --- a/.travis.yml +++ /dev/null @@ -1,57 +0,0 @@ -dist: xenial -language: python -python: - - 3.6 - - 3.7 - - 3.8 - - 3.9 -env: - # note: latest versions first b/c the top-most is included in new - # build stages if not specified - - DJANGO=3.0 - - DJANGO=2.2 -stages: - - django_not_installed - - django_is_installed - - test - - build_and_package_sanity -matrix: - include: - - { stage: django_not_installed, python: 3.6, env: TOXENV=django_not_installed } - - { stage: django_is_installed, python: 3.6, env: TOXENV=django_is_installed } - - { stage: test, python: 3.9, env: DJANGO=3.2 } - - { stage: test, python: 3.8, env: DJANGO=3.1 } - - { stage: test, python: 3.6, env: DJANGO=3.0 } - - { stage: test, python: 3.6, env: DJANGO=2.0 } - - { stage: test, python: 3.6, env: DJANGO=1.11 } - - { stage: test, python: 3.6, env: DJANGO=master } - - { stage: test, python: 3.6, env: TOXENV=flake8 } - - { stage: test, python: 3.6, env: TOXENV=pylint } - - { stage: test, python: 3.6, env: TOXENV=readme } - - { stage: build_and_package_sanity, python: 3.6, env: SANITY_CHECK=1 } - -before_install: - - git clone --depth 1 https://github.com/PyCQA/pylint.git --branch 2.4 --single-branch ~/pylint - -install: - - pip install tox-travis - - pip install -e .[for_tests] -script: - - | - if [ -z "$SANITY_CHECK" ]; then - tox - else - ./scripts/build.sh - fi - -after_success: - - | - if [ -z "$SANITY_CHECK" ] && [ -z "$TOXENV" ]; then - pip install coveralls - coveralls - fi - -notifications: - email: - on_failure: change - on_success: never diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c755297e..43879331 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Changelog ========= +Version 2.5 [WIP] +--------------------------- +- Moved from Travis CI to GitHub Actions (`#366 `_ and `#340 `_) +- Added pre-commit configuration and began enforcing black/isort code formatting (TODO: add github action to enforce) +- Multiple test fixes (including `#338 `_) - newer versions of pylint expect a different format for the expected messages txt files. +- TODO: Bumped dependency for pylint-django-utils to get `multi-threaded pylint support `_ + Version 2.4.4 (26 Apr 2021) --------------------------- diff --git a/LICENSE b/LICENSE index 7730f89e..9c19d076 100644 --- a/LICENSE +++ b/LICENSE @@ -291,7 +291,7 @@ convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Pylint plugin for improving code analysis for when using Django - Copyright (C) 2013 + Copyright (C) 2013 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/README.rst b/README.rst index 6b58fe53..80df3db2 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,8 @@ pylint-django ============= -.. image:: https://travis-ci.org/PyCQA/pylint-django.svg?branch=master - :target: https://travis-ci.org/PyCQA/pylint-django - -.. image:: https://landscape.io/github/landscapeio/pylint-django/master/landscape.png - :target: https://landscape.io/github/landscapeio/pylint-django +.. image:: https://github.com/PyCQA/pylint-django/actions/workflows/testing.yml/badge.svg + :target: https://github.com/PyCQA/pylint-django/actions/workflows/testing.yml .. image:: https://coveralls.io/repos/PyCQA/pylint-django/badge.svg :target: https://coveralls.io/r/PyCQA/pylint-django diff --git a/pylint_django/__init__.py b/pylint_django/__init__.py index ccf92d7d..b0920575 100644 --- a/pylint_django/__init__.py +++ b/pylint_django/__init__.py @@ -5,9 +5,8 @@ from pylint_django import plugin -if sys.version_info < (3, ): - raise DeprecationWarning("Version 0.11.1 was the last to support Python 2. " - "Please migrate to Python 3!") +if sys.version_info < (3,): + raise DeprecationWarning("Version 0.11.1 was the last to support Python 2. " "Please migrate to Python 3!") register = plugin.register # pylint: disable=invalid-name load_configuration = plugin.load_configuration # pylint: disable=invalid-name diff --git a/pylint_django/augmentations/__init__.py b/pylint_django/augmentations/__init__.py index bf4d28d7..b5ec916b 100644 --- a/pylint_django/augmentations/__init__.py +++ b/pylint_django/augmentations/__init__.py @@ -4,30 +4,39 @@ import itertools from astroid import InferenceError +from astroid.nodes import Attribute, ClassDef, ImportFrom from astroid.objects import Super -from astroid.nodes import ClassDef, ImportFrom, Attribute -from astroid.scoped_nodes import ClassDef as ScopedClass, Module - +from astroid.scoped_nodes import ClassDef as ScopedClass +from astroid.scoped_nodes import Module +from django import VERSION as django_version +from django.utils import termcolors +from django.views.generic.base import ContextMixin, RedirectView, View +from django.views.generic.dates import ( + DateMixin, + DayMixin, + MonthMixin, + WeekMixin, + YearMixin, +) +from django.views.generic.detail import ( + SingleObjectMixin, + SingleObjectTemplateResponseMixin, + TemplateResponseMixin, +) +from django.views.generic.edit import DeletionMixin, FormMixin, ModelFormMixin +from django.views.generic.list import ( + MultipleObjectMixin, + MultipleObjectTemplateResponseMixin, +) from pylint.checkers.base import DocStringChecker, NameChecker -from pylint.checkers.design_analysis import MisdesignChecker from pylint.checkers.classes import ClassChecker +from pylint.checkers.design_analysis import MisdesignChecker from pylint.checkers.newstyle import NewStyleConflictChecker -from pylint.checkers.variables import VariablesChecker from pylint.checkers.typecheck import TypeChecker -from pylint.checkers.variables import ScopeConsumer - +from pylint.checkers.variables import ScopeConsumer, VariablesChecker from pylint_plugin_utils import augment_visit, suppress_message -from django import VERSION as django_version -from django.views.generic.base import View, RedirectView, ContextMixin -from django.views.generic.dates import DateMixin, DayMixin, MonthMixin, WeekMixin, YearMixin -from django.views.generic.detail import SingleObjectMixin, SingleObjectTemplateResponseMixin, TemplateResponseMixin -from django.views.generic.edit import DeletionMixin, FormMixin, ModelFormMixin -from django.views.generic.list import MultipleObjectMixin, MultipleObjectTemplateResponseMixin -from django.utils import termcolors - -from pylint_django.utils import node_is_subclass, PY3 - +from pylint_django.utils import PY3, node_is_subclass # Note: it would have been nice to import the Manager object from Django and # get its attributes that way - and this used to be the method - but unfortunately @@ -36,234 +45,234 @@ # Therefore we'll fall back on a hard-coded list of attributes which won't be as accurate, # but this is not 100% accurate anyway. MANAGER_ATTRS = { - 'none', - 'all', - 'count', - 'dates', - 'distinct', - 'extra', - 'get', - 'get_or_create', - 'update_or_create', - 'get_queryset', - 'create', - 'bulk_create', - 'filter', - 'aggregate', - 'annotate', - 'complex_filter', - 'exclude', - 'in_bulk', - 'iterator', - 'latest', - 'order_by', - 'select_for_update', - 'select_related', - 'prefetch_related', - 'values', - 'values_list', - 'update', - 'reverse', - 'defer', - 'only', - 'using', - 'exists', + "none", + "all", + "count", + "dates", + "distinct", + "extra", + "get", + "get_or_create", + "update_or_create", + "get_queryset", + "create", + "bulk_create", + "filter", + "aggregate", + "annotate", + "complex_filter", + "exclude", + "in_bulk", + "iterator", + "latest", + "order_by", + "select_for_update", + "select_related", + "prefetch_related", + "values", + "values_list", + "update", + "reverse", + "defer", + "only", + "using", + "exists", } QS_ATTRS = { - 'filter', - 'exclude', - 'annotate', - 'order_by', - 'reverse', - 'distinct', - 'values', - 'values_list', - 'dates', - 'datetimes', - 'none', - 'all', - 'select_related', - 'prefetch_related', - 'extra', - 'defer', - 'only', - 'using', - 'select_for_update', - 'raw', - 'get', - 'create', - 'get_or_create', - 'update_or_create', - 'bulk_create', - 'count', - 'in_bulk', - 'iterator', - 'latest', - 'earliest', - 'first', - 'last', - 'aggregate', - 'exists', - 'update', - 'delete', - 'as_manager', - 'expression', - 'output_field', + "filter", + "exclude", + "annotate", + "order_by", + "reverse", + "distinct", + "values", + "values_list", + "dates", + "datetimes", + "none", + "all", + "select_related", + "prefetch_related", + "extra", + "defer", + "only", + "using", + "select_for_update", + "raw", + "get", + "create", + "get_or_create", + "update_or_create", + "bulk_create", + "count", + "in_bulk", + "iterator", + "latest", + "earliest", + "first", + "last", + "aggregate", + "exists", + "update", + "delete", + "as_manager", + "expression", + "output_field", } MODELADMIN_ATTRS = { # options - 'actions', - 'actions_on_top', - 'actions_on_bottom', - 'actions_selection_counter', - 'date_hierarchy', - 'empty_value_display', - 'exclude', - 'fields', - 'fieldsets', - 'filter_horizontal', - 'filter_vertical', - 'form', - 'formfield_overrides', - 'inlines', - 'list_display', - 'list_display_links', - 'list_editable', - 'list_filter', - 'list_max_show_all', - 'list_per_page', - 'list_select_related', - 'ordering', - 'paginator', - 'prepopulated_fields', - 'preserve_filters', - 'radio_fields', - 'raw_id_fields', - 'readonly_fields', - 'save_as', - 'save_on_top', - 'search_fields', - 'show_full_result_count', - 'view_on_site', + "actions", + "actions_on_top", + "actions_on_bottom", + "actions_selection_counter", + "date_hierarchy", + "empty_value_display", + "exclude", + "fields", + "fieldsets", + "filter_horizontal", + "filter_vertical", + "form", + "formfield_overrides", + "inlines", + "list_display", + "list_display_links", + "list_editable", + "list_filter", + "list_max_show_all", + "list_per_page", + "list_select_related", + "ordering", + "paginator", + "prepopulated_fields", + "preserve_filters", + "radio_fields", + "raw_id_fields", + "readonly_fields", + "save_as", + "save_on_top", + "search_fields", + "show_full_result_count", + "view_on_site", # template options - 'add_form_template', - 'change_form_template', - 'change_list_template', - 'delete_confirmation_template', - 'delete_selected_confirmation_template', - 'object_history_template', + "add_form_template", + "change_form_template", + "change_list_template", + "delete_confirmation_template", + "delete_selected_confirmation_template", + "object_history_template", } MODEL_ATTRS = { - 'id', - 'DoesNotExist', - 'MultipleObjectsReturned', - '_base_manager', - '_default_manager', - '_meta', - 'delete', - 'get_next_by_date', - 'get_previous_by_date', - 'objects', - 'save', + "id", + "DoesNotExist", + "MultipleObjectsReturned", + "_base_manager", + "_default_manager", + "_meta", + "delete", + "get_next_by_date", + "get_previous_by_date", + "objects", + "save", } FIELD_ATTRS = { - 'null', - 'blank', - 'choices', - 'db_column', - 'db_index', - 'db_tablespace', - 'default', - 'editable', - 'error_messages', - 'help_text', - 'primary_key', - 'unique', - 'unique_for_date', - 'unique_for_month', - 'unique_for_year', - 'verbose_name', - 'validators', + "null", + "blank", + "choices", + "db_column", + "db_index", + "db_tablespace", + "default", + "editable", + "error_messages", + "help_text", + "primary_key", + "unique", + "unique_for_date", + "unique_for_month", + "unique_for_year", + "verbose_name", + "validators", } CHAR_FIELD_ATTRS = { - 'max_length', + "max_length", } DATE_FIELD_ATTRS = { - 'auto_now', - 'auto_now_add', + "auto_now", + "auto_now_add", } DECIMAL_FIELD_ATTRS = { - 'max_digits', - 'decimal_places', + "max_digits", + "decimal_places", } FILE_FIELD_ATTRS = { - 'upload_to', - 'storage', + "upload_to", + "storage", } IMAGE_FIELD_ATTRS = { - 'height_field', - 'width_field', + "height_field", + "width_field", } IP_FIELD_ATTRS = { - 'protocol', - 'unpack_ipv4', + "protocol", + "unpack_ipv4", } SLUG_FIELD_ATTRS = { - 'allow_unicode', + "allow_unicode", } FOREIGNKEY_FIELD_ATTRS = { - 'limit_choices_to', - 'related_name', - 'related_query_name', - 'to_field', - 'db_constraint', - 'swappable', + "limit_choices_to", + "related_name", + "related_query_name", + "to_field", + "db_constraint", + "swappable", } MANYTOMANY_FIELD_ATTRS = { - 'add', - 'clear', - 'related_name', - 'related_query_name', - 'remove', - 'set', - 'limit_choices_to', - 'symmetrical', - 'through', - 'through_fields', - 'db_table', - 'db_constraint', - 'swappable', + "add", + "clear", + "related_name", + "related_query_name", + "remove", + "set", + "limit_choices_to", + "symmetrical", + "through", + "through_fields", + "db_table", + "db_constraint", + "swappable", } ONETOONE_FIELD_ATTRS = { - 'parent_link', + "parent_link", } @@ -273,22 +282,34 @@ VIEW_ATTRS = { ( ( - '{}.{}'.format(cls.__module__, cls.__name__), - '.{}'.format(cls.__name__) + f"{cls.__module__}.{cls.__name__}", + f".{cls.__name__}", ), - tuple(cls.__dict__.keys()) - ) for cls in ( - View, RedirectView, ContextMixin, - DateMixin, DayMixin, MonthMixin, WeekMixin, YearMixin, - SingleObjectMixin, SingleObjectTemplateResponseMixin, TemplateResponseMixin, - DeletionMixin, FormMixin, ModelFormMixin, - MultipleObjectMixin, MultipleObjectTemplateResponseMixin, + tuple(cls.__dict__.keys()), + ) + for cls in ( + View, + RedirectView, + ContextMixin, + DateMixin, + DayMixin, + MonthMixin, + WeekMixin, + YearMixin, + SingleObjectMixin, + SingleObjectTemplateResponseMixin, + TemplateResponseMixin, + DeletionMixin, + FormMixin, + ModelFormMixin, + MultipleObjectMixin, + MultipleObjectTemplateResponseMixin, ) } FORM_ATTRS = { - 'declared_fields', + "declared_fields", } @@ -311,7 +332,7 @@ def ignore_import_warnings_for_related_fields(orig_method, self, node): iterat = consumer.to_consume.items if PY3 else consumer.to_consume.iteritems for name, stmts in iterat(): if isinstance(stmts[0], ImportFrom): - if any(n[0] in ('ForeignKey', 'OneToOneField') for n in stmts[0].names): + if any(n[0] in ("ForeignKey", "OneToOneField") for n in stmts[0].names): continue new_things[name] = stmts @@ -344,14 +365,14 @@ class ModelB(models.Model): """ quack = False - if node.attrname in MANAGER_ATTRS or node.attrname.endswith('_set'): + if node.attrname in MANAGER_ATTRS or node.attrname.endswith("_set"): # if this is a X_set method, that's a pretty strong signal that this is the default # Django name, rather than one set by related_name quack = True else: # we will if isinstance(node.parent, Attribute): - func_name = getattr(node.parent, 'attrname', None) + func_name = getattr(node.parent, "attrname", None) if func_name in MANAGER_ATTRS: quack = True @@ -364,11 +385,13 @@ class ModelB(models.Model): pass else: for cls in inferred_cls: - if (node_is_subclass(cls, - 'django.db.models.manager.Manager', - 'django.db.models.base.Model', - '.Model', - 'django.db.models.fields.related.ForeignObject')): + if node_is_subclass( + cls, + "django.db.models.manager.Manager", + "django.db.models.base.Model", + ".Model", + "django.db.models.fields.related.ForeignObject", + ): # This means that we are looking at a subclass of models.Model # and something is trying to access a _set attribute. # Since this could exist, we will return so as not to raise an @@ -378,54 +401,58 @@ class ModelB(models.Model): def foreign_key_ids(chain, node): - if node.attrname.endswith('_id'): + if node.attrname.endswith("_id"): return chain() def is_model_admin_subclass(node): """Checks that node is derivative of ModelAdmin class.""" - if node.name[-5:] != 'Admin' or isinstance(node.parent, ClassDef): + if node.name[-5:] != "Admin" or isinstance(node.parent, ClassDef): return False - return node_is_subclass(node, 'django.contrib.admin.options.ModelAdmin') + return node_is_subclass(node, "django.contrib.admin.options.ModelAdmin") def is_model_media_subclass(node): """Checks that node is derivative of Media class.""" - if node.name != 'Media' or not isinstance(node.parent, ClassDef): + if node.name != "Media" or not isinstance(node.parent, ClassDef): return False - parents = ('django.contrib.admin.options.ModelAdmin', - 'django.forms.widgets.Media', - 'django.db.models.base.Model', - '.Model', # for the transformed version used in this plugin - 'django.forms.forms.Form', - '.Form', - 'django.forms.widgets.Widget', - '.Widget', - 'django.forms.models.ModelForm', - '.ModelForm') + parents = ( + "django.contrib.admin.options.ModelAdmin", + "django.forms.widgets.Media", + "django.db.models.base.Model", + ".Model", # for the transformed version used in this plugin + "django.forms.forms.Form", + ".Form", + "django.forms.widgets.Widget", + ".Widget", + "django.forms.models.ModelForm", + ".ModelForm", + ) return node_is_subclass(node.parent, *parents) def is_model_meta_subclass(node): """Checks that node is derivative of Meta class.""" - if node.name != 'Meta' or not isinstance(node.parent, ClassDef): + if node.name != "Meta" or not isinstance(node.parent, ClassDef): return False - parents = ('.Model', # for the transformed version used here - 'django.db.models.base.Model', - '.Form', - 'django.forms.forms.Form', - '.ModelForm', - 'django.forms.models.ModelForm', - 'rest_framework.serializers.BaseSerializer', - 'rest_framework.generics.GenericAPIView', - 'rest_framework.viewsets.ReadOnlyModelViewSet', - 'rest_framework.viewsets.ModelViewSet', - 'django_filters.filterset.FilterSet', - 'factory.django.DjangoModelFactory',) + parents = ( + ".Model", # for the transformed version used here + "django.db.models.base.Model", + ".Form", + "django.forms.forms.Form", + ".ModelForm", + "django.forms.models.ModelForm", + "rest_framework.serializers.BaseSerializer", + "rest_framework.generics.GenericAPIView", + "rest_framework.viewsets.ReadOnlyModelViewSet", + "rest_framework.viewsets.ModelViewSet", + "django_filters.filterset.FilterSet", + "factory.django.DjangoModelFactory", + ) return node_is_subclass(node.parent, *parents) @@ -436,9 +463,11 @@ def is_model_factory(node): except: # noqa: E722, pylint: disable=bare-except return False - parents = ('factory.declarations.LazyFunction', - 'factory.declarations.SubFactory', - 'factory.django.DjangoModelFactory') + parents = ( + "factory.declarations.LazyFunction", + "factory.declarations.SubFactory", + "factory.django.DjangoModelFactory", + ) for parent_class in parent_classes: try: @@ -464,7 +493,7 @@ def is_factory_post_generation_method(node): continue for target in inferred: - if target.qname() == 'factory.helpers.post_generation': + if target.qname() == "factory.helpers.post_generation": return True return False @@ -472,15 +501,17 @@ def is_factory_post_generation_method(node): def is_model_mpttmeta_subclass(node): """Checks that node is derivative of MPTTMeta class.""" - if node.name != 'MPTTMeta' or not isinstance(node.parent, ClassDef): + if node.name != "MPTTMeta" or not isinstance(node.parent, ClassDef): return False - parents = ('django.db.models.base.Model', - '.Model', # for the transformed version used in this plugin - 'django.forms.forms.Form', - '.Form', - 'django.forms.models.ModelForm', - '.ModelForm') + parents = ( + "django.db.models.base.Model", + ".Model", # for the transformed version used in this plugin + "django.forms.forms.Form", + ".Form", + "django.forms.models.ModelForm", + ".ModelForm", + ) return node_is_subclass(node.parent, *parents) @@ -503,129 +534,123 @@ def _attribute_is_magic(node, attrs, parents): def is_style_attribute(node): - parents = ('django.core.management.color.Style', ) + parents = ("django.core.management.color.Style",) return _attribute_is_magic(node, STYLE_ATTRS, parents) def is_manager_attribute(node): """Checks that node is attribute of Manager or QuerySet class.""" - parents = ('django.db.models.manager.Manager', - '.Manager', - 'factory.base.BaseFactory.build', - 'django.db.models.query.QuerySet', - '.QuerySet') + parents = ( + "django.db.models.manager.Manager", + ".Manager", + "factory.base.BaseFactory.build", + "django.db.models.query.QuerySet", + ".QuerySet", + ) return _attribute_is_magic(node, MANAGER_ATTRS.union(QS_ATTRS), parents) def is_admin_attribute(node): """Checks that node is attribute of BaseModelAdmin.""" - parents = ('django.contrib.admin.options.BaseModelAdmin', - '.BaseModelAdmin') + parents = ("django.contrib.admin.options.BaseModelAdmin", ".BaseModelAdmin") return _attribute_is_magic(node, MODELADMIN_ATTRS, parents) def is_model_attribute(node): """Checks that node is attribute of Model.""" - parents = ('django.db.models.base.Model', - '.Model') + parents = ("django.db.models.base.Model", ".Model") return _attribute_is_magic(node, MODEL_ATTRS, parents) def is_field_attribute(node): """Checks that node is attribute of Field.""" - parents = ('django.db.models.fields.Field', - '.Field') + parents = ("django.db.models.fields.Field", ".Field") return _attribute_is_magic(node, FIELD_ATTRS, parents) def is_charfield_attribute(node): """Checks that node is attribute of CharField.""" - parents = ('django.db.models.fields.CharField', - '.CharField') + parents = ("django.db.models.fields.CharField", ".CharField") return _attribute_is_magic(node, CHAR_FIELD_ATTRS, parents) def is_datefield_attribute(node): """Checks that node is attribute of DateField.""" - parents = ('django.db.models.fields.DateField', - '.DateField') + parents = ("django.db.models.fields.DateField", ".DateField") return _attribute_is_magic(node, DATE_FIELD_ATTRS, parents) def is_decimalfield_attribute(node): """Checks that node is attribute of DecimalField.""" - parents = ('django.db.models.fields.DecimalField', - '.DecimalField') + parents = ("django.db.models.fields.DecimalField", ".DecimalField") return _attribute_is_magic(node, DECIMAL_FIELD_ATTRS, parents) def is_filefield_attribute(node): """Checks that node is attribute of FileField.""" - parents = ('django.db.models.fields.files.FileField', - '.FileField') + parents = ("django.db.models.fields.files.FileField", ".FileField") return _attribute_is_magic(node, FILE_FIELD_ATTRS, parents) def is_imagefield_attribute(node): """Checks that node is attribute of ImageField.""" - parents = ('django.db.models.fields.files.ImageField', - '.ImageField') + parents = ("django.db.models.fields.files.ImageField", ".ImageField") return _attribute_is_magic(node, IMAGE_FIELD_ATTRS, parents) def is_ipfield_attribute(node): """Checks that node is attribute of GenericIPAddressField.""" - parents = ('django.db.models.fields.GenericIPAddressField', - '.GenericIPAddressField') + parents = ( + "django.db.models.fields.GenericIPAddressField", + ".GenericIPAddressField", + ) return _attribute_is_magic(node, IP_FIELD_ATTRS, parents) def is_slugfield_attribute(node): """Checks that node is attribute of SlugField.""" - parents = ('django.db.models.fields.SlugField', - '.SlugField') + parents = ("django.db.models.fields.SlugField", ".SlugField") return _attribute_is_magic(node, SLUG_FIELD_ATTRS, parents) def is_foreignkeyfield_attribute(node): """Checks that node is attribute of ForeignKey.""" - parents = ('django.db.models.fields.related.ForeignKey', - '.ForeignKey') + parents = ("django.db.models.fields.related.ForeignKey", ".ForeignKey") return _attribute_is_magic(node, FOREIGNKEY_FIELD_ATTRS, parents) def is_manytomanyfield_attribute(node): """Checks that node is attribute of ManyToManyField.""" - parents = ('django.db.models.fields.related.ManyToManyField', - '.ManyToManyField') + parents = ("django.db.models.fields.related.ManyToManyField", ".ManyToManyField") return _attribute_is_magic(node, MANYTOMANY_FIELD_ATTRS, parents) def is_onetoonefield_attribute(node): """Checks that node is attribute of OneToOneField.""" - parents = ('django.db.models.fields.related.OneToOneField', - '.OneToOneField') + parents = ("django.db.models.fields.related.OneToOneField", ".OneToOneField") return _attribute_is_magic(node, ONETOONE_FIELD_ATTRS, parents) def is_form_attribute(node): """Checks that node is attribute of Form.""" - parents = ('django.forms.forms.Form', 'django.forms.models.ModelForm') + parents = ("django.forms.forms.Form", "django.forms.models.ModelForm") return _attribute_is_magic(node, FORM_ATTRS, parents) def is_model_test_case_subclass(node): """Checks that node is derivative of TestCase class.""" - if not node.name.endswith('Test') and not isinstance(node.parent, ClassDef): + if not node.name.endswith("Test") and not isinstance(node.parent, ClassDef): return False - return node_is_subclass(node, 'django.test.testcases.TestCase') + return node_is_subclass(node, "django.test.testcases.TestCase") def generic_is_view_attribute(parents, attrs): """Generates is_X_attribute function for given parents and attrs.""" + def is_attribute(node): return _attribute_is_magic(node, attrs, parents) + return is_attribute @@ -638,9 +663,11 @@ def is_model_view_subclass_method_shouldnt_be_function(node): while parent and not isinstance(parent, ScopedClass): parent = parent.parent - subclass = ('django.views.View', - 'django.views.generic.View', - 'django.views.generic.base.View',) + subclass = ( + "django.views.View", + "django.views.generic.View", + "django.views.generic.base.View", + ) return parent is not None and node_is_subclass(parent, *subclass) @@ -652,7 +679,7 @@ def ignore_unused_argument_warnings_for_request(orig_method, self, stmt, name): The signature of Django view functions require the request argument but it is okay if the request is not used. This function should be used as a wrapper for the `VariablesChecker._is_name_ignored` method. """ - if name in ('request', 'args', 'kwargs'): + if name in ("request", "args", "kwargs"): return True return orig_method(self, stmt, name) @@ -660,9 +687,9 @@ def ignore_unused_argument_warnings_for_request(orig_method, self, stmt, name): def is_model_field_display_method(node): """Accept model's fields with get_*_display names.""" - if not node.attrname.endswith('_display'): + if not node.attrname.endswith("_display"): return False - if not node.attrname.startswith('get_'): + if not node.attrname.startswith("get_"): return False if node.last_child(): @@ -670,7 +697,7 @@ def is_model_field_display_method(node): # blindly accepting get_*_display try: for cls in node.last_child().inferred(): - if node_is_subclass(cls, 'django.db.models.base.Model', '.Model'): + if node_is_subclass(cls, "django.db.models.base.Model", ".Model"): return True except InferenceError: return False @@ -679,7 +706,7 @@ def is_model_field_display_method(node): def is_model_media_valid_attributes(node): """Suppress warnings for valid attributes of Media class.""" - if node.name not in ('js', ): + if node.name not in ("js",): return False parent = node.parent @@ -694,7 +721,7 @@ def is_model_media_valid_attributes(node): def is_templatetags_module_valid_constant(node): """Suppress warnings for valid constants in templatetags module.""" - if node.name not in ('register', ): + if node.name not in ("register",): return False parent = node.parent @@ -709,14 +736,14 @@ def is_templatetags_module_valid_constant(node): def is_urls_module_valid_constant(node): """Suppress warnings for valid constants in urls module.""" - if node.name not in ('urlpatterns', 'app_name'): + if node.name not in ("urlpatterns", "app_name"): return False parent = node.parent while not isinstance(parent, Module): parent = parent.parent - if not parent.name.endswith('urls'): + if not parent.name.endswith("urls"): return False return True @@ -724,7 +751,7 @@ def is_urls_module_valid_constant(node): def allow_meta_protected_access(node): if django_version >= (1, 8): - return node.attrname == '_meta' + return node.attrname == "_meta" return False @@ -738,21 +765,36 @@ def wrap(orig_method, with_method): @functools.wraps(orig_method) def wrap_func(*args, **kwargs): return with_method(orig_method, *args, **kwargs) + return wrap_func def is_wsgi_application(node): frame = node.frame() - return node.name == 'application' and isinstance(frame, Module) and \ - (frame.name == 'asgi' or frame.path[0].endswith('asgi.py') or frame.file.endswith('asgi.py') or - frame.name == 'wsgi' or frame.path[0].endswith('wsgi.py') or frame.file.endswith('wsgi.py')) + return ( + node.name == "application" + and isinstance(frame, Module) + and ( + frame.name == "asgi" + or frame.path[0].endswith("asgi.py") + or frame.file.endswith("asgi.py") + or frame.name == "wsgi" + or frame.path[0].endswith("wsgi.py") + or frame.file.endswith("wsgi.py") + ) + ) # Compat helpers def pylint_newstyle_classdef_compat(linter, warning_name, augment): - if not hasattr(NewStyleConflictChecker, 'visit_classdef'): + if not hasattr(NewStyleConflictChecker, "visit_classdef"): return - suppress_message(linter, getattr(NewStyleConflictChecker, 'visit_classdef'), warning_name, augment) + suppress_message( + linter, + getattr(NewStyleConflictChecker, "visit_classdef"), + warning_name, + augment, + ) def apply_wrapped_augmentations(): @@ -766,13 +808,13 @@ def apply_wrapped_augmentations(): # Unused argument 'request' (get, post) current_is_name_ignored = VariablesChecker._is_name_ignored # pylint: disable=protected-access - if current_is_name_ignored.__name__ == '_is_name_ignored': + if current_is_name_ignored.__name__ == "_is_name_ignored": # pylint: disable=protected-access VariablesChecker._is_name_ignored = wrap(current_is_name_ignored, ignore_unused_argument_warnings_for_request) # ForeignKey and OneToOneField current_leave_module = VariablesChecker.leave_module - if current_leave_module.__name__ == 'leave_module': + if current_leave_module.__name__ == "leave_module": # current_leave_module is not wrapped # Two threads may hit the next assignment concurrently, but the result is the same VariablesChecker.leave_module = wrap(current_leave_module, ignore_import_warnings_for_related_fields) @@ -785,56 +827,108 @@ def apply_augmentations(linter): """Apply augmentation and suppression rules.""" augment_visit(linter, TypeChecker.visit_attribute, foreign_key_sets) augment_visit(linter, TypeChecker.visit_attribute, foreign_key_ids) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_model_field_display_method) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_style_attribute) - suppress_message(linter, NameChecker.visit_assignname, 'invalid-name', is_urls_module_valid_constant) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_model_field_display_method) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_style_attribute) + suppress_message( + linter, + NameChecker.visit_assignname, + "invalid-name", + is_urls_module_valid_constant, + ) # supress errors when accessing magical class attributes - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_manager_attribute) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_admin_attribute) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_model_attribute) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_field_attribute) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_charfield_attribute) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_datefield_attribute) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_decimalfield_attribute) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_filefield_attribute) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_imagefield_attribute) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_ipfield_attribute) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_slugfield_attribute) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_foreignkeyfield_attribute) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_manytomanyfield_attribute) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_onetoonefield_attribute) - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_form_attribute) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_manager_attribute) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_admin_attribute) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_model_attribute) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_field_attribute) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_charfield_attribute) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_datefield_attribute) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_decimalfield_attribute) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_filefield_attribute) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_imagefield_attribute) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_ipfield_attribute) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_slugfield_attribute) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_foreignkeyfield_attribute) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_manytomanyfield_attribute) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_onetoonefield_attribute) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_form_attribute) for parents, attrs in VIEW_ATTRS: - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', generic_is_view_attribute(parents, attrs)) + suppress_message( + linter, + TypeChecker.visit_attribute, + "no-member", + generic_is_view_attribute(parents, attrs), + ) # formviews have too many ancestors, there's nothing the user of the library can do about that - suppress_message(linter, MisdesignChecker.visit_classdef, 'too-many-ancestors', - is_class('django.views.generic.edit.FormView')) + suppress_message( + linter, + MisdesignChecker.visit_classdef, + "too-many-ancestors", + is_class("django.views.generic.edit.FormView"), + ) # class-based generic views just have a longer inheritance chain - suppress_message(linter, MisdesignChecker.visit_classdef, 'too-many-ancestors', - is_class('django.views.generic.detail.BaseDetailView')) - suppress_message(linter, MisdesignChecker.visit_classdef, 'too-many-ancestors', - is_class('django.views.generic.edit.ProcessFormView')) + suppress_message( + linter, + MisdesignChecker.visit_classdef, + "too-many-ancestors", + is_class("django.views.generic.detail.BaseDetailView"), + ) + suppress_message( + linter, + MisdesignChecker.visit_classdef, + "too-many-ancestors", + is_class("django.views.generic.edit.ProcessFormView"), + ) # model forms have no __init__ method anywhere in their bases - suppress_message(linter, ClassChecker.visit_classdef, 'W0232', is_class('django.forms.models.ModelForm')) + suppress_message( + linter, + ClassChecker.visit_classdef, + "W0232", + is_class("django.forms.models.ModelForm"), + ) # Meta - suppress_message(linter, DocStringChecker.visit_classdef, 'missing-docstring', is_model_meta_subclass) - pylint_newstyle_classdef_compat(linter, 'old-style-class', is_model_meta_subclass) - suppress_message(linter, ClassChecker.visit_classdef, 'no-init', is_model_meta_subclass) - suppress_message(linter, MisdesignChecker.leave_classdef, 'too-few-public-methods', is_model_meta_subclass) - suppress_message(linter, ClassChecker.visit_attribute, 'protected-access', allow_meta_protected_access) + suppress_message( + linter, + DocStringChecker.visit_classdef, + "missing-docstring", + is_model_meta_subclass, + ) + pylint_newstyle_classdef_compat(linter, "old-style-class", is_model_meta_subclass) + suppress_message(linter, ClassChecker.visit_classdef, "no-init", is_model_meta_subclass) + suppress_message( + linter, + MisdesignChecker.leave_classdef, + "too-few-public-methods", + is_model_meta_subclass, + ) + suppress_message( + linter, + ClassChecker.visit_attribute, + "protected-access", + allow_meta_protected_access, + ) # Media - suppress_message(linter, NameChecker.visit_assignname, 'C0103', is_model_media_valid_attributes) - suppress_message(linter, DocStringChecker.visit_classdef, 'missing-docstring', is_model_media_subclass) - pylint_newstyle_classdef_compat(linter, 'old-style-class', is_model_media_subclass) - suppress_message(linter, ClassChecker.visit_classdef, 'no-init', is_model_media_subclass) - suppress_message(linter, MisdesignChecker.leave_classdef, 'too-few-public-methods', is_model_media_subclass) + suppress_message(linter, NameChecker.visit_assignname, "C0103", is_model_media_valid_attributes) + suppress_message( + linter, + DocStringChecker.visit_classdef, + "missing-docstring", + is_model_media_subclass, + ) + pylint_newstyle_classdef_compat(linter, "old-style-class", is_model_media_subclass) + suppress_message(linter, ClassChecker.visit_classdef, "no-init", is_model_media_subclass) + suppress_message( + linter, + MisdesignChecker.leave_classdef, + "too-few-public-methods", + is_model_media_subclass, + ) # Admin # Too many public methods (40+/20) @@ -844,27 +938,46 @@ def apply_augmentations(linter): # for method in node.methods(): # if not method.name.startswith('_'): # nb_public_methods += 1 - suppress_message(linter, MisdesignChecker.leave_classdef, 'R0904', is_model_admin_subclass) + suppress_message(linter, MisdesignChecker.leave_classdef, "R0904", is_model_admin_subclass) # Tests - suppress_message(linter, MisdesignChecker.leave_classdef, 'R0904', is_model_test_case_subclass) + suppress_message(linter, MisdesignChecker.leave_classdef, "R0904", is_model_test_case_subclass) # View # Method could be a function (get, post) - suppress_message(linter, ClassChecker.leave_functiondef, 'no-self-use', - is_model_view_subclass_method_shouldnt_be_function) + suppress_message( + linter, + ClassChecker.leave_functiondef, + "no-self-use", + is_model_view_subclass_method_shouldnt_be_function, + ) # django-mptt - suppress_message(linter, DocStringChecker.visit_classdef, 'missing-docstring', is_model_mpttmeta_subclass) - pylint_newstyle_classdef_compat(linter, 'old-style-class', is_model_mpttmeta_subclass) - suppress_message(linter, ClassChecker.visit_classdef, 'W0232', is_model_mpttmeta_subclass) - suppress_message(linter, MisdesignChecker.leave_classdef, 'too-few-public-methods', is_model_mpttmeta_subclass) + suppress_message( + linter, + DocStringChecker.visit_classdef, + "missing-docstring", + is_model_mpttmeta_subclass, + ) + pylint_newstyle_classdef_compat(linter, "old-style-class", is_model_mpttmeta_subclass) + suppress_message(linter, ClassChecker.visit_classdef, "W0232", is_model_mpttmeta_subclass) + suppress_message( + linter, + MisdesignChecker.leave_classdef, + "too-few-public-methods", + is_model_mpttmeta_subclass, + ) # factory_boy's DjangoModelFactory - suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_model_factory) - suppress_message(linter, ClassChecker.visit_functiondef, 'no-self-argument', is_factory_post_generation_method) + suppress_message(linter, TypeChecker.visit_attribute, "no-member", is_model_factory) + suppress_message( + linter, + ClassChecker.visit_functiondef, + "no-self-argument", + is_factory_post_generation_method, + ) # wsgi.py - suppress_message(linter, NameChecker.visit_assignname, 'invalid-name', is_wsgi_application) + suppress_message(linter, NameChecker.visit_assignname, "invalid-name", is_wsgi_application) apply_wrapped_augmentations() diff --git a/pylint_django/checkers/__init__.py b/pylint_django/checkers/__init__.py index b0a5bb00..1a14b746 100644 --- a/pylint_django/checkers/__init__.py +++ b/pylint_django/checkers/__init__.py @@ -1,10 +1,10 @@ """Checkers.""" -from pylint_django.checkers.django_installed import DjangoInstalledChecker -from pylint_django.checkers.models import ModelChecker -from pylint_django.checkers.json_response import JsonResponseChecker -from pylint_django.checkers.forms import FormChecker from pylint_django.checkers.auth_user import AuthUserChecker +from pylint_django.checkers.django_installed import DjangoInstalledChecker from pylint_django.checkers.foreign_key_strings import ForeignKeyStringsChecker +from pylint_django.checkers.forms import FormChecker +from pylint_django.checkers.json_response import JsonResponseChecker +from pylint_django.checkers.models import ModelChecker def register_checkers(linter): diff --git a/pylint_django/checkers/auth_user.py b/pylint_django/checkers/auth_user.py index 7e51fc5f..af6a6600 100644 --- a/pylint_django/checkers/auth_user.py +++ b/pylint_django/checkers/auth_user.py @@ -1,5 +1,4 @@ -from pylint import interfaces -from pylint import checkers +from pylint import checkers, interfaces from pylint.checkers import utils from pylint_django.__pkginfo__ import BASE_ID @@ -8,28 +7,32 @@ class AuthUserChecker(checkers.BaseChecker): __implements__ = (interfaces.IAstroidChecker,) - name = 'auth-user-checker' + name = "auth-user-checker" - msgs = {'E%d41' % BASE_ID: ("Hard-coded 'auth.User'", - 'hard-coded-auth-user', - "Don't hard-code the auth.User model. " - "Use settings.AUTH_USER_MODEL instead!"), - 'E%d42' % BASE_ID: ("User model imported from django.contrib.auth.models", - 'imported-auth-user', - "Don't import django.contrib.auth.models.User model. " - "Use django.contrib.auth.get_user_model() instead!")} + msgs = { + f"E{BASE_ID}41": ( + "Hard-coded 'auth.User'", + "hard-coded-auth-user", + "Don't hard-code the auth.User model. Use settings.AUTH_USER_MODEL instead!", + ), + f"E{BASE_ID}42": ( + "User model imported from django.contrib.auth.models", + "imported-auth-user", + "Don't import django.contrib.auth.models.User model. Use django.contrib.auth.get_user_model() instead!", + ), + } - @utils.check_messages('hard-coded-auth-user') + @utils.check_messages("hard-coded-auth-user") def visit_const(self, node): # for now we don't check if the parent is a ForeignKey field # because the user model should not be hard-coded anywhere - if node.value == 'auth.User': - self.add_message('hard-coded-auth-user', node=node) + if node.value == "auth.User": + self.add_message("hard-coded-auth-user", node=node) - @utils.check_messages('imported-auth-user') + @utils.check_messages("imported-auth-user") def visit_importfrom(self, node): - if node.modname == 'django.contrib.auth.models': + if node.modname == "django.contrib.auth.models": for imported_names in node.names: - if imported_names[0] in ['*', 'User']: - self.add_message('imported-auth-user', node=node) + if imported_names[0] in ["*", "User"]: + self.add_message("imported-auth-user", node=node) break diff --git a/pylint_django/checkers/django_installed.py b/pylint_django/checkers/django_installed.py index de79e4d6..2f580a9f 100644 --- a/pylint_django/checkers/django_installed.py +++ b/pylint_django/checkers/django_installed.py @@ -1,30 +1,35 @@ from __future__ import absolute_import + from pylint.checkers import BaseChecker from pylint.checkers.utils import check_messages + from pylint_django.__pkginfo__ import BASE_ID class DjangoInstalledChecker(BaseChecker): - name = 'django-installed-checker' + name = "django-installed-checker" msgs = { - 'F%s01' % BASE_ID: ("Django is not available on the PYTHONPATH", - 'django-not-available', - "Django could not be imported by the pylint-django plugin, so most Django related " - "improvements to pylint will fail."), - - 'W%s99' % BASE_ID: ('Placeholder message to prevent disabling of checker', - 'django-not-available-placeholder', - 'PyLint does not recognise checkers as being enabled unless they have at least' - ' one message which is not fatal...') + f"F{BASE_ID}01": ( + "Django is not available on the PYTHONPATH", + "django-not-available", + "Django could not be imported by the pylint-django plugin, so most Django related " + "improvements to pylint will fail.", + ), + f"W{BASE_ID}99": ( + "Placeholder message to prevent disabling of checker", + "django-not-available-placeholder", + "PyLint does not recognise checkers as being enabled unless they have at least" + " one message which is not fatal...", + ), } - @check_messages('django-not-available') + @check_messages("django-not-available") def open(self): try: - __import__('django') + __import__("django") except ImportError: # mild hack: this error is added before any modules have been inspected # so we need to set some kind of value to prevent the linter failing to it - self.linter.set_current_module('pylint_django') - self.add_message('django-not-available') + self.linter.set_current_module("pylint_django") + self.add_message("django-not-available") diff --git a/pylint_django/checkers/foreign_key_strings.py b/pylint_django/checkers/foreign_key_strings.py index febb0880..5e7e87d4 100644 --- a/pylint_django/checkers/foreign_key_strings.py +++ b/pylint_django/checkers/foreign_key_strings.py @@ -44,15 +44,13 @@ class ForeignKeyStringsChecker(BaseChecker): ) msgs = { - "E%s10" - % BASE_ID: ( + f"E{BASE_ID}10": ( "Django was not configured. For more information run " "pylint --load-plugins=pylint_django --help-msg=django-not-configured", "django-not-configured", _LONG_MESSAGE, ), - "F%s10" - % BASE_ID: ( + f"F{BASE_ID}10": ( "Provided Django settings %s could not be loaded", "django-settings-module-not-found", "The provided Django settings module %s was not found on the path", @@ -80,15 +78,22 @@ def open(self): # must wait until some module is inspected to be able to raise... so that # state is stashed in this property. - from django.core.exceptions import ( # pylint: disable=import-outside-toplevel - ImproperlyConfigured, - ) + try: + from django.core.exceptions import ( # pylint: disable=import-outside-toplevel + ImproperlyConfigured, + ) + except ModuleNotFoundError: + return try: import django # pylint: disable=import-outside-toplevel django.setup() - from django.apps import apps # noqa pylint: disable=import-outside-toplevel,unused-import + from django.apps import ( # noqa pylint: disable=import-outside-toplevel,unused-import + apps, + ) + + # flake8: noqa=F401, F403 except ImproperlyConfigured: # this means that Django wasn't able to configure itself using some defaults # provided (likely in a DJANGO_SETTINGS_MODULE environment variable) @@ -100,14 +105,15 @@ def open(self): from django.conf import ( # pylint: disable=import-outside-toplevel settings, ) + settings.configure() django.setup() else: # see if we can load the provided settings module try: from django.conf import ( # pylint: disable=import-outside-toplevel - settings, Settings, + settings, ) settings.configure(Settings(self.config.django_settings_module)) diff --git a/pylint_django/checkers/forms.py b/pylint_django/checkers/forms.py index 007504cb..8614b739 100644 --- a/pylint_django/checkers/forms.py +++ b/pylint_django/checkers/forms.py @@ -1,9 +1,8 @@ """Models.""" from astroid.nodes import Assign, AssignName, ClassDef - -from pylint.interfaces import IAstroidChecker -from pylint.checkers.utils import check_messages from pylint.checkers import BaseChecker +from pylint.checkers.utils import check_messages +from pylint.interfaces import IAstroidChecker from pylint_django.__pkginfo__ import BASE_ID from pylint_django.utils import node_is_subclass @@ -11,27 +10,29 @@ def _get_child_meta(node): for child in node.get_children(): - if isinstance(child, ClassDef) and child.name == 'Meta': + if isinstance(child, ClassDef) and child.name == "Meta": return child return None class FormChecker(BaseChecker): """Django model checker.""" + __implements__ = IAstroidChecker - name = 'django-form-checker' + name = "django-form-checker" msgs = { - 'W%d04' % BASE_ID: ("Use explicit fields instead of exclude in ModelForm", - 'modelform-uses-exclude', - "Prevents accidentally allowing users to set fields, " - "especially when adding new fields to a Model") + f"W{BASE_ID}04": ( + "Use explicit fields instead of exclude in ModelForm", + "modelform-uses-exclude", + "Prevents accidentally allowing users to set fields, especially when adding new fields to a Model", + ) } - @check_messages('modelform-uses-exclude') + @check_messages("modelform-uses-exclude") def visit_classdef(self, node): """Class visitor.""" - if not node_is_subclass(node, 'django.forms.models.ModelForm', '.ModelForm'): + if not node_is_subclass(node, "django.forms.models.ModelForm", ".ModelForm"): # we only care about forms return @@ -41,10 +42,9 @@ def visit_classdef(self, node): return for child in meta.get_children(): - if (not isinstance(child, Assign) - or not isinstance(child.targets[0], AssignName)): + if not isinstance(child, Assign) or not isinstance(child.targets[0], AssignName): continue - if child.targets[0].name == 'exclude': - self.add_message('W%s04' % BASE_ID, node=child) + if child.targets[0].name == "exclude": + self.add_message(f"W{BASE_ID}04", node=child) break diff --git a/pylint_django/checkers/json_response.py b/pylint_django/checkers/json_response.py index 922f55fc..392e6092 100644 --- a/pylint_django/checkers/json_response.py +++ b/pylint_django/checkers/json_response.py @@ -7,9 +7,9 @@ """ import astroid - -from pylint import interfaces, checkers +from pylint import checkers, interfaces from pylint.checkers import utils + from pylint_django.__pkginfo__ import BASE_ID @@ -22,37 +22,49 @@ class JsonResponseChecker(checkers.BaseChecker): __implements__ = (interfaces.IAstroidChecker,) # configuration section name - name = 'json-response-checker' - msgs = {'R%s01' % BASE_ID: ("Instead of HttpResponse(json.dumps(data)) use JsonResponse(data)", - 'http-response-with-json-dumps', - 'Used when json.dumps() is used as an argument to HttpResponse().'), - 'R%s02' % BASE_ID: ("Instead of HttpResponse(content_type='application/json') use JsonResponse()", - 'http-response-with-content-type-json', - 'Used when HttpResponse() is returning application/json.'), - 'R%s03' % BASE_ID: ("Redundant content_type parameter for JsonResponse()", - 'redundant-content-type-for-json-response', - 'Used when JsonResponse() contains content_type parameter. ' - 'This is either redundant or the content_type is not JSON ' - 'which is probably an error.')} - - @utils.check_messages('http-response-with-json-dumps', - 'http-response-with-content-type-json', - 'redundant-content-type-for-json-response') + name = "json-response-checker" + msgs = { + f"R{BASE_ID}01": ( + "Instead of HttpResponse(json.dumps(data)) use JsonResponse(data)", + "http-response-with-json-dumps", + "Used when json.dumps() is used as an argument to HttpResponse().", + ), + f"R{BASE_ID}02": ( + "Instead of HttpResponse(content_type='application/json') use JsonResponse()", + "http-response-with-content-type-json", + "Used when HttpResponse() is returning application/json.", + ), + f"R{BASE_ID}03": ( + "Redundant content_type parameter for JsonResponse()", + "redundant-content-type-for-json-response", + "Used when JsonResponse() contains content_type parameter. " + "This is either redundant or the content_type is not JSON " + "which is probably an error.", + ), + } + + @utils.check_messages( + "http-response-with-json-dumps", + "http-response-with-content-type-json", + "redundant-content-type-for-json-response", + ) def visit_call(self, node): - if (node.func.as_string().endswith('HttpResponse') and node.args - and isinstance(node.args[0], astroid.Call) - and node.args[0].func.as_string() == 'json.dumps'): - self.add_message('http-response-with-json-dumps', node=node) + if ( + node.func.as_string().endswith("HttpResponse") + and node.args + and isinstance(node.args[0], astroid.Call) + and node.args[0].func.as_string() == "json.dumps" + ): + self.add_message("http-response-with-json-dumps", node=node) - if node.func.as_string().endswith('HttpResponse') and node.keywords: + if node.func.as_string().endswith("HttpResponse") and node.keywords: for keyword in node.keywords: - if (keyword.arg == 'content_type' - and keyword.value.as_string().lower().find('application/json') > -1): - self.add_message('http-response-with-content-type-json', node=node) + if keyword.arg == "content_type" and keyword.value.as_string().lower().find("application/json") > -1: + self.add_message("http-response-with-content-type-json", node=node) break - if node.func.as_string().endswith('JsonResponse') and node.keywords: + if node.func.as_string().endswith("JsonResponse") and node.keywords: for keyword in node.keywords: - if keyword.arg == 'content_type': - self.add_message('redundant-content-type-for-json-response', node=node) + if keyword.arg == "content_type": + self.add_message("redundant-content-type-for-json-response", node=node) break diff --git a/pylint_django/checkers/migrations.py b/pylint_django/checkers/migrations.py index cdcf8f37..c8b9d075 100644 --- a/pylint_django/checkers/migrations.py +++ b/pylint_django/checkers/migrations.py @@ -9,15 +9,12 @@ """ import astroid - -from pylint import interfaces -from pylint import checkers +from pylint import checkers, interfaces from pylint.checkers import utils - from pylint_plugin_utils import suppress_message -from pylint_django.__pkginfo__ import BASE_ID from pylint_django import compat +from pylint_django.__pkginfo__ import BASE_ID from pylint_django.utils import is_migrations_module @@ -25,18 +22,18 @@ def _is_addfield_with_default(call): if not isinstance(call.func, astroid.Attribute): return False - if not call.func.attrname == 'AddField': + if not call.func.attrname == "AddField": return False for keyword in call.keywords: # looking for AddField(..., field=XXX(..., default=Y, ...), ...) - if keyword.arg == 'field' and isinstance(keyword.value, astroid.Call): + if keyword.arg == "field" and isinstance(keyword.value, astroid.Call): # loop over XXX's keywords # NOTE: not checking if XXX is an actual field type because there could # be many types we're not aware of. Also the migration will probably break # if XXX doesn't instantiate a field object! for field_keyword in keyword.value.keywords: - if field_keyword.arg == 'default': + if field_keyword.arg == "default": return True return False @@ -57,11 +54,14 @@ class NewDbFieldWithDefaultChecker(checkers.BaseChecker): __implements__ = (interfaces.IAstroidChecker,) # configuration section name - name = 'new-db-field-with-default' - msgs = {'W%s98' % BASE_ID: ("%s AddField with default value", - 'new-db-field-with-default', - 'Used when Pylint detects migrations adding new ' - 'fields with a default value.')} + name = "new-db-field-with-default" + msgs = { + f"W{BASE_ID}98": ( + "%s AddField with default value", + "new-db-field-with-default", + "Used when Pylint detects migrations adding new fields with a default value.", + ) + } _migration_modules = [] _possible_offences = {} @@ -86,7 +86,7 @@ def visit_call(self, node): if node not in self._possible_offences[module]: self._possible_offences[module].append(node) - @utils.check_messages('new-db-field-with-default') + @utils.check_messages("new-db-field-with-default") def close(self): def _path(node): return node.path @@ -98,10 +98,10 @@ def _path(node): # filter out the last migration modules under each distinct # migrations directory, iow leave only the latest migrations # for each application - last_name_space = '' + last_name_space = "" latest_migrations = [] for module in self._migration_modules: - name_space = module.path[0].split('migrations')[0] + name_space = module.path[0].split("migrations")[0] if name_space != last_name_space: last_name_space = name_space latest_migrations.append(module) @@ -109,20 +109,23 @@ def _path(node): for module, nodes in self._possible_offences.items(): if module in latest_migrations: for node in nodes: - self.add_message('new-db-field-with-default', args=module.name, node=node) + self.add_message("new-db-field-with-default", args=module.name, node=node) class MissingBackwardsMigrationChecker(checkers.BaseChecker): __implements__ = (interfaces.IAstroidChecker,) - name = 'missing-backwards-migration-callable' + name = "missing-backwards-migration-callable" - msgs = {'W%s97' % BASE_ID: ('Always include backwards migration callable', - 'missing-backwards-migration-callable', - 'Always include a backwards/reverse callable counterpart' - ' so that the migration is not irreversable.')} + msgs = { + f"W{BASE_ID}97": ( + "Always include backwards migration callable", + "missing-backwards-migration-callable", + "Always include a backwards/reverse callable counterpart so that the migration is not irreversable.", + ) + } - @utils.check_messages('missing-backwards-migration-callable') + @utils.check_messages("missing-backwards-migration-callable") def visit_call(self, node): try: module = node.frame().parent @@ -132,26 +135,24 @@ def visit_call(self, node): if not is_migrations_module(module): return - if node.func.as_string().endswith('RunPython') and len(node.args) < 2: + if node.func.as_string().endswith("RunPython") and len(node.args) < 2: if node.keywords: for keyword in node.keywords: - if keyword.arg == 'reverse_code': + if keyword.arg == "reverse_code": return - self.add_message('missing-backwards-migration-callable', - node=node) + self.add_message("missing-backwards-migration-callable", node=node) else: - self.add_message('missing-backwards-migration-callable', - node=node) + self.add_message("missing-backwards-migration-callable", node=node) def is_in_migrations(node): """ - RunPython() migrations receive forward/backwards functions with signature: + RunPython() migrations receive forward/backwards functions with signature: - def func(apps, schema_editor): + def func(apps, schema_editor): - which could be unused. This augmentation will suppress all 'unused-argument' - messages coming from functions in migration modules. + which could be unused. This augmentation will suppress all 'unused-argument' + messages coming from functions in migration modules. """ return is_migrations_module(node.parent) @@ -159,8 +160,8 @@ def func(apps, schema_editor): def load_configuration(linter): # TODO this is redundant and can be removed # don't blacklist migrations for this checker new_black_list = list(linter.config.black_list) - if 'migrations' in new_black_list: - new_black_list.remove('migrations') + if "migrations" in new_black_list: + new_black_list.remove("migrations") linter.config.black_list = new_black_list @@ -173,5 +174,9 @@ def register(linter): # apply augmentations for migration checkers # Unused arguments for migrations - suppress_message(linter, checkers.variables.VariablesChecker.leave_functiondef, - 'unused-argument', is_in_migrations) + suppress_message( + linter, + checkers.variables.VariablesChecker.leave_functiondef, + "unused-argument", + is_in_migrations, + ) diff --git a/pylint_django/checkers/models.py b/pylint_django/checkers/models.py index d229a6a8..032b6f48 100644 --- a/pylint_django/checkers/models.py +++ b/pylint_django/checkers/models.py @@ -1,40 +1,44 @@ """Models.""" from astroid import Const -from astroid.nodes import Assign -from astroid.nodes import ClassDef, FunctionDef, AssignName - -from pylint.interfaces import IAstroidChecker -from pylint.checkers.utils import check_messages +from astroid.nodes import Assign, AssignName, ClassDef, FunctionDef from pylint.checkers import BaseChecker +from pylint.checkers.utils import check_messages +from pylint.interfaces import IAstroidChecker from pylint_django.__pkginfo__ import BASE_ID -from pylint_django.utils import node_is_subclass, PY3 - +from pylint_django.utils import PY3, node_is_subclass MESSAGES = { - 'E%d01' % BASE_ID: ("__unicode__ on a model must be callable (%s)", - 'model-unicode-not-callable', - "Django models require a callable __unicode__ method"), - 'W%d01' % BASE_ID: ("No __unicode__ method on model (%s)", - 'model-missing-unicode', - "Django models should implement a __unicode__ method for string representation"), - 'W%d02' % BASE_ID: ("Found __unicode__ method on model (%s). Python3 uses __str__.", - 'model-has-unicode', - "Django models should not implement a __unicode__ " - "method for string representation when using Python3"), - 'W%d03' % BASE_ID: ("Model does not explicitly define __unicode__ (%s)", - 'model-no-explicit-unicode', - "Django models should implement a __unicode__ method for string representation. " - "A parent class of this model does, but ideally all models should be explicit.") + f"E{BASE_ID}01": ( + "__unicode__ on a model must be callable (%s)", + "model-unicode-not-callable", + "Django models require a callable __unicode__ method", + ), + f"W{BASE_ID}01": ( + "No __unicode__ method on model (%s)", + "model-missing-unicode", + "Django models should implement a __unicode__ method for string representation", + ), + f"W{BASE_ID}02": ( + "Found __unicode__ method on model (%s). Python3 uses __str__.", + "model-has-unicode", + "Django models should not implement a __unicode__ method for string representation when using Python3", + ), + f"W{BASE_ID}03": ( + "Model does not explicitly define __unicode__ (%s)", + "model-no-explicit-unicode", + "Django models should implement a __unicode__ method for string representation. " + "A parent class of this model does, but ideally all models should be explicit.", + ), } def _is_meta_with_abstract(node): - if isinstance(node, ClassDef) and node.name == 'Meta': + if isinstance(node, ClassDef) and node.name == "Meta": for meta_child in node.get_children(): if not isinstance(meta_child, Assign): continue - if not meta_child.targets[0].name == 'abstract': + if not meta_child.targets[0].name == "abstract": continue if not isinstance(meta_child.value, Const): continue @@ -52,17 +56,17 @@ def _has_python_2_unicode_compatible_decorator(node): return False for decorator in node.decorators.nodes: - if getattr(decorator, 'name', None) == 'python_2_unicode_compatible': + if getattr(decorator, "name", None) == "python_2_unicode_compatible": return True return False def _is_unicode_or_str_in_python_2_compatibility(method): - if method.name == '__unicode__': + if method.name == "__unicode__": return True - if method.name == '__str__' and _has_python_2_unicode_compatible_decorator(method.parent): + if method.name == "__str__" and _has_python_2_unicode_compatible_decorator(method.parent): return True return False @@ -70,15 +74,16 @@ def _is_unicode_or_str_in_python_2_compatibility(method): class ModelChecker(BaseChecker): """Django model checker.""" + __implements__ = IAstroidChecker - name = 'django-model-checker' + name = "django-model-checker" msgs = MESSAGES - @check_messages('model-missing-unicode') + @check_messages("model-missing-unicode") def visit_classdef(self, node): """Class visitor.""" - if not node_is_subclass(node, 'django.db.models.base.Model', '.Model'): + if not node_is_subclass(node, "django.db.models.base.Model", ".Model"): # we only care about models return @@ -93,7 +98,7 @@ def visit_classdef(self, node): continue name = grandchildren[0].name - if name != '__unicode__': + if name != "__unicode__": continue grandchild = grandchildren[1] @@ -102,12 +107,12 @@ def visit_classdef(self, node): if assigned.callable(): return - self.add_message('E%s01' % BASE_ID, args=node.name, node=node) + self.add_message(f"E{BASE_ID}01", args=node.name, node=node) return - if isinstance(child, FunctionDef) and child.name == '__unicode__': + if isinstance(child, FunctionDef) and child.name == "__unicode__": if PY3: - self.add_message('W%s02' % BASE_ID, args=node.name, node=node) + self.add_message(f"W{BASE_ID}02", args=node.name, node=node) return # if we get here, then we have no __unicode__ method directly on the class itself @@ -117,7 +122,7 @@ def visit_classdef(self, node): if method.parent != node and _is_unicode_or_str_in_python_2_compatibility(method): # this happens if a parent declares the unicode method but # this node does not - self.add_message('W%s03' % BASE_ID, args=node.name, node=node) + self.add_message(f"W{BASE_ID}03", args=node.name, node=node) return # if the Django compatibility decorator is used then we don't emit a warning @@ -128,4 +133,4 @@ def visit_classdef(self, node): if PY3: return - self.add_message('W%s01' % BASE_ID, args=node.name, node=node) + self.add_message(f"W{BASE_ID}01", args=node.name, node=node) diff --git a/pylint_django/compat.py b/pylint_django/compat.py index 69414d46..4a50aed3 100644 --- a/pylint_django/compat.py +++ b/pylint_django/compat.py @@ -3,15 +3,13 @@ # no sane linter can figure out the hackiness in this compatability layer... try: - from astroid.nodes import ClassDef, FunctionDef, ImportFrom, AssignName, Attribute + from astroid.nodes import AssignName, Attribute, ClassDef, FunctionDef, ImportFrom except ImportError: - from astroid.nodes import ( - Class as ClassDef, - Function as FunctionDef, - From as ImportFrom, - AssName as AssignName, - Getattr as Attribute, - ) + from astroid.nodes import AssName as AssignName + from astroid.nodes import Class as ClassDef + from astroid.nodes import From as ImportFrom + from astroid.nodes import Function as FunctionDef + from astroid.nodes import Getattr as Attribute # pylint 2.04->2.2 : YES was renamed to Uninferable, then YES became deprecated, then was removed try: @@ -27,6 +25,6 @@ # pylint before version 2.3 does not support load_configuration() hook. LOAD_CONFIGURATION_SUPPORTED = False try: - LOAD_CONFIGURATION_SUPPORTED = tuple(pylint.__version__.split('.')) >= ('2', '3') + LOAD_CONFIGURATION_SUPPORTED = tuple(pylint.__version__.split(".")) >= ("2", "3") except AttributeError: LOAD_CONFIGURATION_SUPPORTED = pylint.__pkginfo__.numversion >= (2, 3) diff --git a/pylint_django/plugin.py b/pylint_django/plugin.py index 194f80d7..d3f45a27 100644 --- a/pylint_django/plugin.py +++ b/pylint_django/plugin.py @@ -2,12 +2,11 @@ from pylint.checkers.base import NameChecker from pylint_plugin_utils import get_checker -from pylint_django.checkers import register_checkers - # we want to import the transforms to make sure they get added to the astroid manager, # however we don't actually access them directly, so we'll disable the warning from pylint_django import transforms # noqa, pylint: disable=unused-import from pylint_django import compat +from pylint_django.checkers import register_checkers def load_configuration(linter): @@ -15,11 +14,19 @@ def load_configuration(linter): Amend existing checker config. """ name_checker = get_checker(linter, NameChecker) - name_checker.config.good_names += ('qs', 'urlpatterns', 'register', 'app_name', - 'handler400', 'handler403', 'handler404', 'handler500') + name_checker.config.good_names += ( + "qs", + "urlpatterns", + "register", + "app_name", + "handler400", + "handler403", + "handler404", + "handler500", + ) # we don't care about South migrations - linter.config.black_list += ('migrations', 'south_migrations') + linter.config.black_list += ("migrations", "south_migrations") def register(linter): @@ -33,6 +40,7 @@ def register(linter): try: # pylint: disable=import-outside-toplevel from pylint_django.augmentations import apply_augmentations + apply_augmentations(linter) except ImportError: # probably trying to execute pylint_django when Django isn't installed diff --git a/pylint_django/tests/input/external_django_tables2_noerror_meta_class.py b/pylint_django/tests/input/external_django_tables2_noerror_meta_class.py index d5472948..d1711726 100644 --- a/pylint_django/tests/input/external_django_tables2_noerror_meta_class.py +++ b/pylint_django/tests/input/external_django_tables2_noerror_meta_class.py @@ -4,8 +4,8 @@ # pylint: disable=missing-docstring,too-few-public-methods -from django.db import models import django_tables2 as tables +from django.db import models class SimpleModel(models.Model): diff --git a/pylint_django/tests/input/external_factory_boy_noerror.py b/pylint_django/tests/input/external_factory_boy_noerror.py index 020941a2..9f6152d3 100644 --- a/pylint_django/tests/input/external_factory_boy_noerror.py +++ b/pylint_django/tests/input/external_factory_boy_noerror.py @@ -4,7 +4,6 @@ """ # pylint: disable=attribute-defined-outside-init, missing-docstring, too-few-public-methods import factory - from django import test from django.db import models @@ -15,21 +14,21 @@ class Author(models.Model): class Book(models.Model): title = models.CharField() - author = models.ForeignKey(Author, related_name='books', on_delete=models.CASCADE) + author = models.ForeignKey(Author, related_name="books", on_delete=models.CASCADE) class AuthorFactory(factory.django.DjangoModelFactory): class Meta: - model = 'Author' + model = "Author" - name = factory.Sequence(lambda n: 'Author %d' % n) + name = factory.Sequence(lambda n: f"Author {n}") class BookFactory(factory.django.DjangoModelFactory): class Meta: - model = 'Book' + model = "Book" - title = factory.Sequence(lambda n: 'Book %d' % n) + title = factory.Sequence(lambda n: f"Book {n}") author = factory.SubFactory(AuthorFactory) reviewer = factory.LazyFunction(Author.objects.first()) diff --git a/pylint_django/tests/input/external_model_utils_noerror_override_manager.py b/pylint_django/tests/input/external_model_utils_noerror_override_manager.py index 54533592..84d6038f 100644 --- a/pylint_django/tests/input/external_model_utils_noerror_override_manager.py +++ b/pylint_django/tests/input/external_model_utils_noerror_override_manager.py @@ -4,9 +4,8 @@ # https://github.com/PyCQA/pylint-django/issues/117 # pylint: disable=missing-docstring, invalid-name -from model_utils.managers import InheritanceManager - from django.db import models +from model_utils.managers import InheritanceManager class BaseModel(models.Model): diff --git a/pylint_django/tests/input/external_psycopg2_noerror_postgres_fields.py b/pylint_django/tests/input/external_psycopg2_noerror_postgres_fields.py index e5cb0dde..043126b0 100644 --- a/pylint_django/tests/input/external_psycopg2_noerror_postgres_fields.py +++ b/pylint_django/tests/input/external_psycopg2_noerror_postgres_fields.py @@ -26,11 +26,11 @@ def arrayfield_tests(self): def dictfield_tests(self): print(self.hstorefield.keys()) print(self.hstorefield.values()) - print(self.hstorefield.update({'foo': 'bar'})) + print(self.hstorefield.update({"foo": "bar"})) print(self.jsonfield.keys()) print(self.jsonfield.values()) - print(self.jsonfield.update({'foo': 'bar'})) + print(self.jsonfield.update({"foo": "bar"})) def rangefield_tests(self): print(self.rangefield.lower) diff --git a/pylint_django/tests/input/func_hard_coded_auth_user.py b/pylint_django/tests/input/func_hard_coded_auth_user.py index b5221489..99e3041b 100644 --- a/pylint_django/tests/input/func_hard_coded_auth_user.py +++ b/pylint_django/tests/input/func_hard_coded_auth_user.py @@ -1,9 +1,9 @@ # pylint: disable=missing-docstring, wildcard-import, unused-wildcard-import # flake8: noqa=F401, F403 -from django.db import models -from django.contrib.auth.models import * # [imported-auth-user] +from django.contrib.auth.models import * # [imported-auth-user] from django.contrib.auth.models import User # [imported-auth-user] +from django.db import models class PullRequest(models.Model): diff --git a/pylint_django/tests/input/func_hard_coded_auth_user.txt b/pylint_django/tests/input/func_hard_coded_auth_user.txt index 802abda0..668a35ca 100644 --- a/pylint_django/tests/input/func_hard_coded_auth_user.txt +++ b/pylint_django/tests/input/func_hard_coded_auth_user.txt @@ -1,3 +1,3 @@ -imported-auth-user:5:0::User model imported from django.contrib.auth.models -imported-auth-user:6:0::User model imported from django.contrib.auth.models -hard-coded-auth-user:10:31:PullRequest:Hard-coded 'auth.User' +imported-auth-user:4:0:4:40::User model imported from django.contrib.auth.models:UNDEFINED +imported-auth-user:5:0:5:43::User model imported from django.contrib.auth.models:UNDEFINED +hard-coded-auth-user:10:31:10:42:PullRequest:Hard-coded 'auth.User':UNDEFINED diff --git a/pylint_django/tests/input/func_json_response.py b/pylint_django/tests/input/func_json_response.py index c0bab2e8..25771811 100644 --- a/pylint_django/tests/input/func_json_response.py +++ b/pylint_django/tests/input/func_json_response.py @@ -1,16 +1,17 @@ # pylint: disable=missing-docstring, line-too-long import json + from django import http from django.http import HttpResponse def say_yes(): - return HttpResponse(json.dumps({'rc': 0, 'response': 'ok'})) # [http-response-with-json-dumps] + return HttpResponse(json.dumps({"rc": 0, "response": "ok"})) # [http-response-with-json-dumps] def say_yes2(): - data = {'rc': 0, 'response': 'ok'} + data = {"rc": 0, "response": "ok"} return http.HttpResponse(json.dumps(data)) # [http-response-with-json-dumps] @@ -19,10 +20,10 @@ def say_no(): def redundant_content_type(): - data = {'rc': 0, 'response': 'ok'} - return http.JsonResponse(data, content_type='application/json') # [redundant-content-type-for-json-response] + data = {"rc": 0, "response": "ok"} + return http.JsonResponse(data, content_type="application/json") # [redundant-content-type-for-json-response] def content_type_json(): json_data = "this comes from somewhere" - return HttpResponse(json_data, content_type='application/json') # [http-response-with-content-type-json] + return HttpResponse(json_data, content_type="application/json") # [http-response-with-content-type-json] diff --git a/pylint_django/tests/input/func_json_response.txt b/pylint_django/tests/input/func_json_response.txt index 692bf4e7..e13d0ef9 100644 --- a/pylint_django/tests/input/func_json_response.txt +++ b/pylint_django/tests/input/func_json_response.txt @@ -1,4 +1,4 @@ -http-response-with-json-dumps:9:11:say_yes:Instead of HttpResponse(json.dumps(data)) use JsonResponse(data) -http-response-with-json-dumps:14:11:say_yes2:Instead of HttpResponse(json.dumps(data)) use JsonResponse(data) -redundant-content-type-for-json-response:23:11:redundant_content_type:Redundant content_type parameter for JsonResponse() -http-response-with-content-type-json:28:11:content_type_json:Instead of HttpResponse(content_type='application/json') use JsonResponse() +http-response-with-json-dumps:10:11:10:64:say_yes:Instead of HttpResponse(json.dumps(data)) use JsonResponse(data):UNDEFINED +http-response-with-json-dumps:15:11:15:46:say_yes2:Instead of HttpResponse(json.dumps(data)) use JsonResponse(data):UNDEFINED +redundant-content-type-for-json-response:24:11:24:67:redundant_content_type:Redundant content_type parameter for JsonResponse():UNDEFINED +http-response-with-content-type-json:29:11:29:67:content_type_json:Instead of HttpResponse(content_type='application/json') use JsonResponse():UNDEFINED diff --git a/pylint_django/tests/input/func_model_does_not_use_unicode_py33.txt b/pylint_django/tests/input/func_model_does_not_use_unicode_py33.txt index 7aa53da8..394c8b36 100644 --- a/pylint_django/tests/input/func_model_does_not_use_unicode_py33.txt +++ b/pylint_django/tests/input/func_model_does_not_use_unicode_py33.txt @@ -1 +1 @@ -model-has-unicode:9:0:SomeModel:Found __unicode__ method on model (SomeModel). Python3 uses __str__. +model-has-unicode:9:0:19:29:SomeModel:Found __unicode__ method on model (SomeModel). Python3 uses __str__.:UNDEFINED diff --git a/pylint_django/tests/input/func_model_no_explicit_unicode_str_compat.py b/pylint_django/tests/input/func_model_no_explicit_unicode_str_compat.py index 764a1b37..d111218d 100644 --- a/pylint_django/tests/input/func_model_no_explicit_unicode_str_compat.py +++ b/pylint_django/tests/input/func_model_no_explicit_unicode_str_compat.py @@ -12,7 +12,7 @@ @python_2_unicode_compatible class BaseModel(models.Model): def __str__(self): - return 'Foo' + return "Foo" class SomeModel(BaseModel): # [model-no-explicit-unicode] diff --git a/pylint_django/tests/input/func_model_no_explicit_unicode_str_compat.txt b/pylint_django/tests/input/func_model_no_explicit_unicode_str_compat.txt index bbfd0213..891e66f0 100644 --- a/pylint_django/tests/input/func_model_no_explicit_unicode_str_compat.txt +++ b/pylint_django/tests/input/func_model_no_explicit_unicode_str_compat.txt @@ -1 +1 @@ -model-no-explicit-unicode:18:0:SomeModel:Model does not explicitly define __unicode__ (SomeModel) +model-no-explicit-unicode:18:0:19:8:SomeModel:Model does not explicitly define __unicode__ (SomeModel):UNDEFINED diff --git a/pylint_django/tests/input/func_modelform_exclude.py b/pylint_django/tests/input/func_modelform_exclude.py index ceb72221..432ee0dc 100644 --- a/pylint_django/tests/input/func_modelform_exclude.py +++ b/pylint_django/tests/input/func_modelform_exclude.py @@ -7,4 +7,4 @@ class PersonForm(forms.ModelForm): class Meta: - exclude = ('email',) # [modelform-uses-exclude] + exclude = ("email",) # [modelform-uses-exclude] diff --git a/pylint_django/tests/input/func_modelform_exclude.txt b/pylint_django/tests/input/func_modelform_exclude.txt index 1e2ef863..a36bbf3e 100644 --- a/pylint_django/tests/input/func_modelform_exclude.txt +++ b/pylint_django/tests/input/func_modelform_exclude.txt @@ -1 +1 @@ -modelform-uses-exclude:10:8:PersonForm.Meta:Use explicit fields instead of exclude in ModelForm +modelform-uses-exclude:10:8:10:28:PersonForm.Meta:Use explicit fields instead of exclude in ModelForm:UNDEFINED diff --git a/pylint_django/tests/input/func_noerror_classviews.py b/pylint_django/tests/input/func_noerror_classviews.py index f8320940..e9a8ea25 100644 --- a/pylint_django/tests/input/func_noerror_classviews.py +++ b/pylint_django/tests/input/func_noerror_classviews.py @@ -6,76 +6,70 @@ from django.db import models from django.http import JsonResponse -from django.views.generic import DetailView -from django.views.generic import TemplateView -from django.views.generic import View +from django.views.generic import DetailView, TemplateView, View from django.views.generic.edit import CreateView class BoringView(TemplateView): # ensure that args, kwargs and request are not thrown up as errors def get_context_data(self, **kwargs): - return { - 'request': self.request, - 'args': self.args, - 'kwargs': self.kwargs - } + return {"request": self.request, "args": self.args, "kwargs": self.kwargs} class JsonGetView(View): def get(self, request, *args, **kwargs): # do something with objects but don't use # self or request - return JsonResponse({'rc': 0, 'response': 'ok'}) + return JsonResponse({"rc": 0, "response": "ok"}) class JsonPostView(View): def post(self, request, *args, **kwargs): # do something with objects but don't use # self or request - return JsonResponse({'rc': 0, 'response': 'ok'}) + return JsonResponse({"rc": 0, "response": "ok"}) class JsonPutView(View): def put(self, request, *args, **kwargs): # do something with objects but don't use # self or request - return JsonResponse({'rc': 0, 'response': 'ok'}) + return JsonResponse({"rc": 0, "response": "ok"}) class JsonPatchView(View): def patch(self, request, *args, **kwargs): # do something with objects but don't use # self or request - return JsonResponse({'rc': 0, 'response': 'ok'}) + return JsonResponse({"rc": 0, "response": "ok"}) class JsonDeleteView(View): def delete(self, request, *args, **kwargs): # do something with objects but don't use # self or request - return JsonResponse({'rc': 0, 'response': 'ok'}) + return JsonResponse({"rc": 0, "response": "ok"}) class JsonHeadView(View): def head(self, request, *args, **kwargs): # pylint: disable=method-hidden # do something with objects but don't use # self or request - return JsonResponse({'rc': 0, 'response': 'ok'}) + return JsonResponse({"rc": 0, "response": "ok"}) class JsonOptionsView(View): def options(self, request, *args, **kwargs): # do something with objects but don't use # self or request - return JsonResponse({'rc': 0, 'response': 'ok'}) + return JsonResponse({"rc": 0, "response": "ok"}) class JsonTraceView(View): def trace(self, request, *args, **kwargs): # do something with objects but don't use # self or request - return JsonResponse({'rc': 0, 'response': 'ok'}) + return JsonResponse({"rc": 0, "response": "ok"}) class Book(models.Model): @@ -85,15 +79,15 @@ class Book(models.Model): class GetBook(DetailView): model = Book - template_name = 'books/get.html' - http_method_names = ['get'] + template_name = "books/get.html" + http_method_names = ["get"] class CreateBook(CreateView): model = Book - template_name = 'books/new.html' + template_name = "books/new.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = 'New book' + context["page_title"] = "New book" return context diff --git a/pylint_django/tests/input/func_noerror_foreign_key_attributes.py b/pylint_django/tests/input/func_noerror_foreign_key_attributes.py index 1ae57677..8c20b4fc 100644 --- a/pylint_django/tests/input/func_noerror_foreign_key_attributes.py +++ b/pylint_django/tests/input/func_noerror_foreign_key_attributes.py @@ -1,7 +1,7 @@ """ Checks that Pylint does not complain about foreign key sets on models """ -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring consider-using-f-string from django.db import models @@ -16,6 +16,6 @@ class OtherModel(models.Model): elsething = models.OneToOneField(SomeModel, on_delete=models.CASCADE) def something_doer(self): - part_a = '%s - %s' % (self.something.name, self.something.timestamp) + part_a = f"{self.something.name} - {self.something.timestamp}" part_b = self.elsething.name return part_a, part_b diff --git a/pylint_django/tests/input/func_noerror_foreign_key_key_cls_unbound.py b/pylint_django/tests/input/func_noerror_foreign_key_key_cls_unbound.py index 16c1af23..d4479b5d 100644 --- a/pylint_django/tests/input/func_noerror_foreign_key_key_cls_unbound.py +++ b/pylint_django/tests/input/func_noerror_foreign_key_key_cls_unbound.py @@ -8,7 +8,7 @@ class FairyTail(models.Model): # fails with "UnboundLocalError: local variable 'key_cls' referenced before assignment" - author = models.ForeignKey(to='input.Author', null=True, on_delete=models.CASCADE) + author = models.ForeignKey(to="input.Author", null=True, on_delete=models.CASCADE) def get_author_name(self): return self.author.id diff --git a/pylint_django/tests/input/func_noerror_foreign_key_package.py b/pylint_django/tests/input/func_noerror_foreign_key_package.py index d2e142da..02774b12 100644 --- a/pylint_django/tests/input/func_noerror_foreign_key_package.py +++ b/pylint_django/tests/input/func_noerror_foreign_key_package.py @@ -7,7 +7,7 @@ class Book(models.Model): - author = models.ForeignKey(to='pylint_django.tests.input.Author', on_delete=models.CASCADE) + author = models.ForeignKey(to="pylint_django.tests.input.Author", on_delete=models.CASCADE) def get_author_name(self): return self.author.id diff --git a/pylint_django/tests/input/func_noerror_foreign_key_sets.py b/pylint_django/tests/input/func_noerror_foreign_key_sets.py index a5cfeb3a..37e69090 100644 --- a/pylint_django/tests/input/func_noerror_foreign_key_sets.py +++ b/pylint_django/tests/input/func_noerror_foreign_key_sets.py @@ -25,8 +25,7 @@ class OtherModel(models.Model): class ThirdModel(models.Model): - whatever = models.ForeignKey(SomeModel, related_name='whatevs', - on_delete=models.CASCADE) + whatever = models.ForeignKey(SomeModel, related_name="whatevs", on_delete=models.CASCADE) def count_whatevers(): diff --git a/pylint_django/tests/input/func_noerror_foreignkeys.py b/pylint_django/tests/input/func_noerror_foreignkeys.py index fbfab16f..e9cb5e81 100644 --- a/pylint_django/tests/input/func_noerror_foreignkeys.py +++ b/pylint_django/tests/input/func_noerror_foreignkeys.py @@ -22,7 +22,7 @@ class ISBN(models.Model): class Book(models.Model): book_name = models.CharField(max_length=100) # Check this works with and without `to` keyword - author = models.ForeignKey(to='Author', on_delete=models.CASCADE) + author = models.ForeignKey(to="Author", on_delete=models.CASCADE) isbn = models.OneToOneField(to=ISBN, on_delete=models.CASCADE) genre = models.ForeignKey(Genre, on_delete=models.CASCADE) @@ -56,8 +56,8 @@ def get_username(self): class Human(models.Model): - child = ForeignKey('self', on_delete=models.SET_NULL, null=True) - parent = ForeignKey(to='self', on_delete=models.SET_NULL, null=True) + child = ForeignKey("self", on_delete=models.SET_NULL, null=True) + parent = ForeignKey(to="self", on_delete=models.SET_NULL, null=True) def get_grandchild(self): return self.child.child @@ -68,15 +68,16 @@ def get_grandparent(self): class UserPreferences(models.Model): """ - Used for testing FK which refers to another model by - string, not model class, see - https://github.com/PyCQA/pylint-django/issues/35 + Used for testing FK which refers to another model by + string, not model class, see + https://github.com/PyCQA/pylint-django/issues/35 """ - user = ForeignKey('User', on_delete=models.CASCADE) + + user = ForeignKey("User", on_delete=models.CASCADE) class UserAddress(models.Model): - user = OneToOneField(to='User', on_delete=models.CASCADE) + user = OneToOneField(to="User", on_delete=models.CASCADE) line_1 = models.CharField(max_length=100) line_2 = models.CharField(max_length=100) city = models.CharField(max_length=100) diff --git a/pylint_django/tests/input/func_noerror_form_fields.py b/pylint_django/tests/input/func_noerror_form_fields.py index 9ca2e10d..d6ffbfef 100644 --- a/pylint_django/tests/input/func_noerror_form_fields.py +++ b/pylint_django/tests/input/func_noerror_form_fields.py @@ -4,7 +4,9 @@ """ # pylint: disable=missing-docstring from __future__ import print_function -from datetime import datetime, date + +from datetime import date, datetime + from django import forms from django.contrib.auth.forms import UserCreationForm @@ -18,11 +20,11 @@ class ManyFieldsForm(forms.Form): decimalfield = forms.DecimalField(max_digits=5, decimal_places=2) durationfield = forms.DurationField() emailfield = forms.EmailField() - filefield = forms.FileField(name='test_file', upload_to='test') - filepathfield = forms.FilePathField(path='/some/path') + filefield = forms.FileField(name="test_file", upload_to="test") + filepathfield = forms.FilePathField(path="/some/path") floatfield = forms.FloatField() genericipaddressfield = forms.GenericIPAddressField() - imagefield = forms.ImageField(name='test_image', upload_to='test') + imagefield = forms.ImageField(name="test_image", upload_to="test") intfield = forms.IntegerField(null=True) nullbooleanfield = forms.NullBooleanField() slugfield = forms.SlugField() @@ -36,15 +38,15 @@ def boolean_field_tests(self): def string_field_tests(self): print(self.charfield.strip()) print(self.charfield.upper()) - print(self.charfield.replace('x', 'y')) + print(self.charfield.replace("x", "y")) print(self.filepathfield.strip()) print(self.filepathfield.upper()) - print(self.filepathfield.replace('x', 'y')) + print(self.filepathfield.replace("x", "y")) print(self.emailfield.strip()) print(self.emailfield.upper()) - print(self.emailfield.replace('x', 'y')) + print(self.emailfield.replace("x", "y")) def datetimefield_tests(self): now = datetime.now() diff --git a/pylint_django/tests/input/func_noerror_forms_py33.py b/pylint_django/tests/input/func_noerror_forms_py33.py index e8a5291c..7a8c2aee 100644 --- a/pylint_django/tests/input/func_noerror_forms_py33.py +++ b/pylint_django/tests/input/func_noerror_forms_py33.py @@ -35,8 +35,8 @@ class Meta: class TestFormWidgetAssignment(forms.Form): - multi_field = forms.MultipleChoiceField(choices=[('1', 'First'), ('2', 'Second')]) + multi_field = forms.MultipleChoiceField(choices=[("1", "First"), ("2", "Second")]) class Meta: widgets = {} - widgets['multi_field'] = forms.CheckboxSelectMultiple + widgets["multi_field"] = forms.CheckboxSelectMultiple diff --git a/pylint_django/tests/input/func_noerror_generic_foreign_key.py b/pylint_django/tests/input/func_noerror_generic_foreign_key.py index 282f47a7..8cfe73b3 100644 --- a/pylint_django/tests/input/func_noerror_generic_foreign_key.py +++ b/pylint_django/tests/input/func_noerror_generic_foreign_key.py @@ -4,9 +4,9 @@ """ # pylint: disable=missing-docstring -from django.db import models -from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.db import models class Ownership(models.Model): diff --git a/pylint_django/tests/input/func_noerror_gettext_lazy_format.py b/pylint_django/tests/input/func_noerror_gettext_lazy_format.py index 300e5849..d331c4d5 100644 --- a/pylint_django/tests/input/func_noerror_gettext_lazy_format.py +++ b/pylint_django/tests/input/func_noerror_gettext_lazy_format.py @@ -4,4 +4,4 @@ """ from django.utils.translation import gettext_lazy -gettext_lazy('{something}').format(something='lala') +gettext_lazy("{something}").format(something="lala") diff --git a/pylint_django/tests/input/func_noerror_ignore_meta_subclass.py b/pylint_django/tests/input/func_noerror_ignore_meta_subclass.py index af1c9b08..50a84f1d 100644 --- a/pylint_django/tests/input/func_noerror_ignore_meta_subclass.py +++ b/pylint_django/tests/input/func_noerror_ignore_meta_subclass.py @@ -9,4 +9,4 @@ class SomeModel(models.Model): class Meta: - ordering = ('-id',) + ordering = ("-id",) diff --git a/pylint_django/tests/input/func_noerror_managers_return_querysets.py b/pylint_django/tests/input/func_noerror_managers_return_querysets.py index 1f628c62..9db69920 100644 --- a/pylint_django/tests/input/func_noerror_managers_return_querysets.py +++ b/pylint_django/tests/input/func_noerror_managers_return_querysets.py @@ -7,10 +7,11 @@ class SomeModelQuerySet(models.QuerySet): - """ A missing docstring """ + """A missing docstring""" + @staticmethod def public_method(): - """ A missing docstring """ + """A missing docstring""" return 1 @@ -19,17 +20,17 @@ class SomeModelManager(models.Manager): @staticmethod def public_method(): - """ A missing docstring """ + """A missing docstring""" return 1 @staticmethod def another_public_method(): - """ A missing docstring """ + """A missing docstring""" return 1 class SomeModel(models.Model): - """ A missing docstring """ + """A missing docstring""" name = models.CharField(max_length=20) datetimestamp = models.DateTimeField() @@ -38,14 +39,14 @@ class SomeModel(models.Model): class OtherModel(models.Model): - """ A missing docstring """ + """A missing docstring""" name = models.CharField(max_length=20) somemodel = models.ForeignKey(SomeModel, on_delete=models.CASCADE, null=False) def call_some_querysets(): - """ A missing docstring """ + """A missing docstring""" not_a_list = SomeModel.objects.filter() - not_a_list.values_list('id', flat=True) + not_a_list.values_list("id", flat=True) diff --git a/pylint_django/tests/input/func_noerror_manytomanyfield.py b/pylint_django/tests/input/func_noerror_manytomanyfield.py index 1840e869..8d6c2592 100644 --- a/pylint_django/tests/input/func_noerror_manytomanyfield.py +++ b/pylint_django/tests/input/func_noerror_manytomanyfield.py @@ -2,9 +2,10 @@ Checks that Pylint does not complain about various methods on many-to-many relationships """ +from django.contrib.auth.models import AbstractUser, Permission + # pylint: disable=missing-docstring from django.db import models -from django.contrib.auth.models import AbstractUser, Permission class Book(models.Model): @@ -14,8 +15,7 @@ class Book(models.Model): class Author(models.Model): name = models.CharField(max_length=100) - wrote = models.ManyToManyField(Book, verbose_name="Book", - related_name='books') + wrote = models.ManyToManyField(Book, verbose_name="Book", related_name="books") def get_good_books(self): return self.wrote.filter(good=True) @@ -28,17 +28,17 @@ def wrote_how_many(self): # Custom permissions for CustomUser -USER_PERMS = ['change_customuser', 'add_customuser'] +USER_PERMS = ["change_customuser", "add_customuser"] class CustomUser(AbstractUser): # pylint: disable=model-no-explicit-unicode class Meta: - verbose_name = 'CustomUser' - verbose_name_plural = 'CustomUsers' + verbose_name = "CustomUser" + verbose_name_plural = "CustomUsers" app_label = "users" def grant_permissions(self): - ''' Example adding permissions to User ''' + """Example adding permissions to User""" self.user_permissions.clear() for perm in USER_PERMS: perm = Permission.objects.get(codename=perm) @@ -55,7 +55,7 @@ def set_permissions(self, permissions): self.user_permissions.set(permissions) def save(self, *args, **kwargs): - ''' Saving while granting new permissions ''' + """Saving while granting new permissions""" self.is_staff = True super().save() self.grant_permissions() diff --git a/pylint_django/tests/input/func_noerror_model_fields.py b/pylint_django/tests/input/func_noerror_model_fields.py index 95bd4b1b..3e5ffeb9 100644 --- a/pylint_django/tests/input/func_noerror_model_fields.py +++ b/pylint_django/tests/input/func_noerror_model_fields.py @@ -4,8 +4,10 @@ """ # pylint: disable=missing-docstring from __future__ import print_function -from datetime import datetime, date + +from datetime import date, datetime from decimal import Decimal + from django.db import models @@ -20,11 +22,11 @@ class LotsOfFieldsModel(models.Model): decimalfield = models.DecimalField(max_digits=5, decimal_places=2) durationfield = models.DurationField() emailfield = models.EmailField() - filefield = models.FileField(name='test_file', upload_to='test') + filefield = models.FileField(name="test_file", upload_to="test") filepathfield = models.FilePathField() floatfield = models.FloatField() genericipaddressfield = models.GenericIPAddressField() - imagefield = models.ImageField(name='test_image', upload_to='test') + imagefield = models.ImageField(name="test_image", upload_to="test") ipaddressfield = models.IPAddressField() intfield = models.IntegerField(null=True) nullbooleanfield = models.NullBooleanField() @@ -46,19 +48,19 @@ def boolean_field_tests(self): def string_field_tests(self): print(self.charfield.strip()) print(self.charfield.upper()) - print(self.charfield.replace('x', 'y')) + print(self.charfield.replace("x", "y")) print(self.filepathfield.strip()) print(self.filepathfield.upper()) - print(self.filepathfield.replace('x', 'y')) + print(self.filepathfield.replace("x", "y")) print(self.emailfield.strip()) print(self.emailfield.upper()) - print(self.emailfield.replace('x', 'y')) + print(self.emailfield.replace("x", "y")) print(self.textfield.strip()) print(self.textfield.upper()) - print(self.textfield.replace('x', 'y')) + print(self.textfield.replace("x", "y")) def datetimefield_tests(self): now = datetime.now() @@ -71,7 +73,7 @@ def datefield_tests(self): print(self.datefield.isoformat()) def decimalfield_tests(self): - print(self.decimalfield.compare(Decimal('1.4'))) + print(self.decimalfield.compare(Decimal("1.4"))) def durationfield_tests(self): now = datetime.now() @@ -79,9 +81,9 @@ def durationfield_tests(self): print(self.durationfield.total_seconds()) def filefield_tests(self): - self.filefield.save('/dev/null', 'TEST') + self.filefield.save("/dev/null", "TEST") print(self.filefield.file) - self.imagefield.save('/dev/null', 'TEST') + self.imagefield.save("/dev/null", "TEST") print(self.imagefield.file) def numberfield_tests(self): diff --git a/pylint_django/tests/input/func_noerror_model_methods.py b/pylint_django/tests/input/func_noerror_model_methods.py index a3df4dd2..dc8efd62 100644 --- a/pylint_django/tests/input/func_noerror_model_methods.py +++ b/pylint_django/tests/input/func_noerror_model_methods.py @@ -9,7 +9,7 @@ class SomeModel(models.Model): name = models.CharField(max_length=64) -if __name__ == '__main__': +if __name__ == "__main__": MODEL = SomeModel() MODEL.save() MODEL.delete() @@ -19,5 +19,5 @@ class SomeModel(models.Model): FIRST = SomeModel.objects.first() LAST = SomeModel.objects.last() - DB_RECORD, CREATED = SomeModel.objects.get_or_create(name='Tester') + DB_RECORD, CREATED = SomeModel.objects.get_or_create(name="Tester") EXCLUDED_IDS = [obj.pk for obj in SomeModel.objects.exclude(name__isnull=True)] diff --git a/pylint_django/tests/input/func_noerror_models_py33.py b/pylint_django/tests/input/func_noerror_models_py33.py index 7ad22bbe..5eca5070 100644 --- a/pylint_django/tests/input/func_noerror_models_py33.py +++ b/pylint_django/tests/input/func_noerror_models_py33.py @@ -12,16 +12,16 @@ class Meta: some_field = models.CharField(max_length=20) - other_fields = models.ManyToManyField('AnotherModel') + other_fields = models.ManyToManyField("AnotherModel") def stuff(self): try: print(self._meta) print(self.other_fields.all()[0]) except self.DoesNotExist: - print('does not exist') + print("does not exist") except self.MultipleObjectsReturned: - print('lala') + print("lala") print(self.get_some_field_display()) diff --git a/pylint_django/tests/input/func_noerror_protected_meta_access.py b/pylint_django/tests/input/func_noerror_protected_meta_access.py index 494cc5fa..96e09adc 100644 --- a/pylint_django/tests/input/func_noerror_protected_meta_access.py +++ b/pylint_django/tests/input/func_noerror_protected_meta_access.py @@ -7,6 +7,7 @@ """ # pylint: disable=missing-docstring from __future__ import print_function + from django.db import models @@ -14,11 +15,11 @@ class ModelWhichLikesMeta(models.Model): ursuary = models.BooleanField(default=False) def do_a_thing(self): - return self._meta.get_field('ursuary') + return self._meta.get_field("ursuary") -if __name__ == '__main__': +if __name__ == "__main__": MODEL = ModelWhichLikesMeta() MODEL.save() - print(MODEL._meta.get_field('ursuary')) + print(MODEL._meta.get_field("ursuary")) print(MODEL.do_a_thing()) diff --git a/pylint_django/tests/input/func_noerror_test_wsgi_request.py b/pylint_django/tests/input/func_noerror_test_wsgi_request.py index e15d2911..9c9a3337 100644 --- a/pylint_django/tests/input/func_noerror_test_wsgi_request.py +++ b/pylint_django/tests/input/func_noerror_test_wsgi_request.py @@ -3,8 +3,8 @@ https://github.com/PyCQA/pylint-django/issues/78 """ -from django.test import TestCase from django.db import models +from django.test import TestCase class SomeModel(models.Model): @@ -13,9 +13,10 @@ class SomeModel(models.Model): class SomeTestCase(TestCase): """A test cast.""" + def test_thing(self): """Test a thing.""" expected_object = SomeModel() - response = self.client.get('/get/some/thing/') + response = self.client.get("/get/some/thing/") self.assertEqual(response.status_code, 200) - self.assertEqual(response.context['object'], expected_object) + self.assertEqual(response.context["object"], expected_object) diff --git a/pylint_django/tests/input/func_noerror_ugettext_lazy_format.py b/pylint_django/tests/input/func_noerror_ugettext_lazy_format.py index 1e8ad248..35b14701 100644 --- a/pylint_django/tests/input/func_noerror_ugettext_lazy_format.py +++ b/pylint_django/tests/input/func_noerror_ugettext_lazy_format.py @@ -4,4 +4,4 @@ """ from django.utils.translation import ugettext_lazy -ugettext_lazy('{something}').format(something='lala') +ugettext_lazy("{something}").format(something="lala") diff --git a/pylint_django/tests/input/func_noerror_unicode_py2_compatible.py b/pylint_django/tests/input/func_noerror_unicode_py2_compatible.py index ca744a1b..dace2270 100644 --- a/pylint_django/tests/input/func_noerror_unicode_py2_compatible.py +++ b/pylint_django/tests/input/func_noerror_unicode_py2_compatible.py @@ -4,9 +4,10 @@ See https://github.com/PyCQA/pylint-django/issues/10 """ +from django.db import models + # pylint: disable=missing-docstring from six import python_2_unicode_compatible -from django.db import models @python_2_unicode_compatible diff --git a/pylint_django/tests/input/func_noerror_urls.py b/pylint_django/tests/input/func_noerror_urls.py index 40094274..40a5d3ed 100644 --- a/pylint_django/tests/input/func_noerror_urls.py +++ b/pylint_django/tests/input/func_noerror_urls.py @@ -4,8 +4,12 @@ """ # pylint: disable=missing-docstring +try: + # to be able to test django versions from 1.11 - 3.2 + from django.urls import path +except ImportError: + from django.conf.urls import url as path from django.views.generic import TemplateView -from django.conf.urls import url class BoringView(TemplateView): @@ -13,7 +17,5 @@ class BoringView(TemplateView): urlpatterns = [ - url(r'^something', - BoringView.as_view(), - name='something'), + path(r"^something", BoringView.as_view(), name="something"), ] diff --git a/pylint_django/tests/input/func_noerror_uuid_field.py b/pylint_django/tests/input/func_noerror_uuid_field.py index e6c79987..0d38dfc9 100644 --- a/pylint_django/tests/input/func_noerror_uuid_field.py +++ b/pylint_django/tests/input/func_noerror_uuid_field.py @@ -3,6 +3,7 @@ """ # pylint: disable=missing-class-docstring,missing-function-docstring from __future__ import print_function + from django.db import models diff --git a/pylint_django/tests/input/func_unused_arguments.py b/pylint_django/tests/input/func_unused_arguments.py index 61b2e23a..f7e40b9e 100644 --- a/pylint_django/tests/input/func_unused_arguments.py +++ b/pylint_django/tests/input/func_unused_arguments.py @@ -17,24 +17,25 @@ def user_detail(request, user_id): # [unused-argument] # nothing is done with user_id - return JsonResponse({'username': 'steve'}) + return JsonResponse({"username": "steve"}) class UserView(View): def get(self, request, user_id): # [unused-argument] # nothing is done with user_id - return JsonResponse({'username': 'steve'}) + return JsonResponse({"username": "steve"}) # The following views are already covered in other test cases. # They are included here for completeness sake. + def welcome_view(request): # just don't use `request' b/c we could have Django views # which never use it! - return JsonResponse({'message': 'welcome'}) + return JsonResponse({"message": "welcome"}) class CBV(View): def get(self, request): - return JsonResponse({'message': 'hello world'}) + return JsonResponse({"message": "hello world"}) diff --git a/pylint_django/tests/input/func_unused_arguments.txt b/pylint_django/tests/input/func_unused_arguments.txt index 36f0b607..af5222bc 100644 --- a/pylint_django/tests/input/func_unused_arguments.txt +++ b/pylint_django/tests/input/func_unused_arguments.txt @@ -1,2 +1,2 @@ -unused-argument:18:25:user_detail:Unused argument 'user_id':HIGH -unused-argument:24:27:UserView.get:Unused argument 'user_id':INFERENCE +unused-argument:18:25:18:32:user_detail:Unused argument 'user_id':HIGH +unused-argument:24:27:24:34:UserView.get:Unused argument 'user_id':INFERENCE diff --git a/pylint_django/tests/input/migrations/0001_noerror_initial.py b/pylint_django/tests/input/migrations/0001_noerror_initial.py index 81f00d3d..41186766 100644 --- a/pylint_django/tests/input/migrations/0001_noerror_initial.py +++ b/pylint_django/tests/input/migrations/0001_noerror_initial.py @@ -8,12 +8,12 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='TestRun', + name="TestRun", fields=[ - ('id', models.AutoField(serialize=False, primary_key=True)), - ('summary', models.TextField()), - ('environment_id', models.IntegerField(default=0)), - ('auto_update_run_status', models.BooleanField(default=False)), + ("id", models.AutoField(serialize=False, primary_key=True)), + ("summary", models.TextField()), + ("environment_id", models.IntegerField(default=0)), + ("auto_update_run_status", models.BooleanField(default=False)), ], ), ] diff --git a/pylint_django/tests/input/migrations/0002_new_column.py b/pylint_django/tests/input/migrations/0002_new_column.py index f80eb717..0aee5447 100644 --- a/pylint_django/tests/input/migrations/0002_new_column.py +++ b/pylint_django/tests/input/migrations/0002_new_column.py @@ -14,20 +14,21 @@ """ # pylint: disable=missing-docstring, invalid-name from datetime import timedelta + from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('input', '0001_noerror_initial'), + ("input", "0001_noerror_initial"), ] operations = [ # add a timedelta field migrations.AddField( # [new-db-field-with-default] - model_name='testrun', - name='estimated_time', + model_name="testrun", + name="estimated_time", field=models.DurationField(default=timedelta(0)), ), ] diff --git a/pylint_django/tests/input/migrations/0002_new_column.txt b/pylint_django/tests/input/migrations/0002_new_column.txt index fd085668..794ac3e8 100644 --- a/pylint_django/tests/input/migrations/0002_new_column.txt +++ b/pylint_django/tests/input/migrations/0002_new_column.txt @@ -1 +1 @@ -new-db-field-with-default:28:8:Migration:pylint_django.tests.input.migrations.0002_new_column AddField with default value +new-db-field-with-default:29:8:33:9:Migration:pylint_django.tests.input.migrations.0002_new_column AddField with default value:UNDEFINED diff --git a/pylint_django/tests/input/migrations/0003_without_backwards.py b/pylint_django/tests/input/migrations/0003_without_backwards.py index 55a7d746..a61e5511 100644 --- a/pylint_django/tests/input/migrations/0003_without_backwards.py +++ b/pylint_django/tests/input/migrations/0003_without_backwards.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring, invalid-name +# pylint: disable=missing-docstring, invalid-name, line-too-long from django.db import migrations @@ -10,10 +10,7 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython(), # [missing-backwards-migration-callable] - migrations.RunPython( # [missing-backwards-migration-callable] - forwards_test), - migrations.RunPython( # [missing-backwards-migration-callable] - code=forwards_test), - migrations.RunPython( # [missing-backwards-migration-callable] - code=forwards_test, atomic=False) + migrations.RunPython(forwards_test), # [missing-backwards-migration-callable] + migrations.RunPython(code=forwards_test), # [missing-backwards-migration-callable] + migrations.RunPython(code=forwards_test, atomic=False), # [missing-backwards-migration-callable] ] diff --git a/pylint_django/tests/input/migrations/0003_without_backwards.txt b/pylint_django/tests/input/migrations/0003_without_backwards.txt index 2ed38cb7..8b83fc1f 100644 --- a/pylint_django/tests/input/migrations/0003_without_backwards.txt +++ b/pylint_django/tests/input/migrations/0003_without_backwards.txt @@ -1,4 +1,4 @@ -missing-backwards-migration-callable:12:8:Migration:Always include backwards migration callable -missing-backwards-migration-callable:13:8:Migration:Always include backwards migration callable -missing-backwards-migration-callable:15:8:Migration:Always include backwards migration callable -missing-backwards-migration-callable:17:8:Migration:Always include backwards migration callable +missing-backwards-migration-callable:12:8:12:30:Migration:Always include backwards migration callable:UNDEFINED +missing-backwards-migration-callable:13:8:13:43:Migration:Always include backwards migration callable:UNDEFINED +missing-backwards-migration-callable:14:8:14:48:Migration:Always include backwards migration callable:UNDEFINED +missing-backwards-migration-callable:15:8:15:62:Migration:Always include backwards migration callable:UNDEFINED diff --git a/pylint_django/tests/input/migrations/0004_noerror_with_backwards.py b/pylint_django/tests/input/migrations/0004_noerror_with_backwards.py index 9ed0e9b9..e385e02e 100644 --- a/pylint_django/tests/input/migrations/0004_noerror_with_backwards.py +++ b/pylint_django/tests/input/migrations/0004_noerror_with_backwards.py @@ -15,5 +15,5 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython(forwards_test, backwards_test), migrations.RunPython(forwards_test, reverse_code=backwards_test), - migrations.RunPython(code=forwards_test, reverse_code=backwards_test) + migrations.RunPython(code=forwards_test, reverse_code=backwards_test), ] diff --git a/pylint_django/tests/input/models/__init__.py b/pylint_django/tests/input/models/__init__.py index 66527f20..c88aeda2 100644 --- a/pylint_django/tests/input/models/__init__.py +++ b/pylint_django/tests/input/models/__init__.py @@ -1,4 +1,3 @@ from .author import Author - -__all__ = ('Author',) +__all__ = ("Author",) diff --git a/pylint_django/tests/input/models/author.py b/pylint_django/tests/input/models/author.py index e09af25b..f1b36330 100644 --- a/pylint_django/tests/input/models/author.py +++ b/pylint_django/tests/input/models/author.py @@ -4,4 +4,4 @@ class Author(models.Model): class Meta: - app_label = 'test_app' + app_label = "test_app" diff --git a/pylint_django/tests/input/models/func_noerror_foreign_key_key_cls_unbound_in_same_package.py b/pylint_django/tests/input/models/func_noerror_foreign_key_key_cls_unbound_in_same_package.py index fab1e4b5..d25da4ad 100644 --- a/pylint_django/tests/input/models/func_noerror_foreign_key_key_cls_unbound_in_same_package.py +++ b/pylint_django/tests/input/models/func_noerror_foreign_key_key_cls_unbound_in_same_package.py @@ -17,7 +17,7 @@ class FairyTail(models.Model): # fails with "UnboundLocalError: local variable 'key_cls' referenced before assignment" # when 'Author' model comes from same models package - author = models.ForeignKey(to='Author', null=True, on_delete=models.CASCADE) + author = models.ForeignKey(to="Author", null=True, on_delete=models.CASCADE) def get_author_name(self): return self.author.id # disable via no-member diff --git a/pylint_django/tests/input/test_app/apps.py b/pylint_django/tests/input/test_app/apps.py index fc04070e..f168fd5f 100644 --- a/pylint_django/tests/input/test_app/apps.py +++ b/pylint_django/tests/input/test_app/apps.py @@ -2,4 +2,4 @@ class TestAppConfig(AppConfig): - name = 'test_app' + name = "test_app" diff --git a/pylint_django/tests/settings.py b/pylint_django/tests/settings.py index eb12bc79..075f4207 100644 --- a/pylint_django/tests/settings.py +++ b/pylint_django/tests/settings.py @@ -1,12 +1,12 @@ -SECRET_KEY = 'fake-key' +SECRET_KEY = "fake-key" INSTALLED_APPS = [ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'test_app', + "django.contrib.auth", + "django.contrib.contenttypes", + "test_app", ] MIDDLEWARE = [ - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", ] diff --git a/pylint_django/tests/test_func.py b/pylint_django/tests/test_func.py index ef2a03a0..4df4181a 100644 --- a/pylint_django/tests/test_func.py +++ b/pylint_django/tests/test_func.py @@ -1,18 +1,18 @@ import csv import os import sys -import pytest import pylint +import pytest - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pylint_django.tests.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pylint_django.tests.settings") try: # pylint 2.5: test_functional has been moved to pylint.testutils from pylint.testutils import FunctionalTestFile, LintModuleTest if "test" not in csv.list_dialects(): + class test_dialect(csv.excel): delimiter = ":" lineterminator = "\n" @@ -20,16 +20,16 @@ class test_dialect(csv.excel): csv.register_dialect("test", test_dialect) except (ImportError, AttributeError): # specify directly the directory containing test_functional.py - test_functional_dir = os.getenv('PYLINT_TEST_FUNCTIONAL_DIR', '') + test_functional_dir = os.getenv("PYLINT_TEST_FUNCTIONAL_DIR", "") # otherwise look for in in ~/pylint/tests - pylint 2.4 # this is the pylint git checkout dir, not the pylint module dir if not os.path.isdir(test_functional_dir): - test_functional_dir = os.path.join(os.getenv('HOME', '/home/travis'), 'pylint', 'tests') + test_functional_dir = os.path.join(os.getenv("HOME", "/home/travis"), "pylint", "tests") # or site-packages/pylint/test/ - pylint before 2.4 if not os.path.isdir(test_functional_dir): - test_functional_dir = os.path.join(os.path.dirname(pylint.__file__), 'test') + test_functional_dir = os.path.join(os.path.dirname(pylint.__file__), "test") sys.path.append(test_functional_dir) @@ -38,33 +38,35 @@ class test_dialect(csv.excel): # alter sys.path again because the tests now live as a subdirectory # of pylint_django -sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..")) # so we can find migrations -sys.path.append(os.path.join(os.path.dirname(__file__), 'input')) +sys.path.append(os.path.join(os.path.dirname(__file__), "input")) class PylintDjangoLintModuleTest(LintModuleTest): """ - Only used so that we can load this plugin into the linter! + Only used so that we can load this plugin into the linter! """ + def __init__(self, test_file): super(PylintDjangoLintModuleTest, self).__init__(test_file) - self._linter.load_plugin_modules(['pylint_django']) + self._linter.load_plugin_modules(["pylint_django"]) self._linter.load_plugin_configuration() class PylintDjangoMigrationsTest(PylintDjangoLintModuleTest): """ - Only used so that we can load - pylint_django.checkers.migrations into the linter! + Only used so that we can load + pylint_django.checkers.migrations into the linter! """ + def __init__(self, test_file): super().__init__(test_file) - self._linter.load_plugin_modules(['pylint_django.checkers.migrations']) + self._linter.load_plugin_modules(["pylint_django.checkers.migrations"]) self._linter.load_plugin_configuration() -def get_tests(input_dir='input', sort=False): +def get_tests(input_dir="input", sort=False): def _file_name(test): return test.base @@ -73,7 +75,7 @@ def _file_name(test): suite = [] for fname in os.listdir(input_dir): - if fname != '__init__.py' and fname.endswith('.py'): + if fname != "__init__.py" and fname.endswith(".py"): suite.append(FunctionalTestFile(input_dir, fname)) # when testing the migrations plugin we need to sort by input file name @@ -86,7 +88,7 @@ def _file_name(test): TESTS = get_tests() -TESTS.extend(get_tests('input/models')) +TESTS.extend(get_tests("input/models")) TESTS_NAMES = [t.base for t in TESTS] @@ -99,7 +101,7 @@ def test_everything(test_file): # NOTE: define tests for the migrations checker! -MIGRATIONS_TESTS = get_tests('input/migrations', True) +MIGRATIONS_TESTS = get_tests("input/migrations", True) MIGRATIONS_TESTS_NAMES = [t.base for t in MIGRATIONS_TESTS] @@ -110,5 +112,5 @@ def test_migrations_plugin(test_file): LintTest._runTest() -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(pytest.main(sys.argv)) diff --git a/pylint_django/transforms/__init__.py b/pylint_django/transforms/__init__.py index 5e34abe4..11c56715 100644 --- a/pylint_django/transforms/__init__.py +++ b/pylint_django/transforms/__init__.py @@ -21,15 +21,16 @@ def _add_transform(package_name): def fake_module_builder(): """ - Build a fake module to use within transformations. - @package_name is a parameter from the outer scope b/c according to - the docs this can't receive any parameters. - http://pylint.pycqa.org/projects/astroid/en/latest/extending.html?highlight=MANAGER#module-extender-transforms + Build a fake module to use within transformations. + @package_name is a parameter from the outer scope b/c according to + the docs this can't receive any parameters. + http://pylint.pycqa.org/projects/astroid/en/latest/extending.html?highlight=MANAGER#module-extender-transforms """ - transforms_dir = os.path.join(os.path.dirname(__file__), 'transforms') - fake_module_path = os.path.join(transforms_dir, '%s.py' % re.sub(r'\.', '_', package_name)) + transforms_dir = os.path.join(os.path.dirname(__file__), "transforms") + transformed_name = re.sub(r"\.", "_", package_name) + fake_module_path = os.path.join(transforms_dir, f"{transformed_name}.py") - with open(fake_module_path) as modulefile: + with open(fake_module_path, encoding="utf-8") as modulefile: fake_module = modulefile.read() return astroid.builder.AstroidBuilder(astroid.MANAGER).string_build(fake_module) @@ -37,6 +38,6 @@ def fake_module_builder(): astroid.register_module_extender(astroid.MANAGER, package_name, fake_module_builder) -_add_transform('django.utils.translation') +_add_transform("django.utils.translation") # register transform for FileField/ImageField, see #60 -_add_transform('django.db.models.fields.files') +_add_transform("django.db.models.fields.files") diff --git a/pylint_django/transforms/fields.py b/pylint_django/transforms/fields.py index 5d50c41e..f25eaa68 100644 --- a/pylint_django/transforms/fields.py +++ b/pylint_django/transforms/fields.py @@ -1,28 +1,44 @@ -from astroid import ( - MANAGER, scoped_nodes, nodes, inference_tip, - AstroidImportError -) +from astroid import MANAGER, AstroidImportError, inference_tip, nodes, scoped_nodes from pylint_django import utils - -_STR_FIELDS = ('CharField', 'SlugField', 'URLField', 'TextField', 'EmailField', - 'CommaSeparatedIntegerField', 'FilePathField', 'GenericIPAddressField', - 'IPAddressField', 'RegexField', 'SlugField') -_INT_FIELDS = ('IntegerField', 'SmallIntegerField', 'BigIntegerField', - 'PositiveIntegerField', 'PositiveSmallIntegerField') -_BOOL_FIELDS = ('BooleanField', 'NullBooleanField') -_RANGE_FIELDS = ('RangeField', 'IntegerRangeField', 'BigIntegerRangeField', - 'FloatRangeField', 'DateTimeRangeField', 'DateRangeField') +_STR_FIELDS = ( + "CharField", + "SlugField", + "URLField", + "TextField", + "EmailField", + "CommaSeparatedIntegerField", + "FilePathField", + "GenericIPAddressField", + "IPAddressField", + "RegexField", + "SlugField", +) +_INT_FIELDS = ( + "IntegerField", + "SmallIntegerField", + "BigIntegerField", + "PositiveIntegerField", + "PositiveSmallIntegerField", +) +_BOOL_FIELDS = ("BooleanField", "NullBooleanField") +_RANGE_FIELDS = ( + "RangeField", + "IntegerRangeField", + "BigIntegerRangeField", + "FloatRangeField", + "DateTimeRangeField", + "DateRangeField", +) def is_model_field(cls): - return cls.qname().startswith('django.db.models.fields') or \ - cls.qname().startswith('django.contrib.postgres.fields') + return cls.qname().startswith("django.db.models.fields") or cls.qname().startswith("django.contrib.postgres.fields") def is_form_field(cls): - return cls.qname().startswith('django.forms.fields') + return cls.qname().startswith("django.forms.fields") def is_model_or_form_field(cls): @@ -32,38 +48,38 @@ def is_model_or_form_field(cls): def apply_type_shim(cls, _context=None): # noqa if cls.name in _STR_FIELDS: - base_nodes = scoped_nodes.builtin_lookup('str') + base_nodes = scoped_nodes.builtin_lookup("str") elif cls.name in _INT_FIELDS: - base_nodes = scoped_nodes.builtin_lookup('int') + base_nodes = scoped_nodes.builtin_lookup("int") elif cls.name in _BOOL_FIELDS: - base_nodes = scoped_nodes.builtin_lookup('bool') - elif cls.name == 'FloatField': - base_nodes = scoped_nodes.builtin_lookup('float') - elif cls.name == 'DecimalField': + base_nodes = scoped_nodes.builtin_lookup("bool") + elif cls.name == "FloatField": + base_nodes = scoped_nodes.builtin_lookup("float") + elif cls.name == "DecimalField": try: - base_nodes = MANAGER.ast_from_module_name('_decimal').lookup('Decimal') + base_nodes = MANAGER.ast_from_module_name("_decimal").lookup("Decimal") except AstroidImportError: - base_nodes = MANAGER.ast_from_module_name('_pydecimal').lookup('Decimal') - elif cls.name in ('SplitDateTimeField', 'DateTimeField'): - base_nodes = MANAGER.ast_from_module_name('datetime').lookup('datetime') - elif cls.name == 'TimeField': - base_nodes = MANAGER.ast_from_module_name('datetime').lookup('time') - elif cls.name == 'DateField': - base_nodes = MANAGER.ast_from_module_name('datetime').lookup('date') - elif cls.name == 'DurationField': - base_nodes = MANAGER.ast_from_module_name('datetime').lookup('timedelta') - elif cls.name == 'UUIDField': - base_nodes = MANAGER.ast_from_module_name('uuid').lookup('UUID') - elif cls.name == 'ManyToManyField': - base_nodes = MANAGER.ast_from_module_name('django.db.models.query').lookup('QuerySet') - elif cls.name in ('ImageField', 'FileField'): - base_nodes = MANAGER.ast_from_module_name('django.core.files.base').lookup('File') - elif cls.name == 'ArrayField': - base_nodes = scoped_nodes.builtin_lookup('list') - elif cls.name in ('HStoreField', 'JSONField'): - base_nodes = scoped_nodes.builtin_lookup('dict') + base_nodes = MANAGER.ast_from_module_name("_pydecimal").lookup("Decimal") + elif cls.name in ("SplitDateTimeField", "DateTimeField"): + base_nodes = MANAGER.ast_from_module_name("datetime").lookup("datetime") + elif cls.name == "TimeField": + base_nodes = MANAGER.ast_from_module_name("datetime").lookup("time") + elif cls.name == "DateField": + base_nodes = MANAGER.ast_from_module_name("datetime").lookup("date") + elif cls.name == "DurationField": + base_nodes = MANAGER.ast_from_module_name("datetime").lookup("timedelta") + elif cls.name == "UUIDField": + base_nodes = MANAGER.ast_from_module_name("uuid").lookup("UUID") + elif cls.name == "ManyToManyField": + base_nodes = MANAGER.ast_from_module_name("django.db.models.query").lookup("QuerySet") + elif cls.name in ("ImageField", "FileField"): + base_nodes = MANAGER.ast_from_module_name("django.core.files.base").lookup("File") + elif cls.name == "ArrayField": + base_nodes = scoped_nodes.builtin_lookup("list") + elif cls.name in ("HStoreField", "JSONField"): + base_nodes = scoped_nodes.builtin_lookup("dict") elif cls.name in _RANGE_FIELDS: - base_nodes = MANAGER.ast_from_module_name('psycopg2._range').lookup('Range') + base_nodes = MANAGER.ast_from_module_name("psycopg2._range").lookup("Range") else: return iter([cls]) diff --git a/pylint_django/transforms/foreignkey.py b/pylint_django/transforms/foreignkey.py index eca85aa1..3f3307cd 100644 --- a/pylint_django/transforms/foreignkey.py +++ b/pylint_django/transforms/foreignkey.py @@ -1,10 +1,7 @@ from itertools import chain -from astroid import ( - MANAGER, nodes, InferenceError, inference_tip, - UseInferenceDefault -) -from astroid.nodes import ClassDef, Attribute +from astroid import MANAGER, InferenceError, UseInferenceDefault, inference_tip, nodes +from astroid.nodes import Attribute, ClassDef from pylint_django.utils import node_is_subclass @@ -22,27 +19,23 @@ def is_foreignkey_in_class(node): attr = node.func.name else: return False - return attr in ('OneToOneField', 'ForeignKey') + return attr in ("OneToOneField", "ForeignKey") def _get_model_class_defs_from_module(module, model_name, module_name): class_defs = [] for module_node in module.lookup(model_name)[1]: - if (isinstance(module_node, nodes.ClassDef) - and node_is_subclass(module_node, 'django.db.models.base.Model')): + if isinstance(module_node, nodes.ClassDef) and node_is_subclass(module_node, "django.db.models.base.Model"): class_defs.append(module_node) elif isinstance(module_node, nodes.ImportFrom): imported_module = module_node.do_import_module() - class_defs.extend( - _get_model_class_defs_from_module( - imported_module, model_name, module_name - ) - ) + class_defs.extend(_get_model_class_defs_from_module(imported_module, model_name, module_name)) return class_defs def _module_name_from_django_model_resolution(model_name, module_name): import django # pylint: disable=import-outside-toplevel + django.setup() from django.apps import apps # pylint: disable=import-outside-toplevel @@ -53,11 +46,13 @@ def _module_name_from_django_model_resolution(model_name, module_name): def infer_key_classes(node, context=None): - from django.core.exceptions import ImproperlyConfigured # pylint: disable=import-outside-toplevel + from django.core.exceptions import ( # pylint: disable=import-outside-toplevel + ImproperlyConfigured, + ) keyword_args = [] if node.keywords: - keyword_args = [kw.value for kw in node.keywords if kw.arg == 'to'] + keyword_args = [kw.value for kw in node.keywords if kw.arg == "to"] all_args = chain(node.args, keyword_args) for arg in all_args: @@ -77,16 +72,16 @@ def infer_key_classes(node, context=None): elif isinstance(arg, nodes.Const): try: # can be 'self' , 'Model' or 'app.Model' - if arg.value == 'self': - module_name = '' + if arg.value == "self": + module_name = "" # for relations with `to` first parent be Keyword(arg='to') # and we need to go deeper in parent tree to get model name - if isinstance(arg.parent, nodes.Keyword) and arg.parent.arg == 'to': + if isinstance(arg.parent, nodes.Keyword) and arg.parent.arg == "to": model_name = arg.parent.parent.parent.parent.name else: model_name = arg.parent.parent.parent.name else: - module_name, _, model_name = arg.value.rpartition('.') + module_name, _, model_name = arg.value.rpartition(".") except AttributeError: break @@ -98,7 +93,7 @@ def infer_key_classes(node, context=None): current_module = current_module.parent.frame() module_name = current_module.name - elif not module_name.endswith('models'): + elif not module_name.endswith("models"): # otherwise Django allows specifying an app name first, e.g. # ForeignKey('auth.User') try: @@ -107,11 +102,13 @@ def infer_key_classes(node, context=None): # If Django's model resolution fails we try to convert that to # 'auth.models', 'User' which works nicely with the `endswith()` # comparison below - module_name += '.models' + module_name += ".models" except ImproperlyConfigured as exep: - raise RuntimeError("DJANGO_SETTINGS_MODULE required for resolving ForeignKey " - "string references, see Usage section in README at " - "https://pypi.org/project/pylint-django/!") from exep + raise RuntimeError( + "DJANGO_SETTINGS_MODULE required for resolving ForeignKey " + "string references, see Usage section in README at " + "https://pypi.org/project/pylint-django/!" + ) from exep # ensure that module is loaded in astroid_cache, for cases when models is a package if module_name not in MANAGER.astroid_cache: @@ -123,9 +120,7 @@ def infer_key_classes(node, context=None): # which *we think* they are defined. This will prevent inferring # other models of the same name which are found elsewhere! if model_name in module.locals and module.name.endswith(module_name): - class_defs = _get_model_class_defs_from_module( - module, model_name, module_name - ) + class_defs = _get_model_class_defs_from_module(module, model_name, module_name) if class_defs: return iter([class_defs[0].instantiate_class()]) @@ -135,5 +130,4 @@ def infer_key_classes(node, context=None): def add_transform(manager): - manager.register_transform(nodes.Call, inference_tip(infer_key_classes), - is_foreignkey_in_class) + manager.register_transform(nodes.Call, inference_tip(infer_key_classes), is_foreignkey_in_class) diff --git a/pylint_django/transforms/transforms/django_db_models_fields_files.py b/pylint_django/transforms/transforms/django_db_models_fields_files.py index c9fe3c0d..dd190a4d 100644 --- a/pylint_django/transforms/transforms/django_db_models_fields_files.py +++ b/pylint_django/transforms/transforms/django_db_models_fields_files.py @@ -3,7 +3,7 @@ class FileField(django_fields.FieldFile, django_fields.FileField): - def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, **kwargs): + def __init__(self, verbose_name=None, name=None, upload_to="", storage=None, **kwargs): django_fields.FileField.__init__(verbose_name, name, upload_to, storage, **kwargs) diff --git a/pylint_django/transforms/transforms/django_utils_translation.py b/pylint_django/transforms/transforms/django_utils_translation.py index e193a860..d174376d 100644 --- a/pylint_django/transforms/transforms/django_utils_translation.py +++ b/pylint_django/transforms/transforms/django_utils_translation.py @@ -1,5 +1,5 @@ def gettext_lazy(_): - return '' + return "" ugettext_lazy = gettext_lazy # pylint:disable=invalid-name diff --git a/pylint_django/utils.py b/pylint_django/utils.py index 883ef122..205e3b3f 100644 --- a/pylint_django/utils.py +++ b/pylint_django/utils.py @@ -1,7 +1,7 @@ """Utils.""" import sys -import astroid +import astroid from astroid.bases import Instance from astroid.exceptions import InferenceError from astroid.nodes import ClassDef @@ -37,4 +37,4 @@ def is_migrations_module(node): if not isinstance(node, astroid.Module): return False - return 'migrations' in node.path[0] and not node.path[0].endswith('__init__.py') + return "migrations" in node.path[0] and not node.path[0].endswith("__init__.py") diff --git a/setup.py b/setup.py index 3472cc8c..076fa4fc 100644 --- a/setup.py +++ b/setup.py @@ -2,49 +2,50 @@ """ Setup module for Pylint plugin for Django. """ -from setuptools import setup, find_packages +from setuptools import find_packages, setup -LONG_DESCRIPTION = open('README.rst').read() + "\n" + open('CHANGELOG.rst').read() +with open("README.rst", encoding="utf-8") as readme, open("CHANGELOG.rst", encoding="utf-8") as changelog: + LONG_DESCRIPTION = readme.read() + "\n" + changelog.read() setup( - name='pylint-django', - url='https://github.com/PyCQA/pylint-django', - author='landscape.io', - author_email='code@landscape.io', - description='A Pylint plugin to help Pylint understand the Django web framework', + name="pylint-django", + url="https://github.com/PyCQA/pylint-django", + author="landscape.io", + author_email="code@landscape.io", + description="A Pylint plugin to help Pylint understand the Django web framework", long_description=LONG_DESCRIPTION, - version='2.4.4', + version="2.4.4", packages=find_packages(), include_package_data=True, install_requires=[ - 'pylint-plugin-utils>=0.5', - 'pylint>=2.0', + "pylint-plugin-utils>=0.5", + "pylint>=2.0", ], extras_require={ - 'with_django': ['Django'], - 'for_tests': ['django_tables2', 'factory-boy', 'coverage', 'pytest'], + "with_django": ["Django"], + "for_tests": ["django_tables2", "factory-boy", "coverage", "pytest", "wheel"], }, - license='GPLv2', + license="GPLv2", classifiers=[ - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Operating System :: Unix', - 'Topic :: Software Development :: Quality Assurance', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Framework :: Django :: 1.11', - 'Framework :: Django :: 2.0', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.0', - 'Framework :: Django :: 3.1', - 'Framework :: Django :: 3.2', + "Environment :: Console", + "Intended Audience :: Developers", + "Operating System :: Unix", + "Topic :: Software Development :: Quality Assurance", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Framework :: Django :: 1.11", + "Framework :: Django :: 2.0", + "Framework :: Django :: 2.2", + "Framework :: Django :: 3.0", + "Framework :: Django :: 3.1", + "Framework :: Django :: 3.2", ], - keywords=['pylint', 'django', 'plugin'], + keywords=["pylint", "django", "plugin"], zip_safe=False, project_urls={ - 'Changelog': 'https://github.com/PyCQA/pylint-django/blob/master/CHANGELOG.rst', + "Changelog": "https://github.com/PyCQA/pylint-django/blob/master/CHANGELOG.rst", }, ) diff --git a/tox.ini b/tox.ini index 796a3641..59df6651 100644 --- a/tox.ini +++ b/tox.ini @@ -13,16 +13,16 @@ envlist = requires = pip >=21.0.1 - tox-travis + tox [testenv] commands = - django_not_installed: bash -c \'pylint --load-plugins=pylint_django setup.py | grep django-not-available\' - django_is_installed: pylint --load-plugins=pylint_django --disable=E5110 setup.py + django_not_installed: pylint --rcfile=tox.ini --load-plugins=pylint_django setup.py | grep django-not-configured + django_is_installed: pylint --rcfile=tox.ini --load-plugins=pylint_django --disable=E5110 setup.py flake8: flake8 pylint: pylint --rcfile=tox.ini -d missing-docstring,too-many-branches,too-many-return-statements,too-many-ancestors,fixme --ignore=tests pylint_django setup - readme: bash -c \'python setup.py -q sdist && twine check dist/*\' - py{36}-django{111,20,-master}: coverage run pylint_django/tests/test_func.py -v + readme: bash -c "python setup.py -q sdist && twine check dist/*" + py{36}-django{111,20,-main}: coverage run pylint_django/tests/test_func.py -v py{36,37,38,39}-django{22,30,31,32}: coverage run pylint_django/tests/test_func.py -v clean: find . -type f -name '*.pyc' -delete clean: find . -type d -name __pycache__ -delete @@ -33,6 +33,7 @@ deps = pylint: pylint pylint: Django readme: twine + readme: wheel django111: Django>=1.11,<2.0 django20: Django>=2.0,<2.1 django21: Django>=2.1,<2.2 @@ -40,30 +41,22 @@ deps = django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 django32: Django>=3.2,<4.0 - django-master: Django - django-master: git+https://github.com/pycqa/astroid@master - django-master: git+https://github.com/pycqa/pylint@master + django-main: Django + django-main: git+https://github.com/pycqa/astroid@main + django-main: git+https://github.com/pycqa/pylint@main setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 PYTHONPATH = . allowlist_externals = - django_not_installed: bash + django_not_installed: pylint readme: bash - py{36}-django{111,20,-master}: coverage + py{36}-django{111,20,-main}: coverage py{36,37,38,39}-django{22,30,31,32}: coverage clean: find clean: rm -[travis:env] -DJANGO = - 1.11: django111 - 2.0: django20 - 2.1: django21 - 2.2: django22 - 3.0: django30 - 3.1: django31 - 3.2: django32 - master: django-master - [flake8] -max-line-length = 120 +max-line-length = 140 + +[FORMAT] +max-line-length=140