From 55f062ea01923673f90d6921ef4e88ef04507dcd Mon Sep 17 00:00:00 2001 From: amdev Date: Thu, 27 Feb 2014 14:25:33 +0530 Subject: Initial draft of plugin how-to. --- doc/index.rst | 7 +--- doc/plugins.rst | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 doc/plugins.rst (limited to 'doc') diff --git a/doc/index.rst b/doc/index.rst index 790ff93..bc6f9d8 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -15,6 +15,7 @@ https://bitbucket.org/logilab/pylint features extend ide-integration + plugins contribute tutorial @@ -29,12 +30,6 @@ Content wanted It would be nice to include in the documentation the following information: -- pylint plugins (how to write one, how to install a 3rd party plugin, how to - configure Pylint to run it) - - pylint brain project : what it is, how to install it... Please send your pull requests via bitbucket if you can help with the above. - - - diff --git a/doc/plugins.rst b/doc/plugins.rst new file mode 100644 index 0000000..052c13f --- /dev/null +++ b/doc/plugins.rst @@ -0,0 +1,122 @@ +.. -*- coding: utf-8 -*- + +======= +Plugins +======= + +Why write a plugin? +------------------- + +Pylint is a static analysis tool and Python is a dynamically typed language. +So there will be cases where Pylint cannot analyze files properly (this problem +can happen in statically typed languages also if reflection or dynamic +evaluation is used). Plugin is a way to tell Pylint how to handle such cases, +since only the user would know what needs to be done. + +Example +------- + +Let us run Pylint on a module from the Python source: `warnings.py`_ and see what happens: + +.. sourcecode:: bash + + amitdev$ pylint -E Lib/warnings.py + E:297,36: Instance of 'WarningMessage' has no 'message' member (no-member) + E:298,36: Instance of 'WarningMessage' has no 'filename' member (no-member) + E:298,51: Instance of 'WarningMessage' has no 'lineno' member (no-member) + E:298,64: Instance of 'WarningMessage' has no 'line' member (no-member) + + +Did we catch a genuine error? Let's open the code and look at ``WarningMessage`` class: + +.. sourcecode:: python + + class WarningMessage(object): + + """Holds the result of a single showwarning() call.""" + + _WARNING_DETAILS = ("message", "category", "filename", "lineno", "file", + "line") + + def __init__(self, message, category, filename, lineno, file=None, + line=None): + local_values = locals() + for attr in self._WARNING_DETAILS: + setattr(self, attr, local_values[attr]) + self._category_name = category.__name__ if category else None + + def __str__(self): + ... + +Ah, the fields (``message``, ``category`` etc) are not defined statically on the class. +Instead they are added using ``setattr``. Pylint would have a tough time figuring +this out. + +Enter Plugin +------------ + +We can write a plugin to tell Pylint about how to analyze this properly. A +plugin is a module which should have a function ``register`` and takes the +`lint`_ module as input. So a basic hello-world plugin can be implemented as: + +.. sourcecode:: python + + # Inside hello_plugin.py + def register(linter): + print 'Hello world' + +We can run this plugin by placing this module in the PYTHONPATH and invoking as: + +.. sourcecode:: bash + + amitdev$ pylint -E --load-plugins hello_plugin foo.py + Hello world + +Back to our example: one way to fix that would be to transform the ``WarningMessage`` class +and set the attributes using a plugin so that Pylint can see them. This can be done by +registering a transform function. We can transform any node in the parsed AST like +Module, Class, Function etc. In our case we need to transform a class. It can be done so: + +.. sourcecode:: python + + from astroid import MANAGER + from astroid import scoped_nodes + + def register(linter): + pass + + def transform(cls): + if cls.name == 'WarningMessage': + import warnings + for f in warnings.WarningMessage._WARNING_DETAILS: + cls.locals[f] = [scoped_nodes.Class(f, None)] + + MANAGER.register_transform(scoped_nodes.Class, transform) + +Let's go through the plugin. First, we need to register a class transform, which +is done via the ``register_transform`` function in ``MANAGER``. It takes the node +type and function as parameters. We need to change a class, so we use ``scoped_nodes.Class``. +We also pass a ``transform`` function which does the actual transformation. + +``transform`` function is simple as well. If the class is ``WarningMessage`` then we +add the attributes to its locals (we are not bothered about type of attributes, so setting +them as class will do. But we could set them to any type we want). That's it. + +Note: We don't need to do anything in the ``register`` function of the plugin since we +are not modifying anything in the linter itself. + +Lets run Pylint with this plugin and see: + +.. sourcecode:: bash + + amitdev$ pylint -E --load-plugins warning_plugin Lib/warnings.py + amitdev$ + +All the false positives associated with ``WarningMessage`` are now gone. This is just +an example, any code transformation can be done by plugins. See `nodes`_ and `scoped_nodes`_ +for details about all node types that can be transformed. + +.. _`warnings.py`: http://hg.python.org/cpython/file/2.7/Lib/warnings.py +.. _`scoped_nodes`: https://bitbucket.org/logilab/astroid/src/64026ffc0d94fe09e4bdc2bf5efaab29444645e7/scoped_nodes.py?at=default +.. _`nodes`: https://bitbucket.org/logilab/astroid/src/64026ffc0d94fe09e4bdc2bf5efaab29444645e7/nodes.py?at=default +.. _`lint`: https://bitbucket.org/logilab/pylint/src/f2acea7b640def0237513f66e3de5fa3de73f2de/lint.py?at=default \ No newline at end of file -- cgit v1.2.1