Passed
Push — master ( a5d88a...f810bc )
by Alexander
02:37
created

kiwi_lint.views.DjangoViewsChecker.__init__()   A

Complexity

Conditions 2

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 12
rs 9.95
c 0
b 0
f 0
cc 2
nop 2
1
"""function_based_views.py provides a pylint checker FunctionBasedViewChecker that warns
2
    against using function based views in Django.
3
"""
4
5
from importlib import import_module
6
7
import os
8
import django
9
from django.conf import settings
10
from django.urls.resolvers import URLResolver, URLPattern
11
12
from pylint import checkers
13
from pylint import interfaces
14
15
16
class DjangoViewsChecker(checkers.BaseChecker):
17
    """
18
    Base class for visiting only astroid modules which contain django views.
19
20
    Instance Attributes:
21
        view_module
22
        Type :: union[string|None]
23
24
            view_module == None,        if the current astroid module does _not_ contain views
25
            view_module == module.name, if the current astroid module contains _routed_ views
26
27
        views_by_module:
28
        Type :: dict[module_name: str, set[view_name: str]]
29
    """
30
31
    def __init__(self, linter):
32
        super().__init__(linter)
33
34
        if 'DJANGO_SETTINGS_MODULE' in os.environ:
35
            django.setup()
36
37
            project_urls = import_module(settings.ROOT_URLCONF)
38
            project_urls = project_urls.urlpatterns
39
        else:
40
            project_urls = []
41
        self.views_by_module = self._url_view_mapping(project_urls)
42
        self.view_module = None
43
44
    @classmethod
45
    def _url_view_mapping(cls, root_urlpatterns):
46
        def flatten(urlpatterns, prefix='^', result=None):
47
            """
48
                Flattens the url graph
49
50
                Returns a dictionary of the url pattern string as a key
51
                and tuple of the module name and the view name
52
            """
53
54
            if result is None:
55
                result = {None: []}
56
57
            for url in urlpatterns:
58
                if isinstance(url, URLPattern):
59
                    # path('someurl', view), meaning this is leaf node url
60
                    result.setdefault(url.callback.__module__, set()).add(url.callback.__name__)
61
                elif isinstance(url, URLResolver):
62
                    # path('someurl', include(some_url_patterns)), recurse on some_url_patterns
63
                    flatten(url.url_patterns,
64
                            prefix + url.pattern.regex.pattern.strip('^$'),
65
                            result)
66
67
            return result
68
69
        return flatten(root_urlpatterns)
70
71
    def visit_module(self, node):
72
        if node.name in self.views_by_module.keys() and not node.name.endswith(".admin"):
73
            self.view_module = node.name
74
75
    def leave_module(self, node):  # pylint: disable=unused-argument
76
        """
77
            Reset the current view module b/c otherwise we get false
78
            reports if a function in another module matches a view name for
79
            unreset module!
80
        """
81
        self.view_module = None
82
83
84
class ClassBasedViewChecker(DjangoViewsChecker):
85
    """
86
        This is where we are going to require that all views in
87
        this project be class based!
88
    """
89
    __implements__ = (interfaces.IAstroidChecker, )
90
91
    name = 'class-based-view-checker'
92
93
    msgs = {
94
        'R4611': ('Use class based views',
95
                  'class-based-view-required',
96
                  'Where possible use generic views. See: '
97
                  'https://docs.djangoproject.com/en/2.2/topics/class-based-views/generic-display/')
98
    }
99
100
    def visit_functiondef(self, node):
101
        if node.name in self.views_by_module[self.view_module]:
102
            self.add_message('class-based-view-required', node=node)
103