Skip to content

Commit

Permalink
Add documentation on plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
erik committed Jan 2, 2019
1 parent 7f7ae92 commit 05ad27e
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 9 deletions.
94 changes: 86 additions & 8 deletions docs/plugins.rst
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
Writing Your Own Lint Rules
===========================
Writing Plugins
===============

Squabble supports loading rule definitions from arbitrary plugin
directories. Every Python file from directories in the configuration's
``"plugin"`` section will be loaded and any classes that inherit from
:func:`squabble.rules.BaseRule` will be registered and available for use.
Squabble supports loading rule definitions from directories specified in the
``.squabblerc`` configuration file.

Every Python file in the list of directories will be loaded and any classes
that inherit from :class:`squabble.rules.BaseRule` will be registered and
available for use.


Configuration
-------------

::

{
"plugins": [
"/path/to/plugins/",
...
]
}

Concepts
--------

Rules
~~~~~

Rules are classes which inherit from :func:`squabble.rules.BaseRule` and
Rules are classes which inherit from :class:`squabble.rules.BaseRule` and
are responsible for checking the abstract syntax tree of a SQL file.

At a minimum, each rule will define ``def enable(self, root_context, config)``,
which is responsible for doing any initialization when the rule is enabled.

Rules register callback functions to trigger when certain nodes of the abstract
syntax tree are hit. Rules will report messages_ to indicate any issues
discovered.

For example ::

class MyRule(squabble.rules.BaseRule):
Expand All @@ -30,12 +49,71 @@ Could be configured with this ``.squabblerc`` ::

``enable()`` would be passed ``config={"foo": "bar"}``.

.. _messages:

Messages
~~~~~~~~

Messages inherit from :class:`squabble.message.Message`, and are used to define
specific kinds of lint exceptions a rule can uncover.

At a bare minimum, each message class needs a ``TEMPLATE`` class variable,
which is used when formatting the message to be printed on the command line.

For example ::

class BadlyNamedColumn(squabble.message.Message):
"""
Here are some more details about ``BadlyNamedColumn``.

This is where you would explain why this message is relevant,
how to resolve it, etc.
"""

TEMPLATE = 'tried to {foo} when you should have done {bar}!'

>>> msg = MyMessage(foo='abc', bar='xyz')
>>> msg.format()
'tried to abc when you should have done xyz'
>>> msg.explain()
'Here are some more details ...

Messages may also define a ``CODE`` class variable, which is an integer which
uniquely identifies the class. If not explicitly specified, one will be
assigned, starting at ``9000``. These can be used by the ``--explain`` command
line flag ::

$ squabble --explain 9001
BadlyNamedColumn
Here are some more details about ``BadlyNamedColumn``.

...

Lint context
~~~~~~~~~~~~

Each instance of :class:`squabble.lint.LintContext` holds the callback
functions that have been registered at or below a particular node in the
abstract syntax tree, as well as being responsible for reporting any messages
that get raised.

When the ``enable()`` function for a class inheriting from
:class:`squabble.rules.BaseRule` is called, it will be passed a context
pointing to the root node of the syntax tree. Every callback function will be
passed a context scoped to the node that triggered the callback.

::

def enable(root_context, _config):
root_context.register('CreateStmt', create_table_callback)

def create_table_callback(child_context, node):
# register a callback that is only scoped to this ``node``
child_context.register('ColumnDef', column_def_callback):

def column_def_callback(child_context, node):
...

Details
-------

Expand Down Expand Up @@ -63,7 +141,7 @@ bindings <https://github.com/lelit/pglast/tree/master/pglast/enums>`__.
Example Rule
------------

.. code:: python
.. code-block:: python
import squabble.rule
from squabble.message import Message
Expand Down
17 changes: 17 additions & 0 deletions squabble/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,23 @@ class LintContext:
"""
Contains the node tag callback hooks enabled at or below the `parent_node`
passed to the call to `traverse`.
>>> import pglast
>>> ast = pglast.Node(pglast.parse_sql('''
... CREATE TABLE foo (id INTEGER PRIMARY KEY);
... '''))
>>> ctx = LintContext(session=...)
>>>
>>> def create_stmt(child_ctx, node):
... print('create stmt')
... child_ctx.register('ColumnDef', lambda _c, _n: print('from child'))
...
>>> ctx.register('CreateStmt', create_stmt)
>>> ctx.register('ColumnDef', lambda _c, _n: print('from root'))
>>> ctx.traverse(ast)
create stmt
from child
from root
"""
def __init__(self, session):
self._hooks = {}
Expand Down
10 changes: 9 additions & 1 deletion squabble/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ class Registry:

@classmethod
def register(cls, msg):
"""
Add ``msg`` to the registry, and assign a ``CODE`` value if not
explicitly specified.
"""
if not hasattr(msg, 'CODE'):
setattr(msg, 'CODE', cls._next_code())
logger.info('assigning code %s to %s', msg.CODE, msg)
Expand All @@ -49,6 +53,10 @@ def register(cls, msg):

@classmethod
def by_code(cls, code):
"""
Return the :class:`squabble.message.Message` class identified by
``code``, raising a :class:`KeyError` if it doesn't exist.
"""
return cls._MAP[code]

@classmethod
Expand All @@ -68,7 +76,7 @@ class Message:
Messages may also have a ``CODE`` class member, which is used to
identify the message. The actual value doesn't matter much, as
long as it is unique among all the loaded ``Message``s. If no
long as it is unique among all the loaded ``Message`` s. If no
``CODE`` is defined, one will be assigned.
>>> class TooManyColumns(Message):
Expand Down

0 comments on commit 05ad27e

Please sign in to comment.