From bb1bd18c90391d5cd0628837df1019eca746ae56 Mon Sep 17 00:00:00 2001 From: "Mr. Senko" Date: Wed, 18 Jul 2018 00:39:55 +0300 Subject: [PATCH 1/6] Module.__path__ is now a list for checking if a module is named wsgi.py or includes 'migrations' in its name we take the first element of the list --- pylint_django/augmentations/__init__.py | 13 ++----------- pylint_django/checkers/db_performance.py | 4 ++-- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/pylint_django/augmentations/__init__.py b/pylint_django/augmentations/__init__.py index d84ea858..d93d378f 100644 --- a/pylint_django/augmentations/__init__.py +++ b/pylint_django/augmentations/__init__.py @@ -714,17 +714,8 @@ def wrap_func(*args, **kwargs): def is_wsgi_application(node): frame = node.frame() - - if node.name == 'application' and isinstance(frame, Module): - path_ends_with_wsgi = False - for path in frame.path: - if path.endswith('wsgi.py'): - path_ends_with_wsgi = True - break - - return frame.name == 'wsgi' or path_ends_with_wsgi or frame.file.endswith('wsgi.py') - - return False + return node.name == 'application' and isinstance(frame, Module) and \ + (frame.name == 'wsgi' or frame.path[0].endswith('wsgi.py') or frame.file.endswith('wsgi.py')) def apply_augmentations(linter): diff --git a/pylint_django/checkers/db_performance.py b/pylint_django/checkers/db_performance.py index cf052ea9..13f19efd 100644 --- a/pylint_django/checkers/db_performance.py +++ b/pylint_django/checkers/db_performance.py @@ -40,7 +40,7 @@ def _is_migrations_module(node): if not isinstance(node, astroid.Module): return False - return 'migrations' in node.path and not node.path.endswith('__init__.py') + return 'migrations' in node.path[0] and not node.path[0].endswith('__init__.py') class NewDbFieldWithDefaultChecker(checkers.BaseChecker): @@ -102,7 +102,7 @@ def _path(node): last_name_space = '' latest_migrations = [] for module in self._migration_modules: - name_space = module.path.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) From 0542236fff51b8cb19329af62d1e71150278b2bd Mon Sep 17 00:00:00 2001 From: "Mr. Senko" Date: Mon, 23 Jul 2018 14:07:56 +0300 Subject: [PATCH 2/6] New implementation of module transforms According to this documentation http://pylint.pycqa.org/projects/astroid/en/latest/extending.html?highlight=MANAGER#module-extender-transforms the module transform function doesn't accept any parameters and returns a new Module node. --- pylint_django/transforms/__init__.py | 70 +++++++++++++--------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/pylint_django/transforms/__init__.py b/pylint_django/transforms/__init__.py index 6c7f1479..f2556151 100644 --- a/pylint_django/transforms/__init__.py +++ b/pylint_django/transforms/__init__.py @@ -2,53 +2,49 @@ import os import re -from astroid import MANAGER -from astroid.builder import AstroidBuilder -from astroid import nodes +import astroid from pylint_django.transforms import foreignkey, fields -foreignkey.add_transform(MANAGER) -fields.add_transforms(MANAGER) +foreignkey.add_transform(astroid.MANAGER) +fields.add_transforms(astroid.MANAGER) -def _add_transform(package_name, *class_names): - """Transform package's classes.""" - 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)) +def _add_transform(package_name): + def fake_module_builder(): + """ + Build a fake module to use within transformations. + @package_name is a parameter from the outher 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)) - with open(fake_module_path) as modulefile: - fake_module = modulefile.read() + with open(fake_module_path) as modulefile: + fake_module = modulefile.read() - fake = AstroidBuilder(MANAGER).string_build(fake_module) + return astroid.builder.AstroidBuilder(astroid.MANAGER).string_build(fake_module) - def set_fake_locals(module): - """Set fake locals for package.""" - if module.name != package_name: - return - for class_name in class_names: - # This changed from locals to _locals between astroid 1.3 and 1.4 - if hasattr(module, '_locals'): - module._locals[class_name].extend(fake._locals[class_name]) # pylint: disable=protected-access - else: - module.locals[class_name].extend(fake.locals[class_name]) + astroid.register_module_extender(astroid.MANAGER, package_name, fake_module_builder) - MANAGER.register_transform(nodes.Module, set_fake_locals) +# TODO: no sure what to do with commented out code! It looks like +# we don't need these transforms anymore -_add_transform('django.core.handlers.wsgi', 'WSGIRequest') -_add_transform('django.views.generic.base', 'View') -_add_transform('django.forms', 'Form') -_add_transform('django.forms', 'ModelForm') -_add_transform('django.db.models', - 'Model', - 'Manager') -_add_transform('django.utils.translation', 'ugettext_lazy') -_add_transform('mongoengine', 'Document') -_add_transform('model_utils.managers', - 'InheritanceManager', - 'QueryManager', - 'SoftDeletableManager') +# _add_transform('django.core.handlers.wsgi', 'WSGIRequest') +# _add_transform('django.views.generic.base', 'View') +# _add_transform('django.forms', 'Form') +# _add_transform('django.forms', 'ModelForm') +# _add_transform('django.db.models', +# 'Model', +# 'Manager') +_add_transform('django.utils.translation') +# _add_transform('mongoengine', 'Document') +# _add_transform('model_utils.managers', +# 'InheritanceManager', +# 'QueryManager', +# 'SoftDeletableManager') # register transform for FileField/ImageField, see #60 -_add_transform('django.db.models.fields.files', 'FileField') +_add_transform('django.db.models.fields.files') From 519069a4e07a1a84f9c344d5d14e35c283883eff Mon Sep 17 00:00:00 2001 From: "Mr. Senko" Date: Wed, 25 Jul 2018 00:01:28 +0300 Subject: [PATCH 3/6] Remove old and unused transformations after the previous commit these transformation don't seem to be needed. Maybe the latest pylint/astroid is better at figuring out the internals of these Django classes. Additionally things like django_contrib_postgres_fields.py don't seem to be hooked up anywhere so remove them as well. --- pylint_django/transforms/__init__.py | 15 -- .../django_contrib_postgres_fields.py | 47 ------- .../transforms/django_core_handlers_wsgi.py | 7 - .../transforms/transforms/django_db_models.py | 62 -------- .../transforms/django_db_models_fields.py | 132 ------------------ .../transforms/transforms/django_forms.py | 21 --- .../transforms/django_forms_fields.py | 126 ----------------- .../transforms/django_views_generic_base.py | 8 -- .../transforms/model_utils_managers.py | 13 -- .../transforms/transforms/mongoengine.py | 16 --- 10 files changed, 447 deletions(-) delete mode 100644 pylint_django/transforms/transforms/django_contrib_postgres_fields.py delete mode 100644 pylint_django/transforms/transforms/django_core_handlers_wsgi.py delete mode 100644 pylint_django/transforms/transforms/django_db_models.py delete mode 100644 pylint_django/transforms/transforms/django_db_models_fields.py delete mode 100644 pylint_django/transforms/transforms/django_forms.py delete mode 100644 pylint_django/transforms/transforms/django_forms_fields.py delete mode 100644 pylint_django/transforms/transforms/django_views_generic_base.py delete mode 100644 pylint_django/transforms/transforms/model_utils_managers.py delete mode 100644 pylint_django/transforms/transforms/mongoengine.py diff --git a/pylint_django/transforms/__init__.py b/pylint_django/transforms/__init__.py index f2556151..cf602b0f 100644 --- a/pylint_django/transforms/__init__.py +++ b/pylint_django/transforms/__init__.py @@ -30,21 +30,6 @@ def fake_module_builder(): astroid.register_module_extender(astroid.MANAGER, package_name, fake_module_builder) -# TODO: no sure what to do with commented out code! It looks like -# we don't need these transforms anymore - -# _add_transform('django.core.handlers.wsgi', 'WSGIRequest') -# _add_transform('django.views.generic.base', 'View') -# _add_transform('django.forms', 'Form') -# _add_transform('django.forms', 'ModelForm') -# _add_transform('django.db.models', -# 'Model', -# 'Manager') _add_transform('django.utils.translation') -# _add_transform('mongoengine', 'Document') -# _add_transform('model_utils.managers', -# 'InheritanceManager', -# 'QueryManager', -# 'SoftDeletableManager') # register transform for FileField/ImageField, see #60 _add_transform('django.db.models.fields.files') diff --git a/pylint_django/transforms/transforms/django_contrib_postgres_fields.py b/pylint_django/transforms/transforms/django_contrib_postgres_fields.py deleted file mode 100644 index 64c9a229..00000000 --- a/pylint_django/transforms/transforms/django_contrib_postgres_fields.py +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index b9e6df53..00000000 --- a/pylint_django/transforms/transforms/django_core_handlers_wsgi.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.core.handlers.wsgi import WSGIRequest as WSGIRequestOriginal - - -class WSGIRequest(WSGIRequestOriginal): - status_code = None - content = '' - json = None diff --git a/pylint_django/transforms/transforms/django_db_models.py b/pylint_django/transforms/transforms/django_db_models.py deleted file mode 100644 index bf99630e..00000000 --- a/pylint_django/transforms/transforms/django_db_models.py +++ /dev/null @@ -1,62 +0,0 @@ -from django.core.exceptions import MultipleObjectsReturned \ - as MultipleObjectsReturnedException - - -def __noop(self, *args, **kwargs): - """Just a dumb no-op function to make code a bit more DRY""" - return None - - -def __noop_list(self, *args, **kwargs): - """Just a dumb no-op function to make code a bit more DRY""" - return [] - - -class Model(object): - _meta = None - objects = None - - id = None - pk = None - - MultipleObjectsReturned = MultipleObjectsReturnedException - - save = __noop - delete = __noop - - -class Manager(object): - """ - Eliminate E1002 for Manager object - """ - get_queryset = __noop - none = __noop - all = __noop_list - count = __noop - dates = __noop - distinct = __noop - extra = __noop - get = __noop - get_or_create = __noop - create = __noop - bulk_create = __noop - filter = __noop_list - aggregate = __noop - annotate = __noop - complex_filter = __noop - exclude = __noop - in_bulk = __noop - iterator = __noop - latest = __noop - order_by = __noop - select_for_update = __noop - select_related = __noop - prefetch_related = __noop - values = __noop - values_list = __noop - update = __noop - reverse = __noop - defer = __noop - only = __noop - using = __noop - exists = __noop diff --git a/pylint_django/transforms/transforms/django_db_models_fields.py b/pylint_django/transforms/transforms/django_db_models_fields.py deleted file mode 100644 index f8cf12e4..00000000 --- a/pylint_django/transforms/transforms/django_db_models_fields.py +++ /dev/null @@ -1,132 +0,0 @@ -from django.db.models import fields as django_fields -import datetime -from decimal import Decimal -from uuid import UUID - - -# -------- -# booleans -from utils import PY3 - - -class BooleanField(bool, django_fields.BooleanField): - pass - - -class NullBooleanField(bool, django_fields.NullBooleanField): - pass - - -# ------ -# strings - -class CharField(str, django_fields.CharField): - pass - - -class SlugField(CharField, django_fields.SlugField): - pass - - -class URLField(CharField, django_fields.URLField): - pass - - -class TextField(str, django_fields.TextField): - pass - - -class EmailField(CharField, django_fields.EmailField): - pass - - -class CommaSeparatedIntegerField(CharField, django_fields.CommaSeparatedIntegerField): - pass - - -class FilePathField(CharField, django_fields.FilePathField): - pass - - -# ------- -# numbers - -class IntegerField(int, django_fields.IntegerField): - pass - - -class BigIntegerField(IntegerField, django_fields.BigIntegerField): - pass - - -class SmallIntegerField(IntegerField, django_fields.SmallIntegerField): - pass - - -class PositiveIntegerField(IntegerField, django_fields.PositiveIntegerField): - pass - - -class PositiveSmallIntegerField(IntegerField, django_fields.PositiveSmallIntegerField): - pass - - -class FloatField(float, django_fields.FloatField): - pass - - -class DecimalField(Decimal, django_fields.DecimalField): - # DecimalField is immutable and does not use __init__, but the Django DecimalField does. To - # cheat pylint a little bit, we copy the definition of the DecimalField constructor parameters - # into the __new__ method of Decimal so that Pylint believes we are constructing a Decimal with - # the signature of DecimalField - def __new__(cls, verbose_name=None, name=None, max_digits=None, decimal_places=None, **kwargs): - pass - - -# -------- -# date/time - -# In Python3, the date and datetime objects are immutable, so we need to do -# the same __new__ / __init__ fiddle as for Decimal - -class DateField(datetime.date, django_fields.DateField): - if PY3: - def __new__(cls, verbose_name=None, name=None, auto_now=False, - auto_now_add=False, **kwargs): - pass - - -class DateTimeField(datetime.datetime, django_fields.DateTimeField): - if PY3: - def __new__(cls, verbose_name=None, name=None, auto_now=False, - auto_now_add=False, **kwargs): - pass - - -class TimeField(datetime.time, django_fields.TimeField): - if PY3: - def __new__(cls, verbose_name=None, name=None, auto_now=False, - auto_now_add=False, **kwargs): - pass - - -class DurationField(datetime.timedelta, django_fields.DurationField): - if PY3: - def __new__(cls, verbose_name=None, name=None, **kwargs): - pass - - -# ------- -# misc - -class GenericIPAddressField(str, django_fields.GenericIPAddressField): - pass - - -class IPAddressField(str, django_fields.IPAddressField): - pass - - -class UUIDField(UUID, django_fields.UUIDField): - pass diff --git a/pylint_django/transforms/transforms/django_forms.py b/pylint_django/transforms/transforms/django_forms.py deleted file mode 100644 index 4f9029d6..00000000 --- a/pylint_django/transforms/transforms/django_forms.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.forms import BaseForm, BaseModelForm - - -class Form(BaseForm): - cleaned_data = None - fields = None - instance = None - data = None - _errors = None - base_fields = None - - -class ModelForm(BaseModelForm): - def save_m2m(self): - return None - cleaned_data = None - fields = None - instance = None - data = None - _errors = None - base_fields = None diff --git a/pylint_django/transforms/transforms/django_forms_fields.py b/pylint_django/transforms/transforms/django_forms_fields.py deleted file mode 100644 index 4ace966d..00000000 --- a/pylint_django/transforms/transforms/django_forms_fields.py +++ /dev/null @@ -1,126 +0,0 @@ -import datetime -from decimal import Decimal -from django.forms import fields as django_fields - - -# -------- -# booleans -from utils import PY3 - - -class BooleanField(bool, django_fields.BooleanField): - pass - - -class NullBooleanField(bool, django_fields.NullBooleanField): - pass - - -# ------- -# strings - -class CharField(str, django_fields.CharField): - pass - - -class EmailField(CharField, django_fields.EmailField): - pass - - -class GenericIPAddressField(CharField, django_fields.GenericIPAddressField): - pass - - -class IPAddressField(CharField, django_fields.IPAddressField): - pass - - -class SlugField(CharField, django_fields.SlugField): - pass - - -class URLField(CharField, django_fields.URLField): - pass - - -class RegexField(CharField, django_fields.RegexField): - pass - - -class FilePathField(CharField, django_fields.FilePathField): - pass - - -# ------- -# numbers - -class IntegerField(int, django_fields.IntegerField): - pass - - -class DecimalField(Decimal, django_fields.DecimalField): - # DecimalField is immutable and does not use __init__, but the Django DecimalField does. To - # cheat pylint a little bit, we copy the definition of the DecimalField constructor parameters - # into the __new__ method of Decimal so that Pylint believes we are constructing a Decimal with - # the signature of DecimalField - def __new__(cls, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs): - pass - - -class FloatField(float, django_fields.FloatField): - pass - - -# ------- -# date/time - -# In Python3, the date and datetime objects are immutable, so we need to do -# the same __new__ / __init__ fiddle as for Decimal - -class DateField(datetime.date, django_fields.DateField): - if PY3: - def __new__(cls, input_formats=None, *args, **kwargs): - pass - - -class DateTimeField(datetime.datetime, django_fields.DateTimeField): - if PY3: - def __new__(cls, input_formats=None, *args, **kwargs): - pass - - -class SplitDateTimeField(datetime.datetime, django_fields.SplitDateTimeField): - if PY3: - def __new__(cls, input_date_formats=None, input_time_formats=None, *args, **kwargs): - pass - - -class TimeField(datetime.time, django_fields.TimeField): - if PY3: - 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): - pass - - -class MultipleChoiceField(ChoiceField, django_fields.MultipleChoiceField): - pass - - -class TypedChoiceField(ChoiceField, django_fields.TypedChoiceField): - pass - - -class TypedMultipleChoiceField(ChoiceField, django_fields.TypedMultipleChoiceField): - pass diff --git a/pylint_django/transforms/transforms/django_views_generic_base.py b/pylint_django/transforms/transforms/django_views_generic_base.py deleted file mode 100644 index 2aac139e..00000000 --- a/pylint_django/transforms/transforms/django_views_generic_base.py +++ /dev/null @@ -1,8 +0,0 @@ -class View(object): - request = None - args = () - kwargs = {} - - # as_view is marked as class-only - def as_view(*args, **kwargs): - pass diff --git a/pylint_django/transforms/transforms/model_utils_managers.py b/pylint_django/transforms/transforms/model_utils_managers.py deleted file mode 100644 index 5a060f61..00000000 --- a/pylint_django/transforms/transforms/model_utils_managers.py +++ /dev/null @@ -1,13 +0,0 @@ -from django_db_models import Manager - - -class InheritanceManager(Manager): - pass - - -class QueryManager(Manager): - pass - - -class SoftDeletableManager(Manager): - pass diff --git a/pylint_django/transforms/transforms/mongoengine.py b/pylint_django/transforms/transforms/mongoengine.py deleted file mode 100644 index 548bea97..00000000 --- a/pylint_django/transforms/transforms/mongoengine.py +++ /dev/null @@ -1,16 +0,0 @@ -from mongoengine.errors import DoesNotExist, MultipleObjectsReturned -from mongoengine.queryset.manager import QuerySetManager - - -class Document(object): - _meta = None - objects = QuerySetManager() - - id = None - pk = None - - MultipleObjectsReturned = MultipleObjectsReturned - DoesNotExist = DoesNotExist - - def save(self): - return None From f3d2890e00173f16aff8c6b42b936a7dd4642698 Mon Sep 17 00:00:00 2001 From: "Mr. Senko" Date: Mon, 23 Jul 2018 15:28:39 +0300 Subject: [PATCH 4/6] Handle attributes of type DjangoModelFactory previous augmentation was not handling factory objects very well. This is an ammendment to that. I'm not exactly certain why we haven't seen other failures before. --- pylint_django/augmentations/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pylint_django/augmentations/__init__.py b/pylint_django/augmentations/__init__.py index d93d378f..c07cd1a2 100644 --- a/pylint_django/augmentations/__init__.py +++ b/pylint_django/augmentations/__init__.py @@ -427,18 +427,21 @@ def is_model_factory_meta_subclass(node): def is_model_factory(node): - """Checks that node is derivative of SubFactory class.""" + """Checks that node is derivative of DjangoModelFactory or SubFactory class.""" try: parent_classes = node.expr.inferred() except: # noqa: E722, pylint: disable=bare-except return False - parents = ('factory.declarations.SubFactory',) + parents = ('factory.declarations.SubFactory', 'factory.django.DjangoModelFactory') for parent_class in parent_classes: try: if parent_class.qname() in parents: return True + + if node_is_subclass(parent_class, *parents): + return True except AttributeError: continue From 3fdcbcc2d9c1cfdcaff965f2469269740a5249f8 Mon Sep 17 00:00:00 2001 From: "Mr. Senko" Date: Mon, 23 Jul 2018 15:34:17 +0300 Subject: [PATCH 5/6] Unify is_model_factory_meta_subclass() and is_model_meta_subclass --- pylint_django/augmentations/__init__.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/pylint_django/augmentations/__init__.py b/pylint_django/augmentations/__init__.py index c07cd1a2..b63c0b72 100644 --- a/pylint_django/augmentations/__init__.py +++ b/pylint_django/augmentations/__init__.py @@ -412,17 +412,8 @@ def is_model_meta_subclass(node): 'rest_framework.generics.GenericAPIView', 'rest_framework.viewsets.ReadOnlyModelViewSet', 'rest_framework.viewsets.ModelViewSet', - 'django_filters.filterset.FilterSet',) - 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',) + 'django_filters.filterset.FilterSet', + 'factory.django.DjangoModelFactory',) return node_is_subclass(node.parent, *parents) @@ -803,10 +794,6 @@ def apply_augmentations(linter): suppress_message(linter, MisdesignChecker.leave_classdef, 'too-few-public-methods', is_model_mpttmeta_subclass) # factory_boy's DjangoModelFactory - suppress_message(linter, DocStringChecker.visit_classdef, 'missing-docstring', is_model_factory_meta_subclass) - suppress_message(linter, NewStyleConflictChecker.visit_classdef, 'old-style-class', is_model_factory_meta_subclass) - suppress_message(linter, ClassChecker.visit_classdef, 'W0232', is_model_factory_meta_subclass) - suppress_message(linter, MisdesignChecker.leave_classdef, 'too-few-public-methods', is_model_factory_meta_subclass) suppress_message(linter, TypeChecker.visit_attribute, 'no-member', is_model_factory) # ForeignKey and OneToOneField From 538546044ac5a9785dc85369394d2de8c78349fc Mon Sep 17 00:00:00 2001 From: "Mr. Senko" Date: Wed, 25 Jul 2018 01:27:11 +0300 Subject: [PATCH 6/6] Be more strict when infering ForeignKey models specified as string The failing `func_noerror_foreignkeys` was caused by pylint_django trying to infer a ForeignKey('Author') field by looking at the astroid cache. In this case there was an Author model already defined in `func_noerror_duplicate_except_doesnotexist` which was inferred and of course this model didn't have the `author_name` field hence we got a no-member error. This commit tries to restrict where we load these models from and also takes into account the quirk that Django allows specifying 'appname.Model' instead of 'path.to.python.module.models.Model'. --- pylint_django/transforms/foreignkey.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/pylint_django/transforms/foreignkey.py b/pylint_django/transforms/foreignkey.py index 9645d179..544bb6d7 100644 --- a/pylint_django/transforms/foreignkey.py +++ b/pylint_django/transforms/foreignkey.py @@ -37,12 +37,31 @@ def infer_key_classes(node, context=None): break elif isinstance(arg, nodes.Const): try: - model_name = arg.value.split('.')[-1] # can be 'Model' or 'app.Model' + # can be 'Model' or 'app.Model' + module_name, _, model_name = arg.value.rpartition('.') except AttributeError: break + # when ForeignKey is specified only by class name we assume that + # this class must be found in the current module + if not module_name: + current_module = node.frame() + while not isinstance(current_module, nodes.Module): + current_module = current_module.parent.frame() + + module_name = current_module.name + elif not module_name.endswith('models'): + # otherwise Django allows specifying an app name first, e.g. + # ForeignKey('auth.User') so we try to convert that to + # 'auth.models', 'User' which works nicely with the `endswith()` + # comparison below + module_name += '.models' + for module in MANAGER.astroid_cache.values(): - if model_name in module.locals: + # only load model classes from modules which match the module in + # which *we think* they are defined. This will prevent infering + # other models of the same name which are found elsewhere! + if model_name in module.locals and module.name.endswith(module_name): class_defs = [ module_node for module_node in module.lookup(model_name)[1] if isinstance(module_node, nodes.ClassDef)