Statistics
| Revision:

gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / pylint / checkers / imports.py @ 745

History | View | Annotate | Download (25.7 KB)

1
# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
2
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
3
#
4
# This program is free software; you can redistribute it and/or modify it under
5
# the terms of the GNU General Public License as published by the Free Software
6
# Foundation; either version 2 of the License, or (at your option) any later
7
# version.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License along with
14
# this program; if not, write to the Free Software Foundation, Inc.,
15
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
"""imports checkers for Python code"""
17

    
18
import collections
19
from distutils import sysconfig
20
import os
21
import sys
22

    
23
import six
24

    
25
import astroid
26
from astroid import are_exclusive
27
from astroid.modutils import (get_module_part, is_standard_module,
28
                              file_from_modpath)
29

    
30
from pylint.interfaces import IAstroidChecker
31
from pylint.utils import EmptyReport, get_global_option
32
from pylint.checkers import BaseChecker
33
from pylint.checkers.utils import check_messages, node_ignores_exception
34
from pylint.graph import get_cycles, DotBackend
35
from pylint.reporters.ureports.nodes import VerbatimText, Paragraph
36

    
37

    
38
def _qualified_names(modname):
39
    """Split the names of the given module into subparts
40

41
    For example,
42
        _qualified_names('pylint.checkers.ImportsChecker')
43
    returns
44
        ['pylint', 'pylint.checkers', 'pylint.checkers.ImportsChecker']
45
    """
46
    names = modname.split('.')
47
    return ['.'.join(names[0:i+1]) for i in range(len(names))]
48

    
49

    
50
def _get_import_name(importnode, modname):
51
    """Get a prepared module name from the given import node
52

53
    In the case of relative imports, this will return the
54
    absolute qualified module name, which might be useful
55
    for debugging. Otherwise, the initial module name
56
    is returned unchanged.
57
    """
58
    if isinstance(importnode, astroid.ImportFrom):
59
        if importnode.level:
60
            root = importnode.root()
61
            if isinstance(root, astroid.Module):
62
                modname = root.relative_to_absolute_name(
63
                    modname, level=importnode.level)
64
    return modname
65

    
66

    
67
def _get_first_import(node, context, name, base, level):
68
    """return the node where [base.]<name> is imported or None if not found
69
    """
70
    fullname = '%s.%s' % (base, name) if base else name
71

    
72
    first = None
73
    found = False
74
    for first in context.body:
75
        if first is node:
76
            continue
77
        if first.scope() is node.scope() and first.fromlineno > node.fromlineno:
78
            continue
79
        if isinstance(first, astroid.Import):
80
            if any(fullname == iname[0] for iname in first.names):
81
                found = True
82
                break
83
        elif isinstance(first, astroid.ImportFrom):
84
            if level == first.level and any(
85
                    fullname == '%s.%s' % (first.modname, iname[0])
86
                    for iname in first.names):
87
                found = True
88
                break
89
    if found and not are_exclusive(first, node):
90
        return first
91

    
92
# utilities to represents import dependencies as tree and dot graph ###########
93

    
94
def _make_tree_defs(mod_files_list):
95
    """get a list of 2-uple (module, list_of_files_which_import_this_module),
96
    it will return a dictionary to represent this as a tree
97
    """
98
    tree_defs = {}
99
    for mod, files in mod_files_list:
100
        node = (tree_defs, ())
101
        for prefix in mod.split('.'):
102
            node = node[0].setdefault(prefix, [{}, []])
103
        node[1] += files
104
    return tree_defs
105

    
106

    
107
def _repr_tree_defs(data, indent_str=None):
108
    """return a string which represents imports as a tree"""
109
    lines = []
110
    nodes = data.items()
111
    for i, (mod, (sub, files)) in enumerate(sorted(nodes, key=lambda x: x[0])):
112
        if not files:
113
            files = ''
114
        else:
115
            files = '(%s)' % ','.join(files)
116
        if indent_str is None:
117
            lines.append('%s %s' % (mod, files))
118
            sub_indent_str = '  '
119
        else:
120
            lines.append(r'%s\-%s %s' % (indent_str, mod, files))
121
            if i == len(nodes)-1:
122
                sub_indent_str = '%s  ' % indent_str
123
            else:
124
                sub_indent_str = '%s| ' % indent_str
125
        if sub:
126
            lines.append(_repr_tree_defs(sub, sub_indent_str))
127
    return '\n'.join(lines)
128

    
129

    
130
def _dependencies_graph(filename, dep_info):
131
    """write dependencies as a dot (graphviz) file
132
    """
133
    done = {}
134
    printer = DotBackend(filename[:-4], rankdir='LR')
135
    printer.emit('URL="." node[shape="box"]')
136
    for modname, dependencies in sorted(six.iteritems(dep_info)):
137
        done[modname] = 1
138
        printer.emit_node(modname)
139
        for modname in dependencies:
140
            if modname not in done:
141
                done[modname] = 1
142
                printer.emit_node(modname)
143
    for depmodname, dependencies in sorted(six.iteritems(dep_info)):
144
        for modname in dependencies:
145
            printer.emit_edge(modname, depmodname)
146
    printer.generate(filename)
147

    
148

    
149
def _make_graph(filename, dep_info, sect, gtype):
150
    """generate a dependencies graph and add some information about it in the
151
    report's section
152
    """
153
    _dependencies_graph(filename, dep_info)
154
    sect.append(Paragraph('%simports graph has been written to %s'
155
                          % (gtype, filename)))
156

    
157

    
158
# the import checker itself ###################################################
159

    
160
MSGS = {
161
    'E0401': ('Unable to import %s',
162
              'import-error',
163
              'Used when pylint has been unable to import a module.',
164
              {'old_names': [('F0401', 'import-error')]}),
165
    'R0401': ('Cyclic import (%s)',
166
              'cyclic-import',
167
              'Used when a cyclic import between two or more modules is \
168
              detected.'),
169

    
170
    'W0401': ('Wildcard import %s',
171
              'wildcard-import',
172
              'Used when `from module import *` is detected.'),
173
    'W0402': ('Uses of a deprecated module %r',
174
              'deprecated-module',
175
              'Used a module marked as deprecated is imported.'),
176
    'W0403': ('Relative import %r, should be %r',
177
              'relative-import',
178
              'Used when an import relative to the package directory is '
179
              'detected.',
180
              {'maxversion': (3, 0)}),
181
    'W0404': ('Reimport %r (imported line %s)',
182
              'reimported',
183
              'Used when a module is reimported multiple times.'),
184
    'W0406': ('Module import itself',
185
              'import-self',
186
              'Used when a module is importing itself.'),
187

    
188
    'W0410': ('__future__ import is not the first non docstring statement',
189
              'misplaced-future',
190
              'Python 2.5 and greater require __future__ import to be the \
191
              first non docstring statement in the module.'),
192

    
193
    'C0410': ('Multiple imports on one line (%s)',
194
              'multiple-imports',
195
              'Used when import statement importing multiple modules is '
196
              'detected.'),
197
    'C0411': ('%s comes before %s',
198
              'wrong-import-order',
199
              'Used when PEP8 import order is not respected (standard imports '
200
              'first, then third-party libraries, then local imports)'),
201
    'C0412': ('Imports from package %s are not grouped',
202
              'ungrouped-imports',
203
              'Used when imports are not grouped by packages'),
204
    'C0413': ('Import "%s" should be placed at the top of the '
205
              'module',
206
              'wrong-import-position',
207
              'Used when code and imports are mixed'),
208
    }
209

    
210
class ImportsChecker(BaseChecker):
211
    """checks for
212
    * external modules dependencies
213
    * relative / wildcard imports
214
    * cyclic imports
215
    * uses of deprecated modules
216
    """
217

    
218
    __implements__ = IAstroidChecker
219

    
220
    name = 'imports'
221
    msgs = MSGS
222
    priority = -2
223

    
224
    if six.PY2:
225
        deprecated_modules = ('regsub', 'TERMIOS', 'Bastion', 'rexec')
226
    else:
227
        deprecated_modules = ('optparse', )
228
    options = (('deprecated-modules',
229
                {'default' : deprecated_modules,
230
                 'type' : 'csv',
231
                 'metavar' : '<modules>',
232
                 'help' : 'Deprecated modules which should not be used, \
233
separated by a comma'}
234
               ),
235
               ('import-graph',
236
                {'default' : '',
237
                 'type' : 'string',
238
                 'metavar' : '<file.dot>',
239
                 'help' : 'Create a graph of every (i.e. internal and \
240
external) dependencies in the given file (report RP0402 must not be disabled)'}
241
               ),
242
               ('ext-import-graph',
243
                {'default' : '',
244
                 'type' : 'string',
245
                 'metavar' : '<file.dot>',
246
                 'help' : 'Create a graph of external dependencies in the \
247
given file (report RP0402 must not be disabled)'}
248
               ),
249
               ('int-import-graph',
250
                {'default' : '',
251
                 'type' : 'string',
252
                 'metavar' : '<file.dot>',
253
                 'help' : 'Create a graph of internal dependencies in the \
254
given file (report RP0402 must not be disabled)'}
255
               ),
256
              )
257

    
258
    def __init__(self, linter=None):
259
        BaseChecker.__init__(self, linter)
260
        self.stats = None
261
        self.import_graph = None
262
        self._imports_stack = []
263
        self._first_non_import_node = None
264
        self.__int_dep_info = self.__ext_dep_info = None
265
        self.reports = (('RP0401', 'External dependencies',
266
                         self._report_external_dependencies),
267
                        ('RP0402', 'Modules dependencies graph',
268
                         self._report_dependencies_graph),
269
                       )
270

    
271
        self._site_packages = self._compute_site_packages()
272

    
273
    @staticmethod
274
    def _compute_site_packages():
275
        def _normalized_path(path):
276
            return os.path.normcase(os.path.abspath(path))
277

    
278
        paths = set()
279
        real_prefix = getattr(sys, 'real_prefix', None)
280
        for prefix in filter(None, (real_prefix, sys.prefix)):
281
            path = sysconfig.get_python_lib(prefix=prefix)
282
            path = _normalized_path(path)
283
            paths.add(path)
284

    
285
        # Handle Debian's derivatives /usr/local.
286
        if os.path.isfile("/etc/debian_version"):
287
            for prefix in filter(None, (real_prefix, sys.prefix)):
288
                libpython = os.path.join(prefix, "local", "lib",
289
                                         "python" + sysconfig.get_python_version(),
290
                                         "dist-packages")
291
                paths.add(libpython)
292
        return paths
293

    
294
    def open(self):
295
        """called before visiting project (i.e set of modules)"""
296
        self.linter.add_stats(dependencies={})
297
        self.linter.add_stats(cycles=[])
298
        self.stats = self.linter.stats
299
        self.import_graph = collections.defaultdict(set)
300
        self._ignored_modules = get_global_option(
301
            self, 'ignored-modules', default=[])
302

    
303
    def close(self):
304
        """called before visiting project (i.e set of modules)"""
305
        # don't try to compute cycles if the associated message is disabled
306
        if self.linter.is_message_enabled('cyclic-import'):
307
            vertices = list(self.import_graph)
308
            for cycle in get_cycles(self.import_graph, vertices=vertices):
309
                self.add_message('cyclic-import', args=' -> '.join(cycle))
310

    
311
    @check_messages('wrong-import-position', 'multiple-imports',
312
                    'relative-import', 'reimported')
313
    def visit_import(self, node):
314
        """triggered when an import statement is seen"""
315
        self._check_reimport(node)
316

    
317
        modnode = node.root()
318
        names = [name for name, _ in node.names]
319
        if len(names) >= 2:
320
            self.add_message('multiple-imports', args=', '.join(names), node=node)
321

    
322
        for name in names:
323
            self._check_deprecated_module(node, name)
324
            importedmodnode = self.get_imported_module(node, name)
325
            if isinstance(node.scope(), astroid.Module):
326
                self._check_position(node)
327
                self._record_import(node, importedmodnode)
328

    
329
            if importedmodnode is None:
330
                continue
331

    
332
            self._check_relative_import(modnode, node, importedmodnode, name)
333
            self._add_imported_module(node, importedmodnode.name)
334

    
335
    @check_messages(*(MSGS.keys()))
336
    def visit_importfrom(self, node):
337
        """triggered when a from statement is seen"""
338
        basename = node.modname
339
        self._check_misplaced_future(node)
340
        self._check_deprecated_module(node, basename)
341
        self._check_wildcard_imports(node)
342
        self._check_same_line_imports(node)
343
        self._check_reimport(node, basename=basename, level=node.level)
344

    
345
        modnode = node.root()
346
        importedmodnode = self.get_imported_module(node, basename)
347
        if isinstance(node.scope(), astroid.Module):
348
            self._check_position(node)
349
            self._record_import(node, importedmodnode)
350
        if importedmodnode is None:
351
            return
352
        self._check_relative_import(modnode, node, importedmodnode, basename)
353

    
354
        for name, _ in node.names:
355
            if name != '*':
356
                self._add_imported_module(node, '%s.%s' % (importedmodnode.name, name))
357

    
358
    @check_messages('wrong-import-order', 'ungrouped-imports',
359
                    'wrong-import-position')
360
    def leave_module(self, node):
361
        # Check imports are grouped by category (standard, 3rd party, local)
362
        std_imports, ext_imports, loc_imports = self._check_imports_order(node)
363

    
364
        # Check imports are grouped by package within a given category
365
        met = set()
366
        current_package = None
367
        for import_node, import_name in std_imports + ext_imports + loc_imports:
368
            package, _, _ = import_name.partition('.')
369
            if current_package and current_package != package and package in met:
370
                self.add_message('ungrouped-imports', node=import_node,
371
                                 args=package)
372
            current_package = package
373
            met.add(package)
374

    
375
        self._imports_stack = []
376
        self._first_non_import_node = None
377

    
378
    def visit_if(self, node):
379
        # if the node does not contain an import instruction, and if it is the
380
        # first node of the module, keep a track of it (all the import positions
381
        # of the module will be compared to the position of this first
382
        # instruction)
383
        if self._first_non_import_node:
384
            return
385
        if not isinstance(node.parent, astroid.Module):
386
            return
387
        if any(node.nodes_of_class((astroid.Import, astroid.ImportFrom))):
388
            return
389
        self._first_non_import_node = node
390

    
391
    visit_tryfinally = visit_tryexcept = visit_assignattr = visit_assign \
392
            = visit_ifexp = visit_comprehension = visit_if
393

    
394
    def visit_functiondef(self, node):
395
        # If it is the first non import instruction of the module, record it.
396
        if self._first_non_import_node:
397
            return
398

    
399
        # Check if the node belongs to an `If` or a `Try` block. If they
400
        # contain imports, skip recording this node.
401
        if not isinstance(node.parent.scope(), astroid.Module):
402
            return
403

    
404
        root = node
405
        while not isinstance(root.parent, astroid.Module):
406
            root = root.parent
407

    
408
        if isinstance(root, (astroid.If, astroid.TryFinally, astroid.TryExcept)):
409
            if any(root.nodes_of_class((astroid.Import, astroid.ImportFrom))):
410
                return
411

    
412
        self._first_non_import_node = node
413

    
414
    visit_classdef = visit_for = visit_while = visit_functiondef
415

    
416
    def _check_misplaced_future(self, node):
417
        basename = node.modname
418
        if basename == '__future__':
419
            # check if this is the first non-docstring statement in the module
420
            prev = node.previous_sibling()
421
            if prev:
422
                # consecutive future statements are possible
423
                if not (isinstance(prev, astroid.ImportFrom)
424
                        and prev.modname == '__future__'):
425
                    self.add_message('misplaced-future', node=node)
426
            return
427

    
428
    def _check_same_line_imports(self, node):
429
        # Detect duplicate imports on the same line.
430
        names = (name for name, _ in node.names)
431
        counter = collections.Counter(names)
432
        for name, count in counter.items():
433
            if count > 1:
434
                self.add_message('reimported', node=node,
435
                                 args=(name, node.fromlineno))
436

    
437
    def _check_position(self, node):
438
        """Check `node` import or importfrom node position is correct
439

440
        Send a message  if `node` comes before another instruction
441
        """
442
        # if a first non-import instruction has already been encountered,
443
        # it means the import comes after it and therefore is not well placed
444
        if self._first_non_import_node:
445
            self.add_message('wrong-import-position', node=node,
446
                             args=node.as_string())
447

    
448
    def _record_import(self, node, importedmodnode):
449
        """Record the package `node` imports from"""
450
        importedname = importedmodnode.name if importedmodnode else None
451
        if not importedname:
452
            importedname = node.names[0][0].split('.')[0]
453
        self._imports_stack.append((node, importedname))
454

    
455
    @staticmethod
456
    def _is_fallback_import(node, imports):
457
        imports = [import_node for (import_node, _) in imports]
458
        return any(astroid.are_exclusive(import_node, node)
459
                   for import_node in imports)
460

    
461
    def _check_imports_order(self, node):
462
        """Checks imports of module `node` are grouped by category
463

464
        Imports must follow this order: standard, 3rd party, local
465
        """
466
        extern_imports = []
467
        local_imports = []
468
        std_imports = []
469
        for node, modname in self._imports_stack:
470
            package = modname.split('.')[0]
471
            if is_standard_module(modname):
472
                std_imports.append((node, package))
473
                wrong_import = extern_imports or local_imports
474
                if not wrong_import:
475
                    continue
476
                if self._is_fallback_import(node, wrong_import):
477
                    continue
478
                self.add_message('wrong-import-order', node=node,
479
                                 args=('standard import "%s"' % node.as_string(),
480
                                       '"%s"' % wrong_import[0][0].as_string()))
481
            else:
482
                try:
483
                    filename = file_from_modpath([package])
484
                except ImportError:
485
                    continue
486
                if not filename:
487
                    continue
488

    
489
                filename = os.path.normcase(os.path.abspath(filename))
490
                if not any(filename.startswith(path) for path in self._site_packages):
491
                    local_imports.append((node, package))
492
                    continue
493
                extern_imports.append((node, package))
494
                if not local_imports:
495
                    continue
496
                self.add_message('wrong-import-order', node=node,
497
                                 args=('external import "%s"' % node.as_string(),
498
                                       '"%s"' % local_imports[0][0].as_string()))
499
        return std_imports, extern_imports, local_imports
500

    
501
    def get_imported_module(self, importnode, modname):
502
        try:
503
            return importnode.do_import_module(modname)
504
        except astroid.InferenceError as ex:
505
            dotted_modname = _get_import_name(importnode, modname)
506
            if str(ex) != modname:
507
                args = '%r (%s)' % (dotted_modname, ex)
508
            else:
509
                args = repr(dotted_modname)
510

    
511
            for submodule in _qualified_names(modname):
512
                if submodule in self._ignored_modules:
513
                    return None
514

    
515
            if not node_ignores_exception(importnode, ImportError):
516
                self.add_message("import-error", args=args, node=importnode)
517

    
518
    def _check_relative_import(self, modnode, importnode, importedmodnode,
519
                               importedasname):
520
        """check relative import. node is either an Import or From node, modname
521
        the imported module name.
522
        """
523
        if not self.linter.is_message_enabled('relative-import'):
524
            return
525
        if importedmodnode.file is None:
526
            return False # built-in module
527
        if modnode is importedmodnode:
528
            return False # module importing itself
529
        if modnode.absolute_import_activated() or getattr(importnode, 'level', None):
530
            return False
531
        if importedmodnode.name != importedasname:
532
            # this must be a relative import...
533
            self.add_message('relative-import',
534
                             args=(importedasname, importedmodnode.name),
535
                             node=importnode)
536

    
537
    def _add_imported_module(self, node, importedmodname):
538
        """notify an imported module, used to analyze dependencies"""
539
        module_file = node.root().file
540
        context_name = node.root().name
541
        base = os.path.splitext(os.path.basename(module_file))[0]
542

    
543
        # Determine if we have a `from .something import` in a package's
544
        # __init__. This means the module will never be able to import
545
        # itself using this condition (the level will be bigger or
546
        # if the same module is named as the package, it will be different
547
        # anyway).
548
        if isinstance(node, astroid.ImportFrom):
549
            if node.level and node.level > 0 and base == '__init__':
550
                return
551

    
552
        try:
553
            importedmodname = get_module_part(importedmodname,
554
                                              module_file)
555
        except ImportError:
556
            pass
557

    
558
        if context_name == importedmodname:
559
            self.add_message('import-self', node=node)
560
        elif not is_standard_module(importedmodname):
561
            # handle dependencies
562
            importedmodnames = self.stats['dependencies'].setdefault(
563
                importedmodname, set())
564
            if context_name not in importedmodnames:
565
                importedmodnames.add(context_name)
566
            # update import graph
567
            mgraph = self.import_graph[context_name]
568
            if importedmodname not in mgraph:
569
                mgraph.add(importedmodname)
570

    
571
    def _check_deprecated_module(self, node, mod_path):
572
        """check if the module is deprecated"""
573
        for mod_name in self.config.deprecated_modules:
574
            if mod_path == mod_name or mod_path.startswith(mod_name + '.'):
575
                self.add_message('deprecated-module', node=node, args=mod_path)
576

    
577
    def _check_reimport(self, node, basename=None, level=None):
578
        """check if the import is necessary (i.e. not already done)"""
579
        if not self.linter.is_message_enabled('reimported'):
580
            return
581

    
582
        frame = node.frame()
583
        root = node.root()
584
        contexts = [(frame, level)]
585
        if root is not frame:
586
            contexts.append((root, None))
587

    
588
        for context, level in contexts:
589
            for name, _ in node.names:
590
                first = _get_first_import(node, context, name, basename, level)
591
                if first is not None:
592
                    self.add_message('reimported', node=node,
593
                                     args=(name, first.fromlineno))
594

    
595
    def _report_external_dependencies(self, sect, _, dummy):
596
        """return a verbatim layout for displaying dependencies"""
597
        dep_info = _make_tree_defs(six.iteritems(self._external_dependencies_info()))
598
        if not dep_info:
599
            raise EmptyReport()
600
        tree_str = _repr_tree_defs(dep_info)
601
        sect.append(VerbatimText(tree_str))
602

    
603
    def _report_dependencies_graph(self, sect, _, dummy):
604
        """write dependencies as a dot (graphviz) file"""
605
        dep_info = self.stats['dependencies']
606
        if not dep_info or not (self.config.import_graph
607
                                or self.config.ext_import_graph
608
                                or self.config.int_import_graph):
609
            raise EmptyReport()
610
        filename = self.config.import_graph
611
        if filename:
612
            _make_graph(filename, dep_info, sect, '')
613
        filename = self.config.ext_import_graph
614
        if filename:
615
            _make_graph(filename, self._external_dependencies_info(),
616
                        sect, 'external ')
617
        filename = self.config.int_import_graph
618
        if filename:
619
            _make_graph(filename, self._internal_dependencies_info(),
620
                        sect, 'internal ')
621

    
622
    def _external_dependencies_info(self):
623
        """return cached external dependencies information or build and
624
        cache them
625
        """
626
        if self.__ext_dep_info is None:
627
            package = self.linter.current_name
628
            self.__ext_dep_info = result = {}
629
            for importee, importers in six.iteritems(self.stats['dependencies']):
630
                if not importee.startswith(package):
631
                    result[importee] = importers
632
        return self.__ext_dep_info
633

    
634
    def _internal_dependencies_info(self):
635
        """return cached internal dependencies information or build and
636
        cache them
637
        """
638
        if self.__int_dep_info is None:
639
            package = self.linter.current_name
640
            self.__int_dep_info = result = {}
641
            for importee, importers in six.iteritems(self.stats['dependencies']):
642
                if importee.startswith(package):
643
                    result[importee] = importers
644
        return self.__int_dep_info
645

    
646
    def _check_wildcard_imports(self, node):
647
        for name, _ in node.names:
648
            if name == '*':
649
                self.add_message('wildcard-import', args=node.modname, node=node)
650

    
651

    
652
def register(linter):
653
    """required method to auto register this checker """
654
    linter.register_checker(ImportsChecker(linter))