diff options
62 files changed, 1056 insertions, 454 deletions
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 5b21f01..5409b8f 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -71,6 +71,6 @@ Order doesn't matter (not that much, at least ;) * Stéphane Wirtel: nonlocal-without-binding -* Dmitry Pribysh: multiple-imports. +* Dmitry Pribysh: multiple-imports, not-iterable, not-a-mapping, various patches. * Laura Medioni (Logilab): misplaced-comparison-constant @@ -2,6 +2,29 @@ ChangeLog for Pylint -------------------- -- + + * Don't warn about abstract classes instantiated in their own + body. Closes issue #627. + + * Obsolete options are not present by default in the generated + configuration file. Closes issue #632. + + * non-iterator-returned can detect classes with iterator-metaclasses. + Closes issue #679. + + * Add a new error, 'unsupported-membership-test', emitted when value + to the right of the 'in' operator doesn't support membership test + protocol (i.e. doesn't define __contains__/__iter__/__getitem__) + + * Add new errors, 'not-an-iterable', emitted when non-iterable value + is used in an iterating context (starargs, for-statement, + comprehensions, etc), and 'not-a-mapping', emitted when non-mapping + value is used in a mapping context. Closes issue #563. + + * Make 'no-self-use' checker not emit a warning if there is a 'super()' + call inside the method. + Closes issue #667. + * Add checker to identify multiple imports on one line. Closes issue #598. @@ -70,7 +93,7 @@ ChangeLog for Pylint * When checking for invalid arguments to a callable, in typecheck.py, look up for the __init__ in case the found __new__ comes from builtins. - + Since the __new__ comes from builtins, it will not have attached any information regarding what parameters it expects, so the check will be useless. Retrieving __init__ in that case will at least @@ -79,7 +102,7 @@ ChangeLog for Pylint * Don't emit no-member for classes with unknown bases. Since we don't know what those bases might add, we simply ignore - the error in this case. + the error in this case. * Lookup in the implicit metaclass when checking for no-member, if the class in question has an implicit metaclass, which is @@ -143,7 +166,7 @@ ChangeLog for Pylint * Don't emit undefined-variable if the node is guarded by a NameError, Exception or bare except clause. - + * Add a new warning, 'using-constant-test', which is emitted when a conditional statement (If, IfExp) uses a test which is always constant, such as numbers, classes, functions etc. This is most likely an error from the user's part. @@ -181,11 +204,11 @@ ChangeLog for Pylint * yield-outside-func is also emitted for `yield from`. * Add a new error, 'too-many-star-expressions', emitted when - there are more than one starred expression (*x) in an assignment. + there are more than one starred expression (`*x`) in an assignment. The warning is emitted only on Python 3. * Add a new error, 'invalid-star-assignment-target', emitted when - a starred expression (*x) is used as the lhs side of an assignment, + a starred expression (`*x`) is used as the lhs side of an assignment, as in `*x = [1, 2]`. This is not a SyntaxError on Python 3 though. * Detect a couple of objects which can't be base classes (bool, @@ -197,7 +220,7 @@ ChangeLog for Pylint SyntaxWarning on Python 2. * Add a new error, 'star-needs-assignment-target', emitted on Python 3 when - a Starred expression (*x) is not used in an assignment target. This is not + a Starred expression (`*x`) is not used in an assignment target. This is not caught when parsing the AST on Python 3, so it needs to be a separate check. * Add a new error, 'unsupported-binary-operation', emitted when @@ -322,7 +345,7 @@ ChangeLog for Pylint an async context manager block is used with an object which doesn't support this protocol (PEP 492). - * Add a new convention warning, 'singleton-comparison', emitted when + * Add a new convention warning, 'singleton-comparison', emitted when comparison to True, False or None is found. * Don't emit 'assigning-non-slot' for descriptors. Closes issue #652. @@ -352,7 +375,7 @@ ChangeLog for Pylint depth exceeded error, due to its visitor architecture. The peephole just transforms such calls, if it can, into the final resulting string and this exhibit a problem, because the visit_binop method stops being - called (in the optimized AST it will be a Const node). + called (in the optimized AST it will be a Const node). 2015-03-11 -- 1.4.2 diff --git a/doc/ide-integration.rst b/doc/ide-integration.rst index 30b48e5..ed0c1be 100644 --- a/doc/ide-integration.rst +++ b/doc/ide-integration.rst @@ -1,7 +1,7 @@ -================= - IDE integration -================= +============================ + Editor and IDE integration +============================ To use Pylint with: @@ -12,6 +12,7 @@ To use Pylint with: - gedit_, see https://launchpad.net/gedit-pylint-2 or https://wiki.gnome.org/Apps/Gedit/PylintPlugin, - WingIDE_, see http://www.wingware.com/doc/edit/pylint, - PyCharm_, see http://blog.saturnlaboratories.co.za/archive/2012/09/10/running-pylint-pycharm. + - TextMate_, Pylint is integrated in: @@ -30,6 +31,7 @@ Pylint is integrated in: .. _WingIDE: http://www.wingware.com/ .. _spyder: http://code.google.com/p/spyderlib/ .. _PyCharm: http://www.jetbrains.com/pycharm/ +.. _TextMate: http://macromates.com Using Pylint thru flymake in Emacs ================================== @@ -53,8 +55,8 @@ To enable flymake for Python, insert the following into your .emacs: ;; Set as a minor mode for Python (add-hook 'python-mode-hook '(lambda () (flymake-mode))) -Above stuff is in pylint/elisp/pylint-flymake.el, which should be automatically -installed on Debian systems, in which cases you don't have to put it in your .emacs file. +Above stuff is in ``pylint/elisp/pylint-flymake.el``, which should be automatically +installed on Debian systems, in which cases you don't have to put it in your ``.emacs`` file. Other things you may find useful to set: @@ -108,3 +110,59 @@ Setup the MS Visual Studio .NET 2003 editor to call Pylint .. image:: _static/vs2003_config.jpeg The output of Pylint is then shown in the "Output" pane of the editor. + + +.. _pylint_in_textmate: + +Integrate Pylint with TextMate +============================== + +Install Pylint in the usual way:: + + pip install pylint + +Install the `Python bundle for TextMate <https://github.com/textmate/python.tmbundle>`_: + +#. select *TextMate* > *Preferences* +#. select the *Bundles* tab +#. find and tick the *Python* bundle in the list + +You should now see it in *Bundles* > *Python*. + +In *Preferences*, select the *Variables* tab. If a ``TM_PYCHECKER`` variable is not already listed, add +it, with the value ``pylint``. + +The default keyboard shortcut to run the syntax checker is *Control-Shift-V* - open a ``.py`` file +in Textmate, and try it. + +You should see the output in a new window: + + PyCheckMate 1.2 – Pylint 1.4.4 + + No config file found, using default configuration + +Then all is well, and most likely Pylint will have expressed some opinions about your Python code +(or will exit with ``0`` if your code already conforms to its expectations). + +If you receive a message: + + Please install PyChecker, PyFlakes, Pylint, PEP 8 or flake8 for more extensive code checking. + +That means that Pylint wasn't found, which is likely an issue with command paths - TextMate needs +be looking for Pylint on the right paths. + +Check where Pylint has been installed, using ``which``:: + + $ which pylint + /usr/local/bin/pylint + +The output will tell you where Pylint can be found; in this case, in ``/usr/local/bin``. + +#. select *TextMate* > *Preferences* +#. select the *Variables* tab +#. find and check that a ``PATH`` variable exists, and that it contains the appropriate path (if + the path to Pylint were ``/usr/local/bin/pylint`` as above, then the variable would need to + contain ``/usr/local/bin``). An actual example in this case might be + ``$PATH:/opt/local/bin:/usr/local/bin:/usr/texbin``, which includes other paths. + +... and try running Pylint again. diff --git a/doc/run.rst b/doc/run.rst index 68bc8fd..42f8831 100644 --- a/doc/run.rst +++ b/doc/run.rst @@ -95,7 +95,7 @@ command line using the ``--rcfile`` option. Otherwise, Pylint searches for a configuration file in the following order and uses the first one it finds: #. ``pylintrc`` in the current working directory -# ``.pylintrc`` in the current working directory +#. ``.pylintrc`` in the current working directory #. If the current working directory is in a Python module, Pylint searches \ up the hierarchy of Python modules until it finds a ``pylintrc`` file. \ This allows you to specify coding standards on a module-by-module \ diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index ac86d44..234737e 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -41,6 +41,7 @@ from pylint.checkers.utils import ( is_inside_except, overrides_a_method, get_argument_from_call, + node_frame_class, NoSuchArgumentError, error_of_type, unimplemented_abstract_methods, @@ -309,7 +310,7 @@ class BasicErrorChecker(_BasicChecker): 'E0112': ('More than one starred expression in assignment', 'too-many-star-expressions', 'Emitted when there are more than one starred ' - 'expressions (*x) in an assignment. This is a SyntaxError.', + 'expressions (`*x`) in an assignment. This is a SyntaxError.', {'minversion': (3, 0)}), 'E0113': ('Starred assignment target must be in a list or tuple', 'invalid-star-assignment-target', @@ -497,8 +498,18 @@ class BasicErrorChecker(_BasicChecker): infered = next(node.func.infer()) except astroid.InferenceError: return + if not isinstance(infered, astroid.ClassDef): return + + klass = node_frame_class(node) + if klass is infered: + # Don't emit the warning if the class is instantiated + # in its own body or if the call is not an instance + # creation. If the class is instantiated into its own + # body, we're expecting that it knows what it is doing. + return + # __init__ was called metaclass = infered.metaclass() abstract_methods = _has_abstract_methods(infered) diff --git a/pylint/checkers/classes.py b/pylint/checkers/classes.py index 0fc0fae..86ccb19 100644 --- a/pylint/checkers/classes.py +++ b/pylint/checkers/classes.py @@ -140,6 +140,14 @@ def _is_attribute_property(name, klass): return True return False +def _has_bare_super_call(fundef_node): + for call in fundef_node.nodes_of_class(astroid.Call): + func = call.func + if (isinstance(func, astroid.Name) and + func.name == 'super' and + not call.args): + return True + return False MSGS = { 'F0202': ('Unable to check methods signature (%s / %s)', @@ -545,7 +553,8 @@ a metaclass class method.'} and not node.name in PYMETHODS and not (node.is_abstract() or overrides_a_method(class_node, node.name) or - decorated_with_property(node))): + decorated_with_property(node) or + (six.PY3 and _has_bare_super_call(node)))): self.add_message('no-self-use', node=node) def visit_attribute(self, node): @@ -997,23 +1006,39 @@ class SpecialMethodsChecker(BaseChecker): args=(node.name, expected_params, current_params, verb), node=node) + @staticmethod + def _is_iterator(node): + if node is astroid.YES: + # Just ignore YES objects. + return True + if isinstance(node, Generator): + # Generators can be itered. + return True + + if isinstance(node, astroid.Instance): + try: + node.local_attr(NEXT_METHOD) + return True + except astroid.NotFoundError: + pass + elif isinstance(node, astroid.ClassDef): + metaclass = node.metaclass() + if metaclass and isinstance(metaclass, astroid.ClassDef): + try: + metaclass.local_attr(NEXT_METHOD) + return True + except astroid.NotFoundError: + pass + return False + def _check_iter(self, node): try: infered = node.infer_call_result(node) except astroid.InferenceError: return - for infered_node in infered: - if (infered_node is astroid.YES - or isinstance(infered_node, Generator)): - continue - if isinstance(infered_node, astroid.Instance): - try: - infered_node.local_attr(NEXT_METHOD) - except astroid.NotFoundError: - self.add_message('non-iterator-returned', - node=node) - break + if not all(map(self._is_iterator, infered)): + self.add_message('non-iterator-returned', node=node) def _ancestors_to_call(klass_node, method='__init__'): diff --git a/pylint/checkers/similar.py b/pylint/checkers/similar.py index e500d2c..859a04b 100644 --- a/pylint/checkers/similar.py +++ b/pylint/checkers/similar.py @@ -305,7 +305,7 @@ class SimilarChecker(BaseChecker, Similar): def close(self): """compute and display similarities on closing (i.e. end of parsing)""" - total = sum([len(lineset) for lineset in self.linesets]) + total = sum(len(lineset) for lineset in self.linesets) duplicated = 0 stats = self.stats for num, couples in self._compute_sims(): diff --git a/pylint/checkers/spelling.py b/pylint/checkers/spelling.py index a5b7857..725f25a 100644 --- a/pylint/checkers/spelling.py +++ b/pylint/checkers/spelling.py @@ -21,6 +21,8 @@ import tokenize import string import re +import six + if sys.version_info[0] >= 3: maketrans = str.maketrans else: @@ -244,6 +246,10 @@ class SpellingChecker(BaseTokenChecker): return start_line = node.lineno + 1 + if six.PY2: + encoding = node.root().file_encoding + docstring = docstring.decode(encoding or sys.getdefaultencoding(), + 'replace') # Go through lines of docstring for idx, line in enumerate(docstring.splitlines()): diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 66ac05b..e9fddbd 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -44,6 +44,11 @@ _ZOPE_DEPRECATED = ( ) BUILTINS = six.moves.builtins.__name__ STR_FORMAT = "%s.str.format" % BUILTINS +ITER_METHOD = '__iter__' +NEXT_METHOD = 'next' if six.PY2 else '__next__' +GETITEM_METHOD = '__getitem__' +CONTAINS_METHOD = '__contains__' +KEYS_METHOD = 'keys' def _unflatten(iterable): @@ -84,6 +89,47 @@ def _is_owner_ignored(owner, name, ignored_classes, ignored_modules): return any(name == ignore or qname == ignore for ignore in ignored_classes) +def _hasattr(value, attr): + try: + value.getattr(attr) + return True + except astroid.NotFoundError: + return False + +def _is_comprehension(node): + comprehensions = (astroid.ListComp, + astroid.SetComp, + astroid.DictComp) + return isinstance(node, comprehensions) + + +def _is_iterable(value): + # '__iter__' is for standard iterables + # '__getitem__' is for strings and other old-style iterables + return _hasattr(value, ITER_METHOD) or _hasattr(value, GETITEM_METHOD) + + +def _is_iterator(value): + return _hasattr(value, NEXT_METHOD) and _hasattr(value, ITER_METHOD) + + +def _is_mapping(value): + return _hasattr(value, GETITEM_METHOD) and _hasattr(value, KEYS_METHOD) + +def _supports_membership_test(value): + return _hasattr(value, CONTAINS_METHOD) + + +def _is_inside_mixin_declaration(node): + while node is not None: + if isinstance(node, astroid.ClassDef): + name = getattr(node, 'name', None) + if name is not None and name.lower().endswith("mixin"): + return True + node = node.parent + return False + + MSGS = { 'E1101': ('%s %r has no %r member', 'no-member', @@ -147,6 +193,10 @@ MSGS = { 'E1132': ('Got multiple values for keyword argument %r in function call', 'repeated-keyword', 'Emitted when a function call got multiple values for a keyword.'), + 'E1135': ("Value '%s' doesn't support membership test", + 'unsupported-membership-test', + 'Emitted when an instance in membership test expression doesn\'t' + 'implement membership protocol (__contains__/__iter__/__getitem__)'), } # builtin sequence types in Python 2 and 3. @@ -790,7 +840,165 @@ accessed. Python regular expressions are accepted.'} self.add_message('unsupported-binary-operation', args=str(error), node=node) + def _check_membership_test(self, node): + # instance supports membership test in either of those cases: + # 1. instance defines __contains__ method + # 2. instance is iterable (defines __iter__ or __getitem__) + if _is_comprehension(node) or _is_inside_mixin_declaration(node): + return + + infered = helpers.safe_infer(node) + if infered is None or infered is astroid.YES: + return + + # classes can be iterables/containers too + if isinstance(infered, astroid.ClassDef): + if not helpers.has_known_bases(infered): + return + meta = infered.metaclass() + if meta is not None: + if _supports_membership_test(meta): + return + if _is_iterable(meta): + return + + if isinstance(infered, astroid.Instance): + if not helpers.has_known_bases(infered): + return + if _supports_membership_test(infered) or _is_iterable(infered): + return + + self.add_message('unsupported-membership-test', + args=node.as_string(), + node=node) + + @check_messages('unsupported-membership-test') + def visit_compare(self, node): + if len(node.ops) != 1: + return + operator, right = node.ops[0] + if operator in ['in', 'not in']: + self._check_membership_test(right) + + +class IterableChecker(BaseChecker): + """ + Checks for non-iterables used in an iterable context. + Contexts include: + - for-statement + - starargs in function call + - `yield from`-statement + - list, dict and set comprehensions + - generator expressions + Also checks for non-mappings in function call kwargs. + """ + + __implements__ = (IAstroidChecker,) + name = 'iterable_check' + + msgs = {'E1133': ('Non-iterable value %s is used in an iterating context', + 'not-an-iterable', + 'Used when a non-iterable value is used in place where' + 'iterable is expected'), + 'E1134': ('Non-mapping value %s is used in a mapping context', + 'not-a-mapping', + 'Used when a non-mapping value is used in place where' + 'mapping is expected'), + } + + def _check_iterable(self, node, root_node): + # for/set/dict-comprehensions can't be infered with astroid + # so we have to check for them explicitly + if _is_comprehension(node) or _is_inside_mixin_declaration(node): + return + + infered = helpers.safe_infer(node) + if infered is None or infered is astroid.YES: + return + + if isinstance(infered, astroid.ClassDef): + if not helpers.has_known_bases(infered): + return + # classobj can only be iterable if it has an iterable metaclass + meta = infered.metaclass() + if meta is not None: + if _is_iterable(meta): + return + if _is_iterator(meta): + return + + if isinstance(infered, astroid.Instance): + if not helpers.has_known_bases(infered): + return + if _is_iterable(infered) or _is_iterator(infered): + return + + self.add_message('not-an-iterable', + args=node.as_string(), + node=root_node) + + def _check_mapping(self, node, root_node): + if isinstance(node, astroid.DictComp) or _is_inside_mixin_declaration(node): + return + + infered = helpers.safe_infer(node) + if infered is None or infered is astroid.YES: + return + + if isinstance(infered, astroid.ClassDef): + if not helpers.has_known_bases(infered): + return + meta = infered.metaclass() + if meta is not None and _is_mapping(meta): + return + + if isinstance(infered, astroid.Instance): + if not helpers.has_known_bases(infered): + return + if _is_mapping(infered): + return + + self.add_message('not-a-mapping', + args=node.as_string(), + node=root_node) + + @check_messages('not-an-iterable') + def visit_for(self, node): + self._check_iterable(node.iter, node) + + @check_messages('not-an-iterable') + def visit_yieldfrom(self, node): + self._check_iterable(node.value, node) + + @check_messages('not-an-iterable', 'not-a-mapping') + def visit_call(self, node): + for stararg in node.starargs: + self._check_iterable(stararg.value, node) + for kwarg in node.kwargs: + self._check_mapping(kwarg.value, node) + + @check_messages('not-an-iterable') + def visit_listcomp(self, node): + for gen in node.generators: + self._check_iterable(gen.iter, node) + + @check_messages('not-an-iterable') + def visit_dictcomp(self, node): + for gen in node.generators: + self._check_iterable(gen.iter, node) + + @check_messages('not-an-iterable') + def visit_setcomp(self, node): + for gen in node.generators: + self._check_iterable(gen.iter, node) + + @check_messages('not-an-iterable') + def visit_generatorexp(self, node): + for gen in node.generators: + self._check_iterable(gen.iter, node) + def register(linter): """required method to auto register this checker """ linter.register_checker(TypeChecker(linter)) + linter.register_checker(IterableChecker(linter)) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 2ce12e8..79f6d05 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -248,7 +248,7 @@ MSGS = { 'W0614': ('Unused import %s from wildcard import', 'unused-wildcard-import', 'Used when an imported module or variable is not used from a \ - \'from X import *\' style import.'), + `\'from X import *\'` style import.'), 'W0621': ('Redefining name %r from outer scope (line %s)', 'redefined-outer-name', diff --git a/pylint/config.py b/pylint/config.py index fb5c2da..490f932 100644 --- a/pylint/config.py +++ b/pylint/config.py @@ -554,7 +554,8 @@ class OptionsManagerMixIn(object): if section in skipsections: continue options = [(n, d, v) for (n, d, v) in options - if d.get('type') is not None] + if d.get('type') is not None + and not d.get('deprecated')] if not options: continue if section not in sections: diff --git a/pylint/lint.py b/pylint/lint.py index 698f3e6..d1e39b2 100644 --- a/pylint/lint.py +++ b/pylint/lint.py @@ -736,19 +736,25 @@ class PyLinter(config.OptionsManagerMixIn, with _patch_sysmodules(): self._parallel_check(files_or_modules) - - def _parallel_task(self, files_or_modules): - # Prepare configuration for child linters. - filter_options = {'symbols', 'include-ids', 'long-help'} - filter_options.update([opt_name for opt_name, _ in self._external_opts]) + def _get_jobs_config(self): child_config = {} + filter_options = {'symbols', 'include-ids', 'long-help'} + filter_options.update((opt_name for opt_name, _ in self._external_opts)) for opt_providers in six.itervalues(self._all_options): for optname, optdict, val in opt_providers.options_and_values(): + if optdict.get('deprecated'): + continue + if optname not in filter_options: child_config[optname] = utils._format_option_value( optdict, val) child_config['python3_porting_mode'] = self._python3_porting_mode child_config['plugins'] = self._dynamic_plugins + return child_config + + def _parallel_task(self, files_or_modules): + # Prepare configuration for child linters. + child_config = self._get_jobs_config() children = [] manager = multiprocessing.Manager() diff --git a/pylint/reporters/__init__.py b/pylint/reporters/__init__.py index 5c4437a..664c62a 100644 --- a/pylint/reporters/__init__.py +++ b/pylint/reporters/__init__.py @@ -47,12 +47,9 @@ class BaseReporter(object): def __init__(self, output=None): self.linter = None - # self.include_ids = None # Deprecated - # self.symbols = None # Deprecated self.section = 0 self.out = None self.out_encoding = None - self.encode = None self.set_output(output) # Build the path prefix to strip to get relative paths self.path_strip_prefix = os.getcwd() + os.sep @@ -75,12 +72,11 @@ class BaseReporter(object): def set_output(self, output=None): """set output stream""" self.out = output or sys.stdout - # py3k streams handle their encoding : - if sys.version_info >= (3, 0): - self.encode = lambda x: x - return - def encode(string): + if six.PY3: + encode = lambda self, string: string + else: + def encode(self, string): if not isinstance(string, six.text_type): return string encoding = (getattr(self.out, 'encoding', None) or @@ -90,7 +86,6 @@ class BaseReporter(object): # source code line that can't be encoded with the current locale # settings return string.encode(encoding, 'replace') - self.encode = encode def writeln(self, string=''): """write a line in the output buffer""" @@ -110,12 +105,10 @@ class BaseReporter(object): # Event callbacks def on_set_current_module(self, module, filepath): - """starting analyzis of a module""" - pass + """Hook called when a module starts to be analysed.""" def on_close(self, stats, previous_stats): - """global end of analyzis""" - pass + """Hook called when a module finished analyzing.""" class CollectingReporter(BaseReporter): diff --git a/pylint/reporters/html.py b/pylint/reporters/html.py index b2214b1..b06ee16 100644 --- a/pylint/reporters/html.py +++ b/pylint/reporters/html.py @@ -17,6 +17,8 @@ import itertools import string import sys +import six + from pylint.interfaces import IReporter from pylint.reporters import BaseReporter from pylint.reporters.ureports.html_writer import HTMLWriter @@ -67,7 +69,9 @@ class HTMLReporter(BaseReporter): self._parse_template() # We want to add the lines given by the template - self.msgs += [str(getattr(msg, field)) for field in self.msgargs] + values = [getattr(msg, field) for field in self.msgargs] + self.msgs += [value if isinstance(value, six.text_type) else str(value) + for value in values] def set_output(self, output=None): """set output stream diff --git a/pylint/reporters/ureports/__init__.py b/pylint/reporters/ureports/__init__.py index 0da4051..02322db 100644 --- a/pylint/reporters/ureports/__init__.py +++ b/pylint/reporters/ureports/__init__.py @@ -26,8 +26,6 @@ import sys import six -# pylint: disable=method-hidden; Weird API in compute_content. - class BaseWriter(object): """base class for ureport writers""" @@ -43,7 +41,6 @@ class BaseWriter(object): if not encoding: encoding = getattr(stream, 'encoding', 'UTF-8') self.encoding = encoding or 'UTF-8' - self.__compute_funcs = [] self.out = stream self.begin_format() layout.accept(self) @@ -62,10 +59,7 @@ class BaseWriter(object): def write(self, string): """write a string in the output buffer""" - try: - self.out.write(string) - except UnicodeEncodeError: - self.out.write(string.encode(self.encoding)) + self.out.write(string) def begin_format(self): """begin to format a layout""" @@ -98,27 +92,15 @@ class BaseWriter(object): return an iterator on strings (one for each child element) """ - # use cells ! - def write(data): - try: - stream.write(data) - except UnicodeEncodeError: - stream.write(data.encode(self.encoding)) - def writeln(data=u''): - try: - stream.write(data + os.linesep) - except UnicodeEncodeError: - stream.write(data.encode(self.encoding) + os.linesep) - self.write = write - self.writeln = writeln - self.__compute_funcs.append((write, writeln)) - for child in layout.children: - stream = six.StringIO() - child.accept(self) - yield stream.getvalue() - self.__compute_funcs.pop() + # Patch the underlying output stream with a fresh-generated stream, + # which is used to store a temporary representation of a child + # node. + out = self.out try: - self.write, self.writeln = self.__compute_funcs[-1] - except IndexError: - del self.write - del self.writeln + for child in layout.children: + stream = six.StringIO() + self.out = stream + child.accept(self) + yield stream.getvalue() + finally: + self.out = out diff --git a/pylint/reporters/ureports/html_writer.py b/pylint/reporters/ureports/html_writer.py index 005ac62..c5f74d3 100644 --- a/pylint/reporters/ureports/html_writer.py +++ b/pylint/reporters/ureports/html_writer.py @@ -27,18 +27,6 @@ class HTMLWriter(BaseWriter): super(HTMLWriter, self).__init__() self.snippet = snippet - @staticmethod - def handle_attrs(layout): - """get an attribute string from layout member attributes""" - attrs = u'' - klass = getattr(layout, 'klass', None) - if klass: - attrs += u' class="%s"' % klass - nid = getattr(layout, 'id', None) - if nid: - attrs += u' id="%s"' % nid - return attrs - def begin_format(self): """begin to format a layout""" super(HTMLWriter, self).begin_format() @@ -55,20 +43,20 @@ class HTMLWriter(BaseWriter): def visit_section(self, layout): """display a section as html, using div + h[section level]""" self.section += 1 - self.writeln(u'<div%s>' % self.handle_attrs(layout)) + self.writeln(u'<div>') self.format_children(layout) self.writeln(u'</div>') self.section -= 1 def visit_title(self, layout): """display a title using <hX>""" - self.write(u'<h%s%s>' % (self.section, self.handle_attrs(layout))) + self.write(u'<h%s>' % self.section) self.format_children(layout) self.writeln(u'</h%s>' % self.section) def visit_table(self, layout): """display a table as html""" - self.writeln(u'<table%s>' % self.handle_attrs(layout)) + self.writeln(u'<table>') table_content = self.get_table_content(layout) for i, row in enumerate(table_content): if i == 0 and layout.rheaders: @@ -76,7 +64,7 @@ class HTMLWriter(BaseWriter): elif i+1 == len(table_content) and layout.rrheaders: self.writeln(u'<tr class="header">') else: - self.writeln(u'<tr class="%s">' % (i%2 and 'even' or 'odd')) + self.writeln(u'<tr class="%s">' % (u'even' if i % 2 else u'odd')) for j, cell in enumerate(row): cell = cell or u' ' if (layout.rheaders and i == 0) or \ @@ -89,30 +77,12 @@ class HTMLWriter(BaseWriter): self.writeln(u'</tr>') self.writeln(u'</table>') - def visit_list(self, layout): - """display a list as html""" - self.writeln(u'<ul%s>' % self.handle_attrs(layout)) - for row in list(self.compute_content(layout)): - self.writeln(u'<li>%s</li>' % row) - self.writeln(u'</ul>') - def visit_paragraph(self, layout): """display links (using <p>)""" self.write(u'<p>') self.format_children(layout) self.write(u'</p>') - def visit_span(self, layout): - """display links (using <p>)""" - self.write(u'<span%s>' % self.handle_attrs(layout)) - self.format_children(layout) - self.write(u'</span>') - - def visit_link(self, layout): - """display links (using <a>)""" - self.write(u' <a href="%s"%s>%s</a>' % (layout.url, - self.handle_attrs(layout), - layout.label)) def visit_verbatimtext(self, layout): """display verbatim text (using <pre>)""" self.write(u'<pre>') diff --git a/pylint/reporters/ureports/nodes.py b/pylint/reporters/ureports/nodes.py index 01cbcb7..104ba83 100644 --- a/pylint/reporters/ureports/nodes.py +++ b/pylint/reporters/ureports/nodes.py @@ -22,32 +22,58 @@ A micro report is a tree of layout and content objects. from six import string_types -from pylint.reporters.ureports.tree import VNode +class VNode(object): -class BaseComponent(VNode): - """base report component + def __init__(self, nid=None): + self.id = nid + # navigation + self.parent = None + self.children = [] - attributes - * id : the component's optional id - * klass : the component's optional klass - """ - def __init__(self, id=None, klass=None): - super(BaseComponent, self).__init__(id) - self.klass = klass + def __iter__(self): + return iter(self.children) - -class BaseLayout(BaseComponent): + def append(self, child): + """add a node to children""" + self.children.append(child) + child.parent = self + + def insert(self, index, child): + """insert a child node""" + self.children.insert(index, child) + child.parent = self + + def _get_visit_name(self): + """ + return the visit name for the mixed class. When calling 'accept', the + method <'visit_' + name returned by this method> will be called on the + visitor + """ + try: + return self.TYPE.replace('-', '_') + except Exception: + return self.__class__.__name__.lower() + + def accept(self, visitor, *args, **kwargs): + func = getattr(visitor, 'visit_%s' % self._get_visit_name()) + return func(self, *args, **kwargs) + + def leave(self, visitor, *args, **kwargs): + func = getattr(visitor, 'leave_%s' % self._get_visit_name()) + return func(self, *args, **kwargs) + + +class BaseLayout(VNode): """base container node attributes - * BaseComponent attributes * children : components in this table (i.e. the table's cells) """ def __init__(self, children=(), **kwargs): super(BaseLayout, self).__init__(**kwargs) for child in children: - if isinstance(child, BaseComponent): + if isinstance(child, VNode): self.append(child) else: self.add_text(child) @@ -71,11 +97,10 @@ class BaseLayout(BaseComponent): # non container nodes ######################################################### -class Text(BaseComponent): +class Text(VNode): """a text portion attributes : - * BaseComponent attributes * data : the text value as an encoded or unicode string """ def __init__(self, data, escaped=True, **kwargs): @@ -91,26 +116,9 @@ class VerbatimText(Text): """a verbatim text, display the raw data attributes : - * BaseComponent attributes * data : the text value as an encoded or unicode string """ - -class Link(BaseComponent): - """a labelled link - - attributes : - * BaseComponent attributes - * url : the link's target (REQUIRED) - * label : the link's label as a string (use the url by default) - """ - def __init__(self, url, label=None, **kwargs): - super(Link, self).__init__(**kwargs) - assert url - self.url = url - self.label = label or url - - # container nodes ############################################################# class Section(BaseLayout): @@ -173,11 +181,3 @@ class Table(BaseLayout): self.cheaders = cheaders self.rrheaders = rrheaders self.rcheaders = rcheaders - - -class List(BaseLayout): - """some list data - - attributes : - * BaseLayout attributes - """ diff --git a/pylint/reporters/ureports/text_writer.py b/pylint/reporters/ureports/text_writer.py index 545f999..6109b95 100644 --- a/pylint/reporters/ureports/text_writer.py +++ b/pylint/reporters/ureports/text_writer.py @@ -19,10 +19,6 @@ from __future__ import print_function -import os - -from six.moves import range - from pylint.reporters.ureports import BaseWriter @@ -36,7 +32,6 @@ class TextWriter(BaseWriter): def begin_format(self): super(TextWriter, self).begin_format() self.list_level = 0 - self.pending_urls = [] def visit_section(self, layout): """display a section as text @@ -44,11 +39,6 @@ class TextWriter(BaseWriter): self.section += 1 self.writeln() self.format_children(layout) - if self.pending_urls: - self.writeln() - for label, url in self.pending_urls: - self.writeln(u'.. _`%s`: %s' % (label, url)) - self.pending_urls = [] self.section -= 1 self.writeln() @@ -65,23 +55,15 @@ class TextWriter(BaseWriter): self.format_children(layout) self.writeln() - def visit_span(self, layout): - """enter a span""" - self.format_children(layout) - def visit_table(self, layout): """display a table as text""" table_content = self.get_table_content(layout) # get columns width cols_width = [0]*len(table_content[0]) for row in table_content: - for index in range(len(row)): - col = row[index] + for index, col in enumerate(row): cols_width[index] = max(cols_width[index], len(col)) - if layout.klass == 'field': - self.field_table(layout, table_content, cols_width) - else: - self.default_table(layout, table_content, cols_width) + self.default_table(layout, table_content, cols_width) self.writeln() def default_table(self, layout, table_content, cols_width): @@ -89,47 +71,21 @@ class TextWriter(BaseWriter): cols_width = [size+1 for size in cols_width] format_strings = u' '.join([u'%%-%ss'] * len(cols_width)) format_strings = format_strings % tuple(cols_width) - format_strings = format_strings.split(' ') + format_strings = format_strings.split(u' ') table_linesep = u'\n+' + u'+'.join([u'-'*w for w in cols_width]) + u'+\n' headsep = u'\n+' + u'+'.join([u'='*w for w in cols_width]) + u'+\n' # FIXME: layout.cheaders self.write(table_linesep) - for i in range(len(table_content)): + for index, line in enumerate(table_content): self.write(u'|') - line = table_content[i] - for j in range(len(line)): - self.write(format_strings[j] % line[j]) + for line_index, at_index in enumerate(line): + self.write(format_strings[line_index] % at_index) self.write(u'|') - if i == 0 and layout.rheaders: + if index == 0 and layout.rheaders: self.write(headsep) else: self.write(table_linesep) - def field_table(self, layout, table_content, cols_width): - """special case for field table""" - assert layout.cols == 2 - format_string = u'%s%%-%ss: %%s' % (os.linesep, cols_width[0]) - for field, value in table_content: - self.write(format_string % (field, value)) - - def visit_list(self, layout): - """display a list layout as text""" - bullet = BULLETS[self.list_level % len(BULLETS)] - indent = ' ' * self.list_level - self.list_level += 1 - for child in layout.children: - self.write(u'%s%s%s ' % (os.linesep, indent, bullet)) - child.accept(self) - self.list_level -= 1 - - def visit_link(self, layout): - """add a hyperlink""" - if layout.label != layout.url: - self.write(u'`%s`_' % layout.label) - self.pending_urls.append((layout.label, layout.url)) - else: - self.write(layout.url) - def visit_verbatimtext(self, layout): """display a verbatim layout as text (so difficult ;) """ diff --git a/pylint/reporters/ureports/tree.py b/pylint/reporters/ureports/tree.py deleted file mode 100644 index 99965f2..0000000 --- a/pylint/reporters/ureports/tree.py +++ /dev/null @@ -1,235 +0,0 @@ -# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved. -# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# -# This file is part of pylint. -# -# logilab-common is free software: you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation, either version 2.1 of the License, or (at your option) any -# later version. -# -# pylint is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License along -# with pylint. If not, see <http://www.gnu.org/licenses/>. - - -class NodeNotFound(Exception): - """raised when a node has not been found""" - -EX_SIBLING_NOT_FOUND = "No such sibling as '%s'" -EX_CHILD_NOT_FOUND = "No such child as '%s'" -EX_NODE_NOT_FOUND = "No such node as '%s'" - - -# Base node ################################################################### - -class Node(object): - """a basic tree node, characterized by an id""" - - def __init__(self, nid=None): - self.id = nid - # navigation - self.parent = None - self.children = [] - - def __iter__(self): - return iter(self.children) - - def __str__(self, indent=0): - s = ['%s%s %s' % (' '*indent, self.__class__.__name__, self.id)] - indent += 2 - for child in self.children: - try: - s.append(child.__str__(indent)) - except TypeError: - s.append(child.__str__()) - return '\n'.join(s) - - def is_leaf(self): - return not self.children - - def append(self, child): - """add a node to children""" - self.children.append(child) - child.parent = self - - def remove(self, child): - """remove a child node""" - self.children.remove(child) - child.parent = None - - def insert(self, index, child): - """insert a child node""" - self.children.insert(index, child) - child.parent = self - - def replace(self, old_child, new_child): - """replace a child node with another""" - i = self.children.index(old_child) - self.children.pop(i) - self.children.insert(i, new_child) - new_child.parent = self - - def get_sibling(self, nid): - """return the sibling node that has given id""" - try: - return self.parent.get_child_by_id(nid) - except NodeNotFound: - raise NodeNotFound(EX_SIBLING_NOT_FOUND % nid) - - def next_sibling(self): - """ - return the next sibling for this node if any - """ - parent = self.parent - if parent is None: - # root node has no sibling - return None - index = parent.children.index(self) - try: - return parent.children[index+1] - except IndexError: - return None - - def previous_sibling(self): - """ - return the previous sibling for this node if any - """ - parent = self.parent - if parent is None: - # root node has no sibling - return None - index = parent.children.index(self) - if index > 0: - return parent.children[index-1] - return None - - def get_node_by_id(self, nid): - """ - return node in whole hierarchy that has given id - """ - root = self.root() - try: - return root.get_child_by_id(nid, 1) - except NodeNotFound: - raise NodeNotFound(EX_NODE_NOT_FOUND % nid) - - def get_child_by_id(self, nid, recurse=None): - """ - return child of given id - """ - if self.id == nid: - return self - for c in self.children: - if recurse: - try: - return c.get_child_by_id(nid, 1) - except NodeNotFound: - continue - if c.id == nid: - return c - raise NodeNotFound(EX_CHILD_NOT_FOUND % nid) - - def get_child_by_path(self, path): - """ - return child of given path (path is a list of ids) - """ - if len(path) > 0 and path[0] == self.id: - if len(path) == 1: - return self - else: - for c in self.children: - try: - return c.get_child_by_path(path[1:]) - except NodeNotFound: - pass - raise NodeNotFound(EX_CHILD_NOT_FOUND % path) - - def depth(self): - """ - return depth of this node in the tree - """ - if self.parent is not None: - return 1 + self.parent.depth() - else: - return 0 - - def depth_down(self): - """ - return depth of the tree from this node - """ - if self.children: - return 1 + max([c.depth_down() for c in self.children]) - return 1 - - def width(self): - """ - return the width of the tree from this node - """ - return len(self.leaves()) - - def root(self): - """ - return the root node of the tree - """ - if self.parent is not None: - return self.parent.root() - return self - - def leaves(self): - """ - return a list with all the leaves nodes descendant from this node - """ - leaves = [] - if self.children: - for child in self.children: - leaves += child.leaves() - return leaves - else: - return [self] - - def flatten(self, _list=None): - """ - return a list with all the nodes descendant from this node - """ - if _list is None: - _list = [] - _list.append(self) - for c in self.children: - c.flatten(_list) - return _list - - def lineage(self): - """ - return list of parents up to root node - """ - lst = [self] - if self.parent is not None: - lst.extend(self.parent.lineage()) - return lst - - -class VNode(Node): - - def get_visit_name(self): - """ - return the visit name for the mixed class. When calling 'accept', the - method <'visit_' + name returned by this method> will be called on the - visitor - """ - try: - return self.TYPE.replace('-', '_') - except Exception: - return self.__class__.__name__.lower() - - def accept(self, visitor, *args, **kwargs): - func = getattr(visitor, 'visit_%s' % self.get_visit_name()) - return func(self, *args, **kwargs) - - def leave(self, visitor, *args, **kwargs): - func = getattr(visitor, 'leave_%s' % self.get_visit_name()) - return func(self, *args, **kwargs) diff --git a/pylint/test/functional/abstract_class_instantiated_in_class.py b/pylint/test/functional/abstract_class_instantiated_in_class.py new file mode 100644 index 0000000..9402c12 --- /dev/null +++ b/pylint/test/functional/abstract_class_instantiated_in_class.py @@ -0,0 +1,20 @@ +"""Don't warn if the class is instantiated in its own body."""
+# pylint: disable=missing-docstring
+
+
+import abc
+
+import six
+
+
+@six.add_metaclass(abc.ABCMeta)
+class Ala(object):
+
+ @abc.abstractmethod
+ def bala(self):
+ pass
+
+ @classmethod
+ def portocala(cls):
+ instance = cls()
+ return instance
diff --git a/pylint/test/functional/arguments.py b/pylint/test/functional/arguments.py index 9f0dc63..8ae008d 100644 --- a/pylint/test/functional/arguments.py +++ b/pylint/test/functional/arguments.py @@ -147,6 +147,8 @@ class Issue642(object): # since they have something invalid. from ala_bala_portocola import unknown +# pylint: disable=not-a-mapping,not-an-iterable + function_1_arg(*unknown) function_1_arg(1, *2) function_1_arg(1, 2, 3, **unknown) diff --git a/pylint/test/functional/iterable_context.py b/pylint/test/functional/iterable_context.py new file mode 100644 index 0000000..8dfcbbe --- /dev/null +++ b/pylint/test/functional/iterable_context.py @@ -0,0 +1,141 @@ +""" +Checks that primitive values are not used in an +iterating/mapping context. +""" +# pylint: disable=missing-docstring,invalid-name,too-few-public-methods,no-init,no-self-use,import-error,unused-argument,bad-mcs-method-argument +from __future__ import print_function + +# primitives +numbers = [1, 2, 3] + +for i in numbers: + pass + +for i in iter(numbers): + pass + +for i in "123": + pass + +for i in u"123": + pass + +for i in b"123": + pass + +for i in bytearray(b"123"): + pass + +for i in set(numbers): + pass + +for i in frozenset(numbers): + pass + +for i in dict(a=1, b=2): + pass + +# comprehensions +for i in [x for x in range(10)]: + pass + +for i in {x for x in range(1, 100, 2)}: + pass + +for i in {x: 10 - x for x in range(10)}: + pass + +# generators +def powers_of_two(): + k = 0 + while k < 10: + yield 2 ** k + k += 1 + +for i in powers_of_two(): + pass + +for i in powers_of_two: # [not-an-iterable] + pass + +# check for custom iterators +class A(object): + pass + +class B(object): + def __iter__(self): + return self + + def __next__(self): + return 1 + + def next(self): + return 1 + +class C(object): + "old-style iterator" + def __getitem__(self, k): + if k > 10: + raise IndexError + return k + 1 + + def __len__(self): + return 10 + +for i in C(): + print(i) + + +def test(*args): + print(args) + + +test(*A()) # [not-an-iterable] +test(*B()) +test(*B) # [not-an-iterable] +for i in A(): # [not-an-iterable] + pass +for i in B(): + pass +for i in B: # [not-an-iterable] + pass + +for i in range: # [not-an-iterable] + pass + +# check that primitive non-iterable types are catched +for i in True: # [not-an-iterable] + pass + +for i in None: # [not-an-iterable] + pass + +for i in 8.5: # [not-an-iterable] + pass + +for i in 10: # [not-an-iterable] + pass + + +# skip uninferable instances +from some_missing_module import Iterable + +class MyClass(Iterable): + pass + +m = MyClass() +for i in m: + print(i) + +# skip checks if statement is inside mixin class +class ManagedAccessViewMixin(object): + access_requirements = None + + def get_access_requirements(self): + return self.access_requirements + + def dispatch(self, *_args, **_kwargs): + klasses = self.get_access_requirements() + + for requirement in klasses: + print(requirement) diff --git a/pylint/test/functional/iterable_context.txt b/pylint/test/functional/iterable_context.txt new file mode 100644 index 0000000..fbe1433 --- /dev/null +++ b/pylint/test/functional/iterable_context.txt @@ -0,0 +1,10 @@ +not-an-iterable:58::Non-iterable value powers_of_two is used in an iterating context +not-an-iterable:93::Non-iterable value A() is used in an iterating context +not-an-iterable:95::Non-iterable value B is used in an iterating context +not-an-iterable:96::Non-iterable value A() is used in an iterating context +not-an-iterable:100::Non-iterable value B is used in an iterating context +not-an-iterable:103::Non-iterable value range is used in an iterating context +not-an-iterable:107::Non-iterable value True is used in an iterating context +not-an-iterable:110::Non-iterable value None is used in an iterating context +not-an-iterable:113::Non-iterable value 8.5 is used in an iterating context +not-an-iterable:116::Non-iterable value 10 is used in an iterating context diff --git a/pylint/test/functional/iterable_context_py2.py b/pylint/test/functional/iterable_context_py2.py new file mode 100644 index 0000000..8687f84 --- /dev/null +++ b/pylint/test/functional/iterable_context_py2.py @@ -0,0 +1,18 @@ +""" +Checks that iterable metaclasses are recognized by pylint. +""" +# pylint: disable=missing-docstring,too-few-public-methods,no-init,no-self-use,unused-argument,bad-mcs-method-argument + +# metaclasses as iterables +class Meta(type): + def __iter__(self): + return iter((1, 2, 3)) + +class SomeClass(object): + __metaclass__ = Meta + + +for i in SomeClass: + print i +for i in SomeClass(): # [not-an-iterable] + print i diff --git a/pylint/test/functional/iterable_context_py2.rc b/pylint/test/functional/iterable_context_py2.rc new file mode 100644 index 0000000..61e01ea --- /dev/null +++ b/pylint/test/functional/iterable_context_py2.rc @@ -0,0 +1,3 @@ +[testoptions] +max_pyver=2.7 + diff --git a/pylint/test/functional/iterable_context_py2.txt b/pylint/test/functional/iterable_context_py2.txt new file mode 100644 index 0000000..8de579a --- /dev/null +++ b/pylint/test/functional/iterable_context_py2.txt @@ -0,0 +1 @@ +not-an-iterable:17::Non-iterable value SomeClass() is used in an iterating context diff --git a/pylint/test/functional/iterable_context_py3.py b/pylint/test/functional/iterable_context_py3.py new file mode 100644 index 0000000..cb2a505 --- /dev/null +++ b/pylint/test/functional/iterable_context_py3.py @@ -0,0 +1,18 @@ +""" +Checks that iterable metaclasses are recognized by pylint. +""" +# pylint: disable=missing-docstring,too-few-public-methods,no-init,no-self-use,unused-argument,bad-mcs-method-argument + +# metaclasses as iterables +class Meta(type): + def __iter__(self): + return iter((1, 2, 3)) + +class SomeClass(metaclass=Meta): + pass + + +for i in SomeClass: + print(i) +for i in SomeClass(): # [not-an-iterable] + print(i) diff --git a/pylint/test/functional/iterable_context_py3.rc b/pylint/test/functional/iterable_context_py3.rc new file mode 100644 index 0000000..9bf6df0 --- /dev/null +++ b/pylint/test/functional/iterable_context_py3.rc @@ -0,0 +1,3 @@ +[testoptions] +min_pyver=3.0 + diff --git a/pylint/test/functional/iterable_context_py3.txt b/pylint/test/functional/iterable_context_py3.txt new file mode 100644 index 0000000..8de579a --- /dev/null +++ b/pylint/test/functional/iterable_context_py3.txt @@ -0,0 +1 @@ +not-an-iterable:17::Non-iterable value SomeClass() is used in an iterating context diff --git a/pylint/test/functional/mapping_context.py b/pylint/test/functional/mapping_context.py new file mode 100644 index 0000000..cfab8dc --- /dev/null +++ b/pylint/test/functional/mapping_context.py @@ -0,0 +1,59 @@ +""" +Checks that only valid values are used in a mapping context. +""" +# pylint: disable=missing-docstring,invalid-name,too-few-public-methods,no-self-use,import-error +from __future__ import print_function + + +def test(**kwargs): + print(kwargs) + + +# dictionary value/comprehension +dict_value = dict(a=1, b=2, c=3) +dict_comp = {chr(x): x for x in range(256)} +test(**dict_value) +test(**dict_comp) + + +# in order to be used in kwargs custom mapping class should define +# __iter__(), __getitem__(key) and keys(). +class CustomMapping(object): + def __init__(self): + self.data = dict(a=1, b=2, c=3, d=4, e=5) + + def __getitem__(self, key): + return self.data[key] + + def keys(self): + return self.data.keys() + +test(**CustomMapping()) +test(**CustomMapping) # [not-a-mapping] + +class NotMapping(object): + pass + +test(**NotMapping()) # [not-a-mapping] + +# skip checks if statement is inside mixin class +class SomeMixin(object): + kwargs = None + + def get_kwargs(self): + return self.kwargs + + def run(self, **kwargs): + print(kwargs) + + def dispatch(self): + kws = self.get_kwargs() + self.run(**kws) + +# skip uninferable instances +from some_missing_module import Mapping + +class MyClass(Mapping): + pass + +test(**MyClass()) diff --git a/pylint/test/functional/mapping_context.txt b/pylint/test/functional/mapping_context.txt new file mode 100644 index 0000000..201da1a --- /dev/null +++ b/pylint/test/functional/mapping_context.txt @@ -0,0 +1,2 @@ +not-a-mapping:32::Non-mapping value CustomMapping is used in a mapping context +not-a-mapping:37::Non-mapping value NotMapping() is used in a mapping context diff --git a/pylint/test/functional/mapping_context_py2.py b/pylint/test/functional/mapping_context_py2.py new file mode 100644 index 0000000..afe4400 --- /dev/null +++ b/pylint/test/functional/mapping_context_py2.py @@ -0,0 +1,19 @@ +# pylint: disable=missing-docstring,invalid-name,too-few-public-methods +from __future__ import print_function + + +def test(**kwargs): + print(kwargs) + +# metaclasses as mappings +class Meta(type): + def __getitem__(self, key): + return ord(key) + def keys(self): + return ['a', 'b', 'c'] + +class SomeClass(object): + __metaclass__ = Meta + +test(**SomeClass) +test(**SomeClass()) # [not-a-mapping] diff --git a/pylint/test/functional/mapping_context_py2.rc b/pylint/test/functional/mapping_context_py2.rc new file mode 100644 index 0000000..61e01ea --- /dev/null +++ b/pylint/test/functional/mapping_context_py2.rc @@ -0,0 +1,3 @@ +[testoptions] +max_pyver=2.7 + diff --git a/pylint/test/functional/mapping_context_py2.txt b/pylint/test/functional/mapping_context_py2.txt new file mode 100644 index 0000000..59cca6c --- /dev/null +++ b/pylint/test/functional/mapping_context_py2.txt @@ -0,0 +1 @@ +not-a-mapping:19::Non-mapping value SomeClass() is used in a mapping context diff --git a/pylint/test/functional/mapping_context_py3.py b/pylint/test/functional/mapping_context_py3.py new file mode 100644 index 0000000..042d4d0 --- /dev/null +++ b/pylint/test/functional/mapping_context_py3.py @@ -0,0 +1,19 @@ +# pylint: disable=missing-docstring,invalid-name,too-few-public-methods,no-self-use +from __future__ import print_function + +def test(**kwargs): + print(kwargs) + +# metaclasses as mappings +class Meta(type): + def __getitem__(cls, key): + return ord(key) + + def keys(cls): + return ['a', 'b', 'c'] + +class SomeClass(metaclass=Meta): + pass + +test(**SomeClass) +test(**SomeClass()) # [not-a-mapping] diff --git a/pylint/test/functional/mapping_context_py3.rc b/pylint/test/functional/mapping_context_py3.rc new file mode 100644 index 0000000..9bf6df0 --- /dev/null +++ b/pylint/test/functional/mapping_context_py3.rc @@ -0,0 +1,3 @@ +[testoptions] +min_pyver=3.0 + diff --git a/pylint/test/functional/mapping_context_py3.txt b/pylint/test/functional/mapping_context_py3.txt new file mode 100644 index 0000000..59cca6c --- /dev/null +++ b/pylint/test/functional/mapping_context_py3.txt @@ -0,0 +1 @@ +not-a-mapping:19::Non-mapping value SomeClass() is used in a mapping context diff --git a/pylint/test/functional/member_checks.py b/pylint/test/functional/member_checks.py index 8838fae..50a0d97 100644 --- a/pylint/test/functional/member_checks.py +++ b/pylint/test/functional/member_checks.py @@ -173,3 +173,9 @@ def no_conjugate_member(magic_flag): if isinstance(something, float): return something return something.conjugate() + + +class NoDunderNameInInstance(object): + """Emit a warning when accessing __name__ from an instance.""" + def __init__(self): + self.var = self.__name__ # [no-member] diff --git a/pylint/test/functional/member_checks.txt b/pylint/test/functional/member_checks.txt index 3d50a25..6e527bc 100644 --- a/pylint/test/functional/member_checks.txt +++ b/pylint/test/functional/member_checks.txt @@ -15,3 +15,4 @@ no-member:120::Class 'Client' has no 'missing' member:INFERENCE no-member:144::Class 'Client' has no 'ala' member:INFERENCE no-member:145::Class 'dict' has no 'bala' member:INFERENCE no-member:146::Class 'str' has no 'portocala' member:INFERENCE +no-member:181:NoDunderNameInInstance.__init__:Instance of 'NoDunderNameInInstance' has no '__name__' member:INFERENCE
\ No newline at end of file diff --git a/pylint/test/functional/membership_protocol.py b/pylint/test/functional/membership_protocol.py new file mode 100644 index 0000000..7b3a46f --- /dev/null +++ b/pylint/test/functional/membership_protocol.py @@ -0,0 +1,85 @@ +# pylint: disable=missing-docstring,pointless-statement,expression-not-assigned,too-few-public-methods,import-error,no-init + +# standard types +1 in [1, 2, 3] +1 in {'a': 1, 'b': 2} +1 in {1, 2, 3} +1 in (1, 2, 3) +1 in "123" +1 in u"123" +1 in bytearray(b"123") +1 in frozenset([1, 2, 3]) + +# comprehensions +1 in [x ** 2 % 10 for x in range(10)] +1 in {x ** 2 % 10 for x in range(10)} +1 in {x: x ** 2 % 10 for x in range(10)} + +# iterators +1 in iter([1, 2, 3]) + +# generator +def count(upto=float("inf")): + i = 0 + while True: + if i > upto: + break + yield i + i += 1 + +10 in count(upto=10) + +# custom instance +class UniversalContainer(object): + def __contains__(self, key): + return True + +42 in UniversalContainer() + +# custom iterable +class CustomIterable(object): + def __iter__(self): + return iter((1, 2, 3)) +3 in CustomIterable() + +# old-style iterable +class OldStyleIterable(object): + def __getitem__(self, key): + if key < 10: + return 2 ** key + else: + raise IndexError("bad index") +64 in OldStyleIterable() + +# do not emit warning if class has unknown bases +from some_missing_module import ImportedClass + +class MaybeIterable(ImportedClass): + pass + +10 in MaybeIterable() + +# do not emit warning inside mixins +class UsefulMixin(object): + stuff = None + + def get_stuff(self): + return self.stuff + + def act(self, thing): + stuff = self.get_stuff() + if thing in stuff: + pass + +# error cases +42 in 42 # [unsupported-membership-test] +42 not in None # [unsupported-membership-test] +42 in 8.5 # [unsupported-membership-test] + +class EmptyClass(object): + pass + +42 not in EmptyClass() # [unsupported-membership-test] +42 in EmptyClass # [unsupported-membership-test] +42 not in count # [unsupported-membership-test] +42 in range # [unsupported-membership-test] diff --git a/pylint/test/functional/membership_protocol.txt b/pylint/test/functional/membership_protocol.txt new file mode 100644 index 0000000..6e9bd8e --- /dev/null +++ b/pylint/test/functional/membership_protocol.txt @@ -0,0 +1,7 @@ +unsupported-membership-test:75::Value '42' doesn't support membership test +unsupported-membership-test:76::Value 'None' doesn't support membership test +unsupported-membership-test:77::Value '8.5' doesn't support membership test +unsupported-membership-test:82::Value 'EmptyClass()' doesn't support membership test +unsupported-membership-test:83::Value 'EmptyClass' doesn't support membership test +unsupported-membership-test:84::Value 'count' doesn't support membership test +unsupported-membership-test:85::Value 'range' doesn't support membership test diff --git a/pylint/test/functional/membership_protocol_py2.py b/pylint/test/functional/membership_protocol_py2.py new file mode 100644 index 0000000..1a01637 --- /dev/null +++ b/pylint/test/functional/membership_protocol_py2.py @@ -0,0 +1,36 @@ +# pylint: disable=missing-docstring,too-few-public-methods,no-init,no-self-use,unused-argument,pointless-statement,expression-not-assigned,undefined-variable + +# metaclasses that support membership test protocol +class MetaIterable(type): + def __iter__(cls): + return iter((1, 2, 3)) + +class MetaOldIterable(type): + def __getitem__(cls, key): + if key < 10: + return key ** 2 + else: + raise IndexError("bad index") + +class MetaContainer(type): + def __contains__(cls, key): + return False + + +class IterableClass(object): + __metaclass__ = MetaIterable + +class OldIterableClass(object): + __metaclass__ = MetaOldIterable + +class ContainerClass(object): + __metaclass__ = MetaContainer + + +def test(): + 1 in IterableClass + 1 in OldIterableClass + 1 in ContainerClass + 1 in IterableClass() # [unsupported-membership-test] + 1 in OldIterableClass() # [unsupported-membership-test] + 1 in ContainerClass() # [unsupported-membership-test] diff --git a/pylint/test/functional/membership_protocol_py2.rc b/pylint/test/functional/membership_protocol_py2.rc new file mode 100644 index 0000000..c78f32f --- /dev/null +++ b/pylint/test/functional/membership_protocol_py2.rc @@ -0,0 +1,3 @@ +[testoptions] +max_pyver=3.0 + diff --git a/pylint/test/functional/membership_protocol_py2.txt b/pylint/test/functional/membership_protocol_py2.txt new file mode 100644 index 0000000..4ba7575 --- /dev/null +++ b/pylint/test/functional/membership_protocol_py2.txt @@ -0,0 +1,3 @@ +unsupported-membership-test:34:test:Value 'IterableClass()' doesn't support membership test +unsupported-membership-test:35:test:Value 'OldIterableClass()' doesn't support membership test +unsupported-membership-test:36:test:Value 'ContainerClass()' doesn't support membership test diff --git a/pylint/test/functional/membership_protocol_py3.py b/pylint/test/functional/membership_protocol_py3.py new file mode 100644 index 0000000..6a77f20 --- /dev/null +++ b/pylint/test/functional/membership_protocol_py3.py @@ -0,0 +1,36 @@ +# pylint: disable=missing-docstring,too-few-public-methods,no-init,no-self-use,unused-argument,pointless-statement,expression-not-assigned + +# metaclasses that support membership test protocol +class MetaIterable(type): + def __iter__(cls): + return iter((1, 2, 3)) + +class MetaOldIterable(type): + def __getitem__(cls, key): + if key < 10: + return key ** 2 + else: + raise IndexError("bad index") + +class MetaContainer(type): + def __contains__(cls, key): + return False + + +class IterableClass(metaclass=MetaOldIterable): + pass + +class OldIterableClass(metaclass=MetaOldIterable): + pass + +class ContainerClass(metaclass=MetaContainer): + pass + + +def test(): + 1 in IterableClass + 1 in OldIterableClass + 1 in ContainerClass + 1 in IterableClass() # [unsupported-membership-test] + 1 in OldIterableClass() # [unsupported-membership-test] + 1 in ContainerClass() # [unsupported-membership-test] diff --git a/pylint/test/functional/membership_protocol_py3.rc b/pylint/test/functional/membership_protocol_py3.rc new file mode 100644 index 0000000..9bf6df0 --- /dev/null +++ b/pylint/test/functional/membership_protocol_py3.rc @@ -0,0 +1,3 @@ +[testoptions] +min_pyver=3.0 + diff --git a/pylint/test/functional/membership_protocol_py3.txt b/pylint/test/functional/membership_protocol_py3.txt new file mode 100644 index 0000000..4ba7575 --- /dev/null +++ b/pylint/test/functional/membership_protocol_py3.txt @@ -0,0 +1,3 @@ +unsupported-membership-test:34:test:Value 'IterableClass()' doesn't support membership test +unsupported-membership-test:35:test:Value 'OldIterableClass()' doesn't support membership test +unsupported-membership-test:36:test:Value 'ContainerClass()' doesn't support membership test diff --git a/pylint/test/functional/no_self_use_py3.py b/pylint/test/functional/no_self_use_py3.py new file mode 100644 index 0000000..f401508 --- /dev/null +++ b/pylint/test/functional/no_self_use_py3.py @@ -0,0 +1,12 @@ +# pylint: disable=missing-docstring,no-init,unused-argument,invalid-name,too-few-public-methods + +class A: + def __init__(self): + self.store = {} + + def get(self, key, default=None): + return self.store.get(key, default) + +class B(A): + def get_memo(self, obj): + return super().get(obj) diff --git a/pylint/test/functional/no_self_use_py3.rc b/pylint/test/functional/no_self_use_py3.rc new file mode 100644 index 0000000..a2ab06c --- /dev/null +++ b/pylint/test/functional/no_self_use_py3.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.0
\ No newline at end of file diff --git a/pylint/test/functional/no_self_use_py3.txt b/pylint/test/functional/no_self_use_py3.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/pylint/test/functional/no_self_use_py3.txt @@ -0,0 +1 @@ + diff --git a/pylint/test/functional/non_iterator_returned.py b/pylint/test/functional/non_iterator_returned.py index 845500b..804ceee 100644 --- a/pylint/test/functional/non_iterator_returned.py +++ b/pylint/test/functional/non_iterator_returned.py @@ -1,8 +1,8 @@ """Check non-iterators returned by __iter__ """ -# pylint: disable=too-few-public-methods +# pylint: disable=too-few-public-methods, missing-docstring, no-self-use -__revision__ = 0 +import six class FirstGoodIterator(object): """ yields in iterator. """ @@ -17,11 +17,11 @@ class SecondGoodIterator(object): def __iter__(self): return self - def __next__(self): # pylint: disable=no-self-use + def __next__(self): """ Infinite iterator, but still an iterator """ return 1 - def next(self): # pylint: disable=no-self-use + def next(self): """Same as __next__, but for Python 2.""" return 1 @@ -37,6 +37,26 @@ class FourthGoodIterator(object): def __iter__(self): return iter(range(10)) + +class IteratorMetaclass(type): + def __next__(cls): + return 1 + + def next(cls): + return 2 + + +@six.add_metaclass(IteratorMetaclass) +class IteratorClass(object): + """Iterable through the metaclass.""" + + +class FifthGoodIterator(object): + """__iter__ returns a class which uses an iterator-metaclass.""" + def __iter__(self): + return IteratorClass + + class FirstBadIterator(object): """ __iter__ returns a list """ @@ -54,3 +74,17 @@ class ThirdBadIterator(object): def __iter__(self): # [non-iterator-returned] return SecondBadIterator() + +class FourthBadIterator(object): + """__iter__ returns a class.""" + + def __iter__(self): # [non-iterator-returned] + return ThirdBadIterator + +class FifthBadIterator(object): + """All branches should return an iterator.""" + + def __iter__(self): # [non-iterator-returned] + if self: + return 1 + return SecondGoodIterator() diff --git a/pylint/test/functional/non_iterator_returned.txt b/pylint/test/functional/non_iterator_returned.txt index f2881eb..fe3db10 100644 --- a/pylint/test/functional/non_iterator_returned.txt +++ b/pylint/test/functional/non_iterator_returned.txt @@ -1,3 +1,5 @@ -non-iterator-returned:43:FirstBadIterator.__iter__:__iter__ returns non-iterator -non-iterator-returned:49:SecondBadIterator.__iter__:__iter__ returns non-iterator -non-iterator-returned:55:ThirdBadIterator.__iter__:__iter__ returns non-iterator
\ No newline at end of file +non-iterator-returned:63:FirstBadIterator.__iter__:__iter__ returns non-iterator +non-iterator-returned:69:SecondBadIterator.__iter__:__iter__ returns non-iterator +non-iterator-returned:75:ThirdBadIterator.__iter__:__iter__ returns non-iterator +non-iterator-returned:81:FourthBadIterator.__iter__:__iter__ returns non-iterator +non-iterator-returned:87:FifthBadIterator.__iter__:__iter__ returns non-iterator
\ No newline at end of file diff --git a/pylint/test/functional/unbalanced_tuple_unpacking.py b/pylint/test/functional/unbalanced_tuple_unpacking.py index 02722c7..bd21a05 100644 --- a/pylint/test/functional/unbalanced_tuple_unpacking.py +++ b/pylint/test/functional/unbalanced_tuple_unpacking.py @@ -97,3 +97,10 @@ def test_decimal(): dec = Decimal(2) first, second, third = dec.as_tuple() return first, second, third + + +def test_issue_559(): + """Test that we don't have a false positive wrt to issue #559.""" + from ctypes import c_int + root_x, root_y, win_x, win_y = [c_int()] * 4 + return root_x, root_y, win_x, win_y diff --git a/pylint/test/functional/yield_from_iterable_py33.py b/pylint/test/functional/yield_from_iterable_py33.py new file mode 100644 index 0000000..7803936 --- /dev/null +++ b/pylint/test/functional/yield_from_iterable_py33.py @@ -0,0 +1,7 @@ +""" +Check that `yield from`-statement takes an iterable. +""" +# pylint: disable=missing-docstring + +def to_ten(): + yield from 10 # [not-an-iterable] diff --git a/pylint/test/functional/yield_from_iterable_py33.rc b/pylint/test/functional/yield_from_iterable_py33.rc new file mode 100644 index 0000000..3330edd --- /dev/null +++ b/pylint/test/functional/yield_from_iterable_py33.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.3
\ No newline at end of file diff --git a/pylint/test/functional/yield_from_iterable_py33.txt b/pylint/test/functional/yield_from_iterable_py33.txt new file mode 100644 index 0000000..906ee93 --- /dev/null +++ b/pylint/test/functional/yield_from_iterable_py33.txt @@ -0,0 +1 @@ +not-an-iterable:7:to_ten:Non-iterable value 10 is used in an iterating context
diff --git a/pylint/test/regrtest_data/html_crash_420.py b/pylint/test/regrtest_data/html_crash_420.py new file mode 100644 index 0000000..a0edbb5 --- /dev/null +++ b/pylint/test/regrtest_data/html_crash_420.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*-
+tag2struct = {u"#": "R_HEADER"
+ ,u"£": "RDR_HEADER"
+ ,u"µ": "RDR_DRAFT"
+ }
\ No newline at end of file diff --git a/pylint/test/test_self.py b/pylint/test/test_self.py index ba4cdab..324cc16 100644 --- a/pylint/test/test_self.py +++ b/pylint/test/test_self.py @@ -145,6 +145,12 @@ class RunTC(unittest.TestCase): messages = parser.get('MESSAGES CONTROL', 'disable').split(",") self.assertIn('suppressed-message', messages) + def test_generate_rcfile_no_obsolete_methods(self): + out = six.StringIO() + self._run_pylint(["--generate-rcfile"], out=out) + output = out.getvalue() + self.assertNotIn("profile", output) + def _test_deprecated_options(self, option, expected): out = six.StringIO() self._run_pylint([option, "--rcfile=", "pylint.config"], out=out) @@ -259,6 +265,11 @@ class RunTC(unittest.TestCase): self._test_output([module, "--disable=all", "--enable=all", "-rn"], expected_output=expected) + def test_html_crash_report(self): + out = six.StringIO() + module = join(HERE, 'regrtest_data', 'html_crash_420.py') + self._runtest([module], code=16, reporter=HTMLReporter(out)) + if __name__ == '__main__': unittest.main() diff --git a/pylint/test/unittest_checker_base.py b/pylint/test/unittest_checker_base.py index ec3ae2b..9901765 100644 --- a/pylint/test/unittest_checker_base.py +++ b/pylint/test/unittest_checker_base.py @@ -1,7 +1,6 @@ """Unittest for the base checker.""" import re -import sys import unittest import astroid diff --git a/pylint/test/unittest_checker_typecheck.py b/pylint/test/unittest_checker_typecheck.py index b7135ea..0aaa8a5 100644 --- a/pylint/test/unittest_checker_typecheck.py +++ b/pylint/test/unittest_checker_typecheck.py @@ -5,6 +5,7 @@ from astroid import test_utils from pylint.checkers import typecheck from pylint.testutils import CheckerTestCase, Message, set_config + class TypeCheckerTest(CheckerTestCase): "Tests for pylint.checkers.typecheck" CHECKER_CLASS = typecheck.TypeChecker diff --git a/pylint/testutils.py b/pylint/testutils.py index adc4e0c..ecb26ce 100644 --- a/pylint/testutils.py +++ b/pylint/testutils.py @@ -272,6 +272,7 @@ class LintTestUsingModule(unittest.TestCase): def test_functionality(self): tocheck = [self.package+'.'+self.module] + # pylint: disable=not-an-iterable; can't handle boolean checks for now if self.depends: tocheck += [self.package+'.%s' % name.replace('.py', '') for name, _ in self.depends] @@ -317,6 +318,7 @@ class LintTestUsingFile(LintTestUsingModule): if not isdir(importable): importable += '.py' tocheck = [importable] + # pylint: disable=not-an-iterable; can't handle boolean checks for now if self.depends: tocheck += [join(self.INPUT_DIR, name) for name, _ in self.depends] self._test(tocheck) diff --git a/pylint/utils.py b/pylint/utils.py index f303411..2f8de80 100644 --- a/pylint/utils.py +++ b/pylint/utils.py @@ -142,9 +142,8 @@ def category_id(cid): return MSG_TYPES_LONG.get(cid) -def _decoding_readline(stream, module): - return lambda: stream.readline().decode(module.file_encoding, - 'replace') +def _decoding_readline(stream, encoding): + return lambda: stream.readline().decode(encoding, 'replace') def tokenize_module(module): @@ -152,7 +151,8 @@ def tokenize_module(module): readline = stream.readline if sys.version_info < (3, 0): if module.file_encoding is not None: - readline = _decoding_readline(stream, module) + readline = _decoding_readline(stream, module.file_encoding) + return list(tokenize.generate_tokens(readline)) return list(tokenize.tokenize(readline)) @@ -997,7 +997,8 @@ def deprecated_option(shortname=None, opt_type=None, help_msg=None): 'hide': True, 'type': opt_type, 'action': 'callback', - 'callback': _warn_deprecated + 'callback': _warn_deprecated, + 'deprecated': True } if shortname: option['shortname'] = shortname |