diff --git a/ChangeLog b/ChangeLog index 1bfec7da341..b78a697885f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,9 +7,14 @@ What's New in Pylint 2.4.0? Release date: TBA +* New check: ``import-outside-toplevel`` + + This check warns when modules are imported from places other than a + module toplevel, e.g. inside a function or a class. + * Added a new check, ``consider-using-sys-exit`` - This check is emitted when we detect that a quit() or exit() is invoked + This check is emitted when we detect that a quit() or exit() is invoked instead of sys.exit(), which is the preferred way of exiting in program. Close #2925 diff --git a/doc/whatsnew/2.4.rst b/doc/whatsnew/2.4.rst index 4c279ca9abd..ba34811a217 100644 --- a/doc/whatsnew/2.4.rst +++ b/doc/whatsnew/2.4.rst @@ -13,13 +13,18 @@ Summary -- Release highlights New checkers ============ +* ``import-outside-toplevel`` + + This check warns when modules are imported from places other than a + module toplevel, e.g. inside a function or a class. + * Added a new check, ``consider-using-sys-exit`` - This check is emitted when we detect that a quit() or exit() is invoked + This check is emitted when we detect that a quit() or exit() is invoked instead of sys.exit(), which is the preferred way of exiting in program. Close #2925 - + * Added a new check, ``arguments-out-of-order`` This check warns if you have arguments with names that match those in @@ -118,7 +123,7 @@ New checkers Other Changes ============= -* Don't emit ``protected-access`` when a single underscore prefixed attribute is used +* Don't emit ``protected-access`` when a single underscore prefixed attribute is used inside a special method Close #1802 diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index e6f3a8e8d95..aae37abffda 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -278,6 +278,12 @@ def _make_graph(filename, dep_info, sect, gtype): "Used when an import alias is same as original package." "e.g using import numpy as numpy instead of import numpy as np", ), + "C0415": ( + "Import outside toplevel (%s)", + "import-outside-toplevel", + "Used when an import statement is used anywhere other than the module + toplevel. Move this import to the top of the file." + ), } @@ -476,6 +482,7 @@ def visit_import(self, node): """triggered when an import statement is seen""" self._check_reimport(node) self._check_import_as_rename(node) + self._check_toplevel(node) modnode = node.root() names = [name for name, _ in node.names] @@ -969,6 +976,14 @@ def _wildcard_import_is_allowed(self, imported_module): and "__all__" in imported_module.locals ) + def _check_toplevel(self, node): + if isinstance(node.parent, (astroid.FunctionDef, astroid.ClassDef)): + self.add_message( + "import-outside-toplevel", + args=", ".join(name[0] for name in node.names), + node=node, + ) + def register(linter): """required method to auto register this checker """ diff --git a/pylint/graph.py b/pylint/graph.py index 3eb48022759..0dc7a1460bd 100644 --- a/pylint/graph.py +++ b/pylint/graph.py @@ -14,6 +14,7 @@ import codecs import os import os.path as osp +import subprocess import sys import tempfile @@ -80,8 +81,6 @@ def generate(self, outputfile=None, dotfile=None, mapfile=None): :rtype: str :return: a path to the generated file """ - import subprocess # introduced in py 2.4 - name = self.graphname if not dotfile: # if 'outputfile' is a dot file use it as 'dotfile' diff --git a/tests/functional/globals.py b/tests/functional/globals.py index 1a24ff73636..e7f3cc85bda 100644 --- a/tests/functional/globals.py +++ b/tests/functional/globals.py @@ -29,4 +29,4 @@ def define_constant(): def global_with_import(): """should only warn for global-statement""" global sys # [global-statement] - import sys + import sys # pylint: disable=import-outside-toplevel diff --git a/tests/functional/import_outside_toplevel.py b/tests/functional/import_outside_toplevel.py new file mode 100644 index 00000000000..620deb86f58 --- /dev/null +++ b/tests/functional/import_outside_toplevel.py @@ -0,0 +1,26 @@ +# pylint: disable=unused-import,multiple-imports,no-self-use,missing-docstring,invalid-name,too-few-public-methods + +import subprocess + + +def f(): + import json # [import-outside-toplevel] + + +def g(): + import os, sys # [import-outside-toplevel] + + +def h(): + import time as thyme # [import-outside-toplevel] + + +def i(): + import random as rand, socket as sock # [import-outside-toplevel] + + +class C: + import re # [import-outside-toplevel] + + def j(self): + import ssl # [import-outside-toplevel] diff --git a/tests/functional/import_outside_toplevel.txt b/tests/functional/import_outside_toplevel.txt new file mode 100644 index 00000000000..a618eaeccdf --- /dev/null +++ b/tests/functional/import_outside_toplevel.txt @@ -0,0 +1,6 @@ +import-outside-toplevel:7:f:Import outside toplevel (json) +import-outside-toplevel:11:g:Import outside toplevel (os, sys) +import-outside-toplevel:15:h:Import outside toplevel (time) +import-outside-toplevel:19:i:Import outside toplevel (random, socket) +import-outside-toplevel:23:C:Import outside toplevel (re) +import-outside-toplevel:26:C.j:Import outside toplevel (ssl) diff --git a/tests/functional/invalid_exceptions_raised.py b/tests/functional/invalid_exceptions_raised.py index 78e6b65a09b..34f740de4c9 100644 --- a/tests/functional/invalid_exceptions_raised.py +++ b/tests/functional/invalid_exceptions_raised.py @@ -1,4 +1,4 @@ -# pylint:disable=too-few-public-methods,no-init,import-error,missing-docstring, not-callable, useless-object-inheritance +# pylint:disable=too-few-public-methods,no-init,import-error,missing-docstring, not-callable, useless-object-inheritance,import-outside-toplevel """test pb with exceptions and old/new style classes""" @@ -77,7 +77,7 @@ def bad_case9(): def unknown_bases(): """Don't emit when we don't know the bases.""" - from lala import bala + from lala import bala # pylint: disable=import-outside-toplevel class MyException(bala): pass raise MyException diff --git a/tests/functional/invalid_name.py b/tests/functional/invalid_name.py index 1a949904edd..83dd22360e7 100644 --- a/tests/functional/invalid_name.py +++ b/tests/functional/invalid_name.py @@ -1,5 +1,5 @@ """ Tests for invalid-name checker. """ -# pylint: disable=unused-import, no-absolute-import, wrong-import-position +# pylint: disable=unused-import, no-absolute-import, wrong-import-position,import-outside-toplevel AAA = 24 try: diff --git a/tests/functional/member_checks.py b/tests/functional/member_checks.py index 8d1d636dcc5..ad7793fd519 100644 --- a/tests/functional/member_checks.py +++ b/tests/functional/member_checks.py @@ -1,5 +1,5 @@ # pylint: disable=print-statement,missing-docstring,no-self-use,too-few-public-methods,bare-except,broad-except, useless-object-inheritance -# pylint: disable=using-constant-test,expression-not-assigned, assigning-non-slot, unused-variable,pointless-statement, wrong-import-order, wrong-import-position +# pylint: disable=using-constant-test,expression-not-assigned, assigning-non-slot, unused-variable,pointless-statement, wrong-import-order, wrong-import-position,import-outside-toplevel from __future__ import print_function import six class Provider(object): diff --git a/tests/functional/raising_format_tuple.py b/tests/functional/raising_format_tuple.py index cfdda102326..0f3a3349a08 100644 --- a/tests/functional/raising_format_tuple.py +++ b/tests/functional/raising_format_tuple.py @@ -47,5 +47,5 @@ def bad_unicode(arg): def raise_something_without_name(arg): '''Regression test for nodes without .node attribute''' - import standard_exceptions # pylint: disable=import-error + import standard_exceptions # pylint: disable=import-error,import-outside-toplevel raise standard_exceptions.MyException(u'An %s', arg) # [raising-format-tuple] diff --git a/tests/functional/undefined_variable.py b/tests/functional/undefined_variable.py index 59af47e1976..545565eca3b 100644 --- a/tests/functional/undefined_variable.py +++ b/tests/functional/undefined_variable.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring, multiple-statements, useless-object-inheritance +# pylint: disable=missing-docstring, multiple-statements, useless-object-inheritance,import-outside-toplevel # pylint: disable=too-few-public-methods, no-init, no-self-use,bare-except,broad-except, import-error from __future__ import print_function DEFINED = 1 diff --git a/tests/functional/unused_variable.py b/tests/functional/unused_variable.py index e475a90389c..cb5215d99ed 100644 --- a/tests/functional/unused_variable.py +++ b/tests/functional/unused_variable.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring, invalid-name, too-few-public-methods, no-self-use, useless-object-inheritance +# pylint: disable=missing-docstring, invalid-name, too-few-public-methods, no-self-use, useless-object-inheritance,import-outside-toplevel def test_regression_737(): import xml # [unused-import] diff --git a/tests/functional/wrong_import_position4.py b/tests/functional/wrong_import_position4.py index 3f1174f9dc2..c728241b737 100644 --- a/tests/functional/wrong_import_position4.py +++ b/tests/functional/wrong_import_position4.py @@ -1,5 +1,5 @@ """Checks import position rule""" -# pylint: disable=unused-import,relative-import,ungrouped-imports,import-error,no-name-in-module,relative-beyond-top-level,unused-variable +# pylint: disable=unused-import,relative-import,ungrouped-imports,import-error,no-name-in-module,relative-beyond-top-level,unused-variable,import-outside-toplevel def method1(): """Method 1""" import x diff --git a/tests/input/func_w0404.py b/tests/input/func_w0404.py index 2ce7d651c4b..1a3db7f66c9 100644 --- a/tests/input/func_w0404.py +++ b/tests/input/func_w0404.py @@ -14,13 +14,13 @@ def no_reimport(): """docstring""" - import os + import os #pylint: disable=import-outside-toplevel print(os) def reimport(): """This function contains a reimport.""" - import sys + import sys #pylint: disable=import-outside-toplevel del sys diff --git a/tests/input/func_w0405.py b/tests/input/func_w0405.py index 50a069d7a9c..95c4d134f69 100644 --- a/tests/input/func_w0405.py +++ b/tests/input/func_w0405.py @@ -2,7 +2,7 @@ """ from __future__ import absolute_import, print_function -# pylint: disable=using-constant-test,ungrouped-imports,wrong-import-position +# pylint: disable=using-constant-test,ungrouped-imports,wrong-import-position,import-outside-toplevel import os from os.path import join, exists import os diff --git a/tests/input/func_w0612.py b/tests/input/func_w0612.py index 3d873bbebd4..2c6fee8b8df 100644 --- a/tests/input/func_w0612.py +++ b/tests/input/func_w0612.py @@ -1,6 +1,6 @@ """test unused variable """ -# pylint: disable=invalid-name, redefined-outer-name, no-absolute-import +# pylint: disable=invalid-name, redefined-outer-name, no-absolute-import,import-outside-toplevel from __future__ import print_function PATH = OS = collections = deque = None