diff --git a/.travis.yml b/.travis.yml index a26e70ca..ea495b37 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,21 @@ sudo: false language: python python: - - "2.7" - - "3.3" - - "3.4" - "3.5" + - "3.6" env: - - DJANGO='Django>=1.7,<1.8' SKIP='2.6 3.5' - - DJANGO='Django>=1.8,<1.9' SKIP='2.6' - - DJANGO='Django>=1.9,<1.10' SKIP='2.6 3.3' - - DJANGO='Django>=1.10,<1.11' SKIP='2.6 3.3' + - DJANGO="1.11,<2.0" + - DJANGO="2.0,<3.0" + install: - scripts/travis-install.sh + - pip install coverage coveralls djangorestframework psycopg2 + - pip install "Django>=$DJANGO" + - pip install git+https://github.com/landscapeio/pylint-plugin-utils.git@master script: - scripts/travis-build.sh + - PYTHONPATH=. coverage run test/test_func.py after_success: coveralls +notifications: + email: + on_failure: change + on_success: never diff --git a/CHANGELOG.md b/CHANGELOG.md index ed288aeb..43abd892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## Version 0.8.0 (unreleased) + +* [#109](http://github.com/landscapeio/pylint-django/pull/109), + adding 'urlpatterns', 'register', 'app_name' to good names. Obsoletes + [#111](http://github.com/landscapeio/pylint-django/pull/111), fixes + [#108](http://github.com/landscapeio/pylint-django/issues/108) + (Vinay Pai) +* Add 'handler500' to good names (Mr. Senko) +* [#103](http://github.com/landscapeio/pylint-django/pull/103): + Support factory_boy's DjangoModelFactory Meta class (Konstantinos Koukopoulos) +* [#100](https://github.com/landscapeio/pylint-django/pull/100): + Fix E1101:Instance of '__proxy__' has no 'format' member' when using .format() + on a ugettext_lazy translation. Fixes + [#80](https://github.com/landscapeio/pylint-django/issues/80) (canarduck) +* [#99](https://github.com/landscapeio/pylint-django/pull/99): + Add tests and transforms for DurationField, fixes + [#95](https://github.com/landscapeio/pylint-django/issues/95) (James M. Allen) +* [#92](https://github.com/landscapeio/pylint-django/pull/92): + Add json field to WSGIRequest proxy (sjk4sc) +* [#84](https://github.com/landscapeio/pylint-django/pull/84): + Add support for django.contrib.postgres.fields and UUIDField (Villiers Strauss) +* Stop testing with older Django versions. Currently testing with Django 1.11.x and 2.0 +* Stop testing on Python 2, no functional changes in the source code though +* Update tests for latest version of pylint (>=1.8) + ## Version 0.7.4 * [#88](https://github.com/landscapeio/pylint-django/pull/88) Fixed builds with Django 1.10 (thanks to [federicobond](https://github.com/federicobond)) * [#91](https://github.com/landscapeio/pylint-django/pull/91) Fixed race condition when running with pylint parallel execution mode (thanks to [jeremycarroll](https://github.com/jeremycarroll)) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index e3515a83..8f82e36d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -5,4 +5,5 @@ * [jproffitt](https://github.com/jproffitt) * [lhupfeldt](https://github.com/lhupfeldt) * [smirolo](https://github.com/smirolo) -* [mbertolacci](https://github.com/mbertolacci) \ No newline at end of file +* [mbertolacci](https://github.com/mbertolacci) +* [atodorov](https://github.com/atodorov) diff --git a/pylint_django/augmentations/__init__.py b/pylint_django/augmentations/__init__.py index 23eb1cdd..2b54f263 100644 --- a/pylint_django/augmentations/__init__.py +++ b/pylint_django/augmentations/__init__.py @@ -407,6 +407,16 @@ def is_model_meta_subclass(node): return node_is_subclass(node.parent, *parents) +def is_model_factory_meta_subclass(node): + """Checks that node is derivative of DjangoModelFactory class.""" + if node.name != 'Meta' or not isinstance(node.parent, ClassDef): + return False + + parents = ('factory.django.DjangoModelFactory', + '.DjangoModelFactory',) + return node_is_subclass(node.parent, *parents) + + def is_model_mpttmeta_subclass(node): """Checks that node is derivative of MPTTMeta class.""" if node.name != 'MPTTMeta' or not isinstance(node.parent, ClassDef): @@ -715,15 +725,16 @@ def apply_augmentations(linter): suppress_message(linter, _visit_attribute(TypeChecker), 'E1101', 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, _visit_class(MisdesignChecker), 'R0901', is_class('django.views.generic.edit.FormView')) + suppress_message(linter, _visit_class(MisdesignChecker), 'too-many-ancestors', is_class('django.views.generic.edit.FormView')) # model forms have no __init__ method anywhere in their bases suppress_message(linter, _visit_class(ClassChecker), 'W0232', is_class('django.forms.models.ModelForm')) # forms implement __getitem__ but not __len__, thus raising a "Badly implemented container" warning which - # we will suppress. - suppress_message(linter, _leave_class(MisdesignChecker), 'R0924', is_class('django.forms.forms.Form')) - suppress_message(linter, _leave_class(MisdesignChecker), 'R0924', is_class('django.forms.models.ModelForm')) + # we will suppress. NOTE: removed from pylint, https://github.com/PyCQA/pylint/issues/112 + # keeping here in case it gets re-implemented + # suppress_message(linter, _leave_class(MisdesignChecker), 'R0924', is_class('django.forms.forms.Form')) + # suppress_message(linter, _leave_class(MisdesignChecker), 'R0924', is_class('django.forms.models.ModelForm')) # Meta suppress_message(linter, _visit_class(DocStringChecker), 'missing-docstring', is_model_meta_subclass) @@ -739,14 +750,6 @@ def apply_augmentations(linter): suppress_message(linter, _visit_class(ClassChecker), 'no-init', is_model_media_subclass) suppress_message(linter, _leave_class(MisdesignChecker), 'too-few-public-methods', is_model_media_subclass) - # Too few public methods started appearing for Views and Models as part of Pylint>=1.4 / astroid>=1.3.3 - # Not sure why, suspect this is a failure to get the parent classes somewhere - # For now, just suppress it on models and views - suppress_message(linter, _leave_class(MisdesignChecker), 'too-few-public-methods', - is_class('.Model')) - suppress_message(linter, _leave_class(MisdesignChecker), 'too-few-public-methods', - is_class('.View')) - # Admin # Too many public methods (40+/20) # TODO: Count public methods of django.contrib.admin.options.ModelAdmin and increase @@ -772,6 +775,12 @@ def apply_augmentations(linter): suppress_message(linter, _visit_class(ClassChecker), 'W0232', is_model_mpttmeta_subclass) suppress_message(linter, _leave_class(MisdesignChecker), 'too-few-public-methods', is_model_mpttmeta_subclass) + # factory_boy's DjangoModelFactory + suppress_message(linter, _visit_class(DocStringChecker), 'missing-docstring', is_model_factory_meta_subclass) + suppress_message(linter, _visit_class(NewStyleConflictChecker), 'old-style-class', is_model_factory_meta_subclass) + suppress_message(linter, _visit_class(ClassChecker), 'W0232', is_model_factory_meta_subclass) + suppress_message(linter, _leave_class(MisdesignChecker), 'too-few-public-methods', is_model_factory_meta_subclass) + # ForeignKey and OneToOneField # Must update this in a thread safe way to support the parallel option on pylint (-j) current_leave_module = VariablesChecker.leave_module diff --git a/pylint_django/plugin.py b/pylint_django/plugin.py index e24ef33c..668c4be4 100644 --- a/pylint_django/plugin.py +++ b/pylint_django/plugin.py @@ -16,13 +16,8 @@ def register(linter): However, we will also use it to amend existing checker config. """ name_checker = get_checker(linter, NameChecker) - name_checker.config.good_names += ('qs',) - - # Default pylint.checkers.base.CONST_NAME_RGX = re.compile('(([A-Z_][A-Z0-9_]*)|(__.*__))$'). - start = name_checker.config.const_rgx.pattern[:-2] - end = name_checker.config.const_rgx.pattern[-2:] - const_rgx = '%s|(urls|urlpatterns|register)%s' % (start, end) - name_checker.config.const_rgx = re.compile(const_rgx) + name_checker.config.good_names += ('qs', 'urlpatterns', 'register', 'app_name', + 'handler500') # we don't care about South migrations linter.config.black_list += ('migrations', 'south_migrations') diff --git a/pylint_django/transforms/fields.py b/pylint_django/transforms/fields.py index ec311155..1e3a5a8f 100644 --- a/pylint_django/transforms/fields.py +++ b/pylint_django/transforms/fields.py @@ -9,10 +9,13 @@ _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') + return cls.qname().startswith('django.db.models.fields') or \ + cls.qname().startswith('django.contrib.postgres.fields') def is_form_field(cls): @@ -45,10 +48,20 @@ def apply_type_shim(cls, context=None): # noqa 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') else: return iter([cls]) diff --git a/pylint_django/transforms/transforms/django_contrib_postgres_fields.py b/pylint_django/transforms/transforms/django_contrib_postgres_fields.py new file mode 100644 index 00000000..64c9a229 --- /dev/null +++ b/pylint_django/transforms/transforms/django_contrib_postgres_fields.py @@ -0,0 +1,47 @@ +from django.contrib.postgres import fields as django_fields +from psycopg2 import extras + + +# -------- +# lists + +class ArrayField(list, django_fields.ArrayField): + pass + + +# -------- +# dicts + +class HStoreField(dict, django_fields.HStoreField): + pass + + +class JSONField(dict, django_fields.JSONField): + pass + + +# -------- +# ranges + +class RangeField(extras.Range, django_fields.RangeField): + pass + + +class IntegerRangeField(extras.NumericRange, django_fields.IntegerRangeField): + pass + + +class BigIntegerRangeField(extras.NumericRange, django_fields.BigIntegerRangeField): + pass + + +class FloatRangeField(extras.NumericRange, django_fields.FloatRangeField): + pass + + +class DateTimeRangeField(extras.DateTimeTZRange, django_fields.DateRangeField): + pass + + +class DateRangeField(extras.DateRange, django_fields.DateRangeField): + pass diff --git a/pylint_django/transforms/transforms/django_core_handlers_wsgi.py b/pylint_django/transforms/transforms/django_core_handlers_wsgi.py index f8c4ef37..b9e6df53 100644 --- a/pylint_django/transforms/transforms/django_core_handlers_wsgi.py +++ b/pylint_django/transforms/transforms/django_core_handlers_wsgi.py @@ -4,3 +4,4 @@ class WSGIRequest(WSGIRequestOriginal): status_code = None content = '' + json = None diff --git a/pylint_django/transforms/transforms/django_db_models_fields.py b/pylint_django/transforms/transforms/django_db_models_fields.py index 0f1b6106..f8cf12e4 100644 --- a/pylint_django/transforms/transforms/django_db_models_fields.py +++ b/pylint_django/transforms/transforms/django_db_models_fields.py @@ -1,6 +1,7 @@ from django.db.models import fields as django_fields import datetime from decimal import Decimal +from uuid import UUID # -------- @@ -110,6 +111,12 @@ def __new__(cls, verbose_name=None, name=None, auto_now=False, pass +class DurationField(datetime.timedelta, django_fields.DurationField): + if PY3: + def __new__(cls, verbose_name=None, name=None, **kwargs): + pass + + # ------- # misc @@ -119,3 +126,7 @@ class GenericIPAddressField(str, django_fields.GenericIPAddressField): class IPAddressField(str, django_fields.IPAddressField): pass + + +class UUIDField(UUID, django_fields.UUIDField): + pass diff --git a/pylint_django/transforms/transforms/django_forms_fields.py b/pylint_django/transforms/transforms/django_forms_fields.py index 18105429..4ace966d 100644 --- a/pylint_django/transforms/transforms/django_forms_fields.py +++ b/pylint_django/transforms/transforms/django_forms_fields.py @@ -101,7 +101,13 @@ def __new__(cls, input_formats=None, *args, **kwargs): pass - # -------- +class DurationField(datetime.timedelta, django_fields.DurationField): + if PY3: + def __new__(cls, *args, **kwargs): + pass + + +# -------- # choice class ChoiceField(object, django_fields.ChoiceField): diff --git a/pylint_django/transforms/transforms/django_utils_translation.py b/pylint_django/transforms/transforms/django_utils_translation.py index c838edef..70d7274c 100644 --- a/pylint_django/transforms/transforms/django_utils_translation.py +++ b/pylint_django/transforms/transforms/django_utils_translation.py @@ -1 +1 @@ -ugettext_lazy = lambda x: None +ugettext_lazy = lambda x: '' diff --git a/scripts/test.sh b/scripts/test.sh index ad93a9a9..32d97261 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,2 +1,2 @@ #!/bin/bash -python test/test_func.py +PYTHONPATH=. python test/test_func.py diff --git a/scripts/travis-build.sh b/scripts/travis-build.sh deleted file mode 100755 index 02251cae..00000000 --- a/scripts/travis-build.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -python scripts/travis_skip.py - -if [ "$?" -eq "0" ] -then - coverage run test/test_func.py -else - echo "Skipping" -fi diff --git a/scripts/travis-install.sh b/scripts/travis-install.sh deleted file mode 100755 index a977c103..00000000 --- a/scripts/travis-install.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -python scripts/travis_skip.py - -if [ "$?" -eq "0" ] -then - pip install coverage coveralls - pip install $DJANGO - pip install git+https://github.com/landscapeio/pylint-plugin-utils.git@develop - pip install --editable . -else - echo "Skipping" -fi diff --git a/scripts/travis_skip.py b/scripts/travis_skip.py deleted file mode 100755 index 8b0932b3..00000000 --- a/scripts/travis_skip.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python -import os -import sys -for skip_ver in os.environ.get('SKIP', '').split(): - skip_ver = tuple(map(int, skip_ver.split('.'))) - if skip_ver == sys.version_info[:len(skip_ver)]: - sys.exit(1) -sys.exit(0) diff --git a/setup.py b/setup.py index 2e77228b..c74e6d1a 100644 --- a/setup.py +++ b/setup.py @@ -25,10 +25,9 @@ 'Intended Audience :: Developers', 'Operating System :: Unix', 'Topic :: Software Development :: Quality Assurance', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/external_drf/func_noerror_serializer.py b/test/input/external_drf_noerror_serializer.py similarity index 83% rename from test/external_drf/func_noerror_serializer.py rename to test/input/external_drf_noerror_serializer.py index 5b045a3d..c4bc7da8 100644 --- a/test/external_drf/func_noerror_serializer.py +++ b/test/input/external_drf_noerror_serializer.py @@ -1,11 +1,10 @@ """ Checks that Pylint does not complain about DRF serializers """ -# pylint: disable=C0111,W5101,R0903 +# pylint: disable=C0111,W5101 from rest_framework import serializers class TestSerializerSubclass(serializers.ModelSerializer): class Meta: pass - diff --git a/test/input/external_drf_noerror_serializer.rc b/test/input/external_drf_noerror_serializer.rc new file mode 100644 index 00000000..a63d6bff --- /dev/null +++ b/test/input/external_drf_noerror_serializer.rc @@ -0,0 +1,2 @@ +[testoptions] +requires = rest_framework diff --git a/test/input/external_psycopg2_noerror_postgres_fields.py b/test/input/external_psycopg2_noerror_postgres_fields.py new file mode 100644 index 00000000..e5cb0dde --- /dev/null +++ b/test/input/external_psycopg2_noerror_postgres_fields.py @@ -0,0 +1,52 @@ +""" +Checks that Pylint does not complain Postgres model fields. +""" +# pylint: disable=C0111,W5101 +from __future__ import print_function + +from django.contrib.postgres import fields +from django.db import models + + +class PostgresFieldsModel(models.Model): + arrayfield = fields.ArrayField(models.CharField()) + hstorefield = fields.HStoreField() + jsonfield = fields.JSONField() + rangefield = fields.RangeField() + integerrangefield = fields.IntegerRangeField() + bigintegerrangefield = fields.BigIntegerRangeField() + floatrangefield = fields.FloatRangeField() + datetimerangefield = fields.DateTimeRangeField() + daterangefield = fields.DateRangeField() + + def arrayfield_tests(self): + sorted_array = self.arrayfield.sort() + print(sorted_array) + + def dictfield_tests(self): + print(self.hstorefield.keys()) + print(self.hstorefield.values()) + print(self.hstorefield.update({'foo': 'bar'})) + + print(self.jsonfield.keys()) + print(self.jsonfield.values()) + print(self.jsonfield.update({'foo': 'bar'})) + + def rangefield_tests(self): + print(self.rangefield.lower) + print(self.rangefield.upper) + + print(self.integerrangefield.lower) + print(self.integerrangefield.upper) + + print(self.bigintegerrangefield.lower) + print(self.bigintegerrangefield.upper) + + print(self.floatrangefield.lower) + print(self.floatrangefield.upper) + + print(self.datetimerangefield.lower) + print(self.datetimerangefield.upper) + + print(self.daterangefield.lower) + print(self.daterangefield.upper) diff --git a/test/input/external_psycopg2_noerror_postgres_fields.rc b/test/input/external_psycopg2_noerror_postgres_fields.rc new file mode 100644 index 00000000..79f0c092 --- /dev/null +++ b/test/input/external_psycopg2_noerror_postgres_fields.rc @@ -0,0 +1,2 @@ +[testoptions] +requires = psycopg2 diff --git a/test/input/func_model_does_not_use_unicode_py33.py b/test/input/func_model_does_not_use_unicode_py33.py index 34258e1c..0b28841e 100644 --- a/test/input/func_model_does_not_use_unicode_py33.py +++ b/test/input/func_model_does_not_use_unicode_py33.py @@ -1,14 +1,14 @@ """ -Ensures that django models without a __unicode__ method are flagged +Ensures that under PY3 django models with a __unicode__ method are flagged """ -# pylint: disable=C0111 +# pylint: disable=missing-docstring from django.db import models -class SomeModel(models.Model): +class SomeModel(models.Model): # [model-has-unicode] something = models.CharField(max_length=255) - # no __unicode__ method + # no __str__ method something.something_else = 1 diff --git a/test/input/func_model_does_not_use_unicode_py33.txt b/test/input/func_model_does_not_use_unicode_py33.txt new file mode 100644 index 00000000..8d3d8bb4 --- /dev/null +++ b/test/input/func_model_does_not_use_unicode_py33.txt @@ -0,0 +1 @@ +model-has-unicode:9:SomeModel:Found __unicode__ method on model (SomeModel). Python3 uses __str__. diff --git a/test/input/func_model_requires_unicode_py_28.py b/test/input/func_model_requires_unicode_py_28.py deleted file mode 100644 index 72cc761f..00000000 --- a/test/input/func_model_requires_unicode_py_28.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Ensures that django models without a __unicode__ method are flagged, -but only models which are not abstract -""" -# pylint: disable=C0111 - -from django.db import models - - -class SomeModel(models.Model): - something = models.CharField(max_length=255) - # no __unicode__ method - - something.something_else = 1 - - def lala(self): - pass - - -class SomeAbstractModel(models.Model): - class Meta: - abstract = True diff --git a/test/input/func_model_unicode_parent_py_28.py b/test/input/func_model_unicode_parent_py_28.py deleted file mode 100644 index 38e937a1..00000000 --- a/test/input/func_model_unicode_parent_py_28.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Ensures that django models whose parent have a -__unicide__ method are not flagged -""" -# pylint: disable=C0111 -from django.db import models - - -class MyModel(models.Model): - def __unicode__(self): - return str(self.id) - - -class MySubclassModel(MyModel): - pass diff --git a/test/input/func_noerror_classviews.py b/test/input/func_noerror_classviews.py index 249e86d5..b37b101d 100644 --- a/test/input/func_noerror_classviews.py +++ b/test/input/func_noerror_classviews.py @@ -2,7 +2,7 @@ Checks that Pylint does not complain about attributes and methods when using Class-based Views """ -# pylint: disable=C0111,W5101 +# pylint: disable=missing-docstring from django.views.generic import TemplateView diff --git a/test/input/func_noerror_foreign_key_attributes.py b/test/input/func_noerror_foreign_key_attributes.py index b0b55a9e..1ae57677 100644 --- a/test/input/func_noerror_foreign_key_attributes.py +++ b/test/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=C0111,W5101 +# pylint: disable=missing-docstring from django.db import models @@ -12,8 +12,8 @@ class SomeModel(models.Model): class OtherModel(models.Model): - something = models.ForeignKey(SomeModel) - elsething = models.OneToOneField(SomeModel) + something = models.ForeignKey(SomeModel, on_delete=models.CASCADE) + elsething = models.OneToOneField(SomeModel, on_delete=models.CASCADE) def something_doer(self): part_a = '%s - %s' % (self.something.name, self.something.timestamp) diff --git a/test/input/func_noerror_foreign_key_ids.py b/test/input/func_noerror_foreign_key_ids.py index 201403b9..90370735 100644 --- a/test/input/func_noerror_foreign_key_ids.py +++ b/test/input/func_noerror_foreign_key_ids.py @@ -1,7 +1,7 @@ """ Checks that Pylint does not complain about foreign key id access """ -# pylint: disable=C0111,W5101,wrong-import-position +# pylint: disable=missing-docstring,wrong-import-position from django.db import models @@ -10,7 +10,7 @@ class SomeModel(models.Model): class SomeOtherModel(models.Model): - some_model = models.ForeignKey(SomeModel) + some_model = models.ForeignKey(SomeModel, on_delete=models.CASCADE) number = models.IntegerField() def do_something(self): diff --git a/test/input/func_noerror_foreign_key_sets.py b/test/input/func_noerror_foreign_key_sets.py index 779adf4b..22ce18bc 100644 --- a/test/input/func_noerror_foreign_key_sets.py +++ b/test/input/func_noerror_foreign_key_sets.py @@ -1,7 +1,7 @@ """ Checks that Pylint does not complain about foreign key sets on models """ -# pylint: disable=C0111,W5101 +# pylint: disable=missing-docstring from django.db import models @@ -18,11 +18,12 @@ def get_first(self): class OtherModel(models.Model): count = models.IntegerField() - something = models.ForeignKey(SomeModel) + something = models.ForeignKey(SomeModel, on_delete=models.CASCADE) class ThirdModel(models.Model): - whatever = models.ForeignKey(SomeModel, related_name='whatevs') + whatever = models.ForeignKey(SomeModel, related_name='whatevs', + on_delete=models.CASCADE) def count_whatevers(): diff --git a/test/input/func_noerror_foreignkeys.py b/test/input/func_noerror_foreignkeys.py index fa82df5a..ec5401f4 100644 --- a/test/input/func_noerror_foreignkeys.py +++ b/test/input/func_noerror_foreignkeys.py @@ -2,7 +2,7 @@ Checks that Pylint does not complain about various methods on Django model fields. """ -# pylint: disable=C0111,W5101,wrong-import-position +# pylint: disable=missing-docstring,wrong-import-position from django.db import models from django.db.models import ForeignKey, OneToOneField @@ -17,8 +17,8 @@ class ISBN(models.Model): class Book(models.Model): book_name = models.CharField(max_length=100) - author = models.ForeignKey(Author) - isbn = models.OneToOneField(ISBN) + author = models.ForeignKey(Author, on_delete=models.CASCADE) + isbn = models.OneToOneField(ISBN, on_delete=models.CASCADE) def get_isbn(self): return self.isbn.value @@ -32,7 +32,7 @@ class Fruit(models.Model): class Seed(models.Model): - fruit = ForeignKey(Fruit) + fruit = ForeignKey(Fruit, on_delete=models.CASCADE) def get_fruit_name(self): return self.fruit.fruit_name @@ -43,7 +43,7 @@ class User(models.Model): class UserProfile(models.Model): - user = OneToOneField(User) + user = OneToOneField(User, on_delete=models.CASCADE) def get_username(self): return self.user.username diff --git a/test/input/func_noerror_form_fields.py b/test/input/func_noerror_form_fields.py index 683fc78e..7ddb4ff5 100644 --- a/test/input/func_noerror_form_fields.py +++ b/test/input/func_noerror_form_fields.py @@ -2,7 +2,7 @@ Checks that Pylint does not complain about various methods on Django form forms. """ -# pylint: disable=C0111,R0904 +# pylint: disable=missing-docstring,R0904 from __future__ import print_function from datetime import datetime, date from django import forms @@ -15,15 +15,13 @@ class ManyFieldsForm(forms.Form): datetimefield = forms.DateTimeField(auto_now_add=True) datefield = forms.DateField(auto_now_add=True) 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') floatfield = forms.FloatField() genericipaddressfield = forms.GenericIPAddressField() imagefield = forms.ImageField(name='test_image', upload_to='test') - # note: IPAdressField has been deprecated since django 1.7 so might not - # be available if using django 1.8+ - ipaddressfield = getattr(forms, 'IPAddressField', 'GenericIPAddressField')() intfield = forms.IntegerField(null=True) nullbooleanfield = forms.NullBooleanField() slugfield = forms.SlugField() @@ -60,6 +58,11 @@ def datefield_tests(self): def decimalfield_tests(self): print(self.decimalfield.adjusted()) + def durationfield_tests(self): + now = datetime.now() + print(now - self.durationfield) + print(self.durationfield.total_seconds()) + def filefield_tests(self): print(self.filefield) print(self.imagefield) diff --git a/test/input/func_noerror_forms_py33.py b/test/input/func_noerror_forms_py33.py index 05206680..38912282 100644 --- a/test/input/func_noerror_forms_py33.py +++ b/test/input/func_noerror_forms_py33.py @@ -1,7 +1,7 @@ """ Checks that Pylint does not complain about django Forms """ -# pylint: disable=C0111,W5101,R0903,wrong-import-position +# pylint: disable=missing-docstring,wrong-import-position from django import forms diff --git a/test/input/func_noerror_forms_py_28.py b/test/input/func_noerror_forms_py_28.py deleted file mode 100644 index b972f523..00000000 --- a/test/input/func_noerror_forms_py_28.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Checks that Pylint does not complain about django Forms -""" -# pylint: disable=C0111,W5101,R0903 - -from django import forms - - -class TestForm(forms.Form): - class Meta: - pass - - some_field = forms.CharField() - - def clean(self): - print self.cleaned_data - print self.fields - print self.error_class - - -class TestModelForm(forms.ModelForm): - class Meta: - pass - - -class TestFormSubclass(forms.Form): - class Meta: - pass - - -class TestModelFormSubclass(forms.ModelForm): - class Meta: - pass diff --git a/test/input/func_noerror_formview_ancestors.py b/test/input/func_noerror_formview_ancestors.py index 18f9d3be..35f72c9c 100644 --- a/test/input/func_noerror_formview_ancestors.py +++ b/test/input/func_noerror_formview_ancestors.py @@ -2,7 +2,7 @@ Checks that Pylint does not complain about django FormViews having too many ancestors """ -# pylint: disable=C0111,W5101 +# pylint: disable=missing-docstring from django.views.generic import FormView diff --git a/test/input/func_noerror_ignore_meta_subclass.py b/test/input/func_noerror_ignore_meta_subclass.py index 7e612619..af1c9b08 100644 --- a/test/input/func_noerror_ignore_meta_subclass.py +++ b/test/input/func_noerror_ignore_meta_subclass.py @@ -2,7 +2,7 @@ This test ensures that a 'Meta' class defined on a Django model does not raise warnings such as 'old-style-class' and 'too-few-public-methods' """ -# pylint: disable=C0111,W5101 +# pylint: disable=missing-docstring from django.db import models diff --git a/test/input/func_noerror_issue_46.py b/test/input/func_noerror_issue_46.py index 1adb5974..0e20d95e 100644 --- a/test/input/func_noerror_issue_46.py +++ b/test/input/func_noerror_issue_46.py @@ -1,7 +1,7 @@ """ Checks that Pylint does not complain about raising DoesNotExist """ -# pylint: disable=C0111,W5101,W5103 +# pylint: disable=missing-docstring from django.db import models diff --git a/test/input/func_noerror_manytomanyfield.py b/test/input/func_noerror_manytomanyfield.py index e98c2cc9..8ff877ca 100644 --- a/test/input/func_noerror_manytomanyfield.py +++ b/test/input/func_noerror_manytomanyfield.py @@ -2,7 +2,7 @@ Checks that Pylint does not complain about various methods on many-to-many relationships """ -# pylint: disable=C0111,W5101 +# pylint: disable=missing-docstring from django.db import models diff --git a/test/input/func_noerror_model_fields.py b/test/input/func_noerror_model_fields.py index 7009814e..08145fe5 100644 --- a/test/input/func_noerror_model_fields.py +++ b/test/input/func_noerror_model_fields.py @@ -2,7 +2,7 @@ Checks that Pylint does not complain about various methods on Django model fields. """ -# pylint: disable=C0111,W5101 +# pylint: disable=missing-docstring from __future__ import print_function from datetime import datetime, date from decimal import Decimal @@ -18,6 +18,7 @@ class LotsOfFieldsModel(models.Model): datetimefield = models.DateTimeField(auto_now_add=True) datefield = models.DateField(auto_now_add=True) decimalfield = models.DecimalField(max_digits=5, decimal_places=2) + durationfield = models.DurationField() emailfield = models.EmailField() filefield = models.FileField(name='test_file', upload_to='test') filepathfield = models.FilePathField() @@ -69,6 +70,11 @@ def datefield_tests(self): def decimalfield_tests(self): print(self.decimalfield.compare(Decimal('1.4'))) + def durationfield_tests(self): + now = datetime.now() + print(now - self.durationfield) + print(self.durationfield.total_seconds()) + def filefield_tests(self): print(self.filefield.file) print(self.imagefield.file) diff --git a/test/input/func_noerror_model_methods.py b/test/input/func_noerror_model_methods.py index 43a27744..0cad4ec2 100644 --- a/test/input/func_noerror_model_methods.py +++ b/test/input/func_noerror_model_methods.py @@ -1,7 +1,7 @@ """ Checks that Pylint does not complain about using Model and Manager methods """ -# pylint: disable=C0111,W5101,W5103 +# pylint: disable=missing-docstring from django.db import models diff --git a/test/input/func_noerror_model_unicode_callable.py b/test/input/func_noerror_model_unicode_callable.py index 0ef1406c..a963ee0e 100644 --- a/test/input/func_noerror_model_unicode_callable.py +++ b/test/input/func_noerror_model_unicode_callable.py @@ -1,7 +1,7 @@ """ Ensures that django models without a __unicode__ method are flagged """ -# pylint: disable=C0111,wrong-import-position +# pylint: disable=missing-docstring,wrong-import-position from django.db import models diff --git a/test/input/func_noerror_model_unicode_lambda.py b/test/input/func_noerror_model_unicode_lambda.py index 22ad0e7a..25c2ff11 100644 --- a/test/input/func_noerror_model_unicode_lambda.py +++ b/test/input/func_noerror_model_unicode_lambda.py @@ -1,7 +1,7 @@ """ Ensures that django models without a __unicode__ method are flagged """ -# pylint: disable=C0111,wrong-import-position +# pylint: disable=missing-docstring,wrong-import-position from django.db import models diff --git a/test/input/func_noerror_models_py33.py b/test/input/func_noerror_models_py33.py index 871ff471..7ad22bbe 100644 --- a/test/input/func_noerror_models_py33.py +++ b/test/input/func_noerror_models_py33.py @@ -2,7 +2,7 @@ Checks that Pylint does not complain about a fairly standard Django Model """ -# pylint: disable=C0111,W5101 +# pylint: disable=missing-docstring from django.db import models diff --git a/test/input/func_noerror_models_py_28.py b/test/input/func_noerror_models_py_28.py deleted file mode 100644 index 175c3dbf..00000000 --- a/test/input/func_noerror_models_py_28.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -Checks that Pylint does not complain about a fairly standard -Django Model -""" -# pylint: disable=C0111,W5101,W5103 -from django.db import models - - -class SomeModel(models.Model): - class Meta: - pass - - some_field = models.CharField(max_length=20) - - 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' - except self.MultipleObjectsReturned: - print 'lala' - - print self.get_some_field_display() - - def __unicode__(self): - print 'some model %s' % self.id - - -class SubclassModel(SomeModel): - class Meta: - pass diff --git a/test/input/versions/func_noerror_proteted_meta_access.py b/test/input/func_noerror_protected_meta_access.py similarity index 94% rename from test/input/versions/func_noerror_proteted_meta_access.py rename to test/input/func_noerror_protected_meta_access.py index 762dc8f7..c8ec668d 100644 --- a/test/input/versions/func_noerror_proteted_meta_access.py +++ b/test/input/func_noerror_protected_meta_access.py @@ -5,7 +5,7 @@ (see https://github.com/landscapeio/pylint-django/issues/66, and https://docs.djangoproject.com/en/1.9/ref/models/meta/) """ -# pylint: disable=C0111,W5101,W5103 +# pylint: disable=missing-docstring from __future__ import print_function from django.db import models diff --git a/test/input/func_noerror_ugettext_lazy_format.py b/test/input/func_noerror_ugettext_lazy_format.py new file mode 100644 index 00000000..1e8ad248 --- /dev/null +++ b/test/input/func_noerror_ugettext_lazy_format.py @@ -0,0 +1,7 @@ +""" +Checks that Pylint does not complain about django lazy proxy +when using ugettext_lazy +""" +from django.utils.translation import ugettext_lazy + +ugettext_lazy('{something}').format(something='lala') diff --git a/test/input/func_noerror_unicode_py2_compatible.py b/test/input/func_noerror_unicode_py2_compatible.py index 454039a5..840a812a 100644 --- a/test/input/func_noerror_unicode_py2_compatible.py +++ b/test/input/func_noerror_unicode_py2_compatible.py @@ -4,7 +4,7 @@ See https://github.com/landscapeio/pylint-django/issues/10 """ -# pylint: disable=C0111 +# pylint: disable=missing-docstring from django.utils.encoding import python_2_unicode_compatible from django.db import models diff --git a/test/input/func_noerror_urls.py b/test/input/func_noerror_urls.py index b41ad0ad..3d04dd15 100644 --- a/test/input/func_noerror_urls.py +++ b/test/input/func_noerror_urls.py @@ -2,7 +2,7 @@ Checks that Pylint does not complain about attributes and methods when creating a typical urls.py """ -# pylint: disable=C0111 +# pylint: disable=missing-docstring # pylint: disable=C0103 # ^ eventually we should be able to override or update the # CONST_NAME_RGX value diff --git a/test/input/func_noerror_uuid_field.py b/test/input/func_noerror_uuid_field.py new file mode 100644 index 00000000..1fa5b12c --- /dev/null +++ b/test/input/func_noerror_uuid_field.py @@ -0,0 +1,19 @@ +""" +Checks that Pylint does not complain about UUID fields. +""" +# pylint: disable=C0111,W5101 +from __future__ import print_function +from django.db import models + + +class LotsOfFieldsModel(models.Model): + uuidfield = models.UUIDField() + + def uuidfield_tests(self): + print(self.uuidfield.bytes) + print(self.uuidfield.bytes_le) + print(self.uuidfield.fields[2]) + print(self.uuidfield.hex) + # print(self.uuidfield.int) # Don't know how to properly check this one + print(self.uuidfield.variant) + print(self.uuidfield.version) diff --git a/test/messages/func_model_does_not_use_unicode_py33.txt b/test/messages/func_model_does_not_use_unicode_py33.txt deleted file mode 100644 index 711cbcc6..00000000 --- a/test/messages/func_model_does_not_use_unicode_py33.txt +++ /dev/null @@ -1 +0,0 @@ -W: 9:SomeModel: Found __unicode__ method on model (SomeModel). Python3 uses __str__. \ No newline at end of file diff --git a/test/messages/func_model_requires_unicode_py_28.txt b/test/messages/func_model_requires_unicode_py_28.txt deleted file mode 100644 index 91be4bd9..00000000 --- a/test/messages/func_model_requires_unicode_py_28.txt +++ /dev/null @@ -1 +0,0 @@ -W: 10:SomeModel: No __unicode__ method on model (SomeModel) diff --git a/test/messages/func_model_unicode_parent_py_28.txt b/test/messages/func_model_unicode_parent_py_28.txt deleted file mode 100644 index 918c0bea..00000000 --- a/test/messages/func_model_unicode_parent_py_28.txt +++ /dev/null @@ -1 +0,0 @@ -W: 14:MySubclassModel: Model does not explicitly define __unicode__ (MySubclassModel) \ No newline at end of file diff --git a/test/test_func.py b/test/test_func.py index 473ba949..b6fd2745 100644 --- a/test/test_func.py +++ b/test/test_func.py @@ -1,73 +1,47 @@ import os import sys -import unittest -from django.conf import settings -from pylint.testutils import make_tests, LintTestUsingFile, cb_test_gen, linter -from pylint_django.compat import django_version - - -settings.configure() - - -HERE = os.path.dirname(os.path.abspath(__file__)) +import pytest +import pylint +# because there's no __init__ file in pylint/test/ +sys.path.append(os.path.join(os.path.dirname(pylint.__file__), 'test')) +import test_functional -linter.load_plugin_modules(['pylint_django']) -# Disable some things on Python2.6, since we use a different pylint version here -# (1.3 on Python2.6, 1.4+ on later versions) -if sys.version_info < (2, 7): - linter.global_set_option('required-attributes', ()) - linter.global_set_option('disable', ('E0012',)) - - -SKIP_TESTS_FOR_DJANGO_VERSION = { - # if the second value is False, skip the test, otherwise run it - ('func_noerror_protected_meta_access', django_version >= (1, 8)), -} - +from pylint_django.compat import django_version -def module_exists(module_name): - try: - __import__(module_name) - except ImportError: - return False - else: - return True +class PylintDjangoLintModuleTest(test_functional.LintModuleTest): + """ + 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']) -def tests(input_dir, messages_dir): - callbacks = [cb_test_gen(LintTestUsingFile)] - input_dir = os.path.join(HERE, input_dir) - messages_dir = os.path.join(HERE, messages_dir) +def get_tests(): + HERE = os.path.dirname(os.path.abspath(__file__)) + input_dir = os.path.join(HERE, 'input') - # first tests which pass for all Django versions - tests = make_tests(input_dir, messages_dir, None, callbacks) + suite = [] + for fname in os.listdir(input_dir): + if fname != '__init__.py' and fname.endswith('.py'): + suite.append(test_functional.FunctionalTestFile(input_dir, fname)) + return suite - # now skip some tests test for specific versions - for example, - # _meta access should not work for django<1.8 but should run and - # pass for django 1.4 - skip the tests which will be checking - # a piece of functionality in pylint-django that should only - # in higher versions. - specific_tests = [] - for test_name, version_range in SKIP_TESTS_FOR_DJANGO_VERSION: - if not version_range: - specific_tests.append(test_name) - filter_rgx = '(%s)' % '|'.join(specific_tests) - tests += make_tests(os.path.join(input_dir, 'versions'), messages_dir, filter_rgx, callbacks) - return tests +TESTS = get_tests() +TESTS_NAMES = [t.base for t in TESTS] -def suite(): - test_list = tests('input', 'messages') - - if module_exists('rest_framework'): - test_list += tests('external_drf', '') +@pytest.mark.parametrize("test_file", TESTS, ids=TESTS_NAMES) +def test_everything(test_file): + # copied from pylint.tests.test_functional.test_functional + LintTest = PylintDjangoLintModuleTest(test_file) + LintTest.setUp() + LintTest._runTest() - return unittest.TestSuite([unittest.makeSuite(test, suiteClass=unittest.TestSuite) - for test in test_list]) if __name__ == '__main__': - unittest.main(defaultTest='suite') + sys.exit(pytest.main(sys.argv)) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index bb88cbbf..00000000 --- a/tox.ini +++ /dev/null @@ -1,7 +0,0 @@ -[tox] -envlist = py27,py33,py34,py35 - -[testenv] -deps=Django -commands=python test/test_func.py -