Source code for squabble.message

import inspect
import logging

from squabble import SquabbleException


logger = logging.getLogger(__name__)


[docs]class DuplicateMessageCodeException(SquabbleException): def __init__(self, dupe): original = Registry.by_code(dupe.CODE) message = 'Message %s has the same code as %s' % (dupe, original) super().__init__(message)
[docs]class Registry: """ Singleton which maps message code values to classes. >>> class MyMessage(Message): ... '''My example message.''' ... TEMPLATE = '...' ... CODE = 5678 >>> cls = Registry.by_code(5678) >>> cls.explain() 'My example message.' >>> cls is MyMessage True Duplicate codes are not allowed, and will throw an exception. >>> class MyDuplicateMessage(Message): ... CODE = 5678 Traceback (most recent call last): ... squabble.message.DuplicateMessageCodeException: ... """ _MAP = {} _CODE_COUNTER = 9000
[docs] @classmethod def register(cls, msg): """ Add ``msg`` to the registry, and assign a ``CODE`` value if not explicitly specified. """ if msg.CODE is None: setattr(msg, 'CODE', cls._next_code()) logger.info('assigning code %s to %s', msg.CODE, msg) # Don't allow duplicates if msg.CODE in cls._MAP: raise DuplicateMessageCodeException(msg) cls._MAP[msg.CODE] = msg
[docs] @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 def _next_code(cls): cls._CODE_COUNTER += 1 return cls._CODE_COUNTER
[docs]class Message: """ Messages represent specific issues identified by a lint rule. Each class that inherits from ``Message`` should have a docstring which explains the reasoning and context of the message, as well as a class member variable named ``TEMPLATE``, which is used to display a brief explanation on the command line. 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 ``CODE`` is defined, one will be assigned. >>> class TooManyColumns(Message): ... ''' ... This may indicate poor design, consider multiple tables instead. ... ''' ... TEMPLATE = 'table "{table}" has > {limit} columns' ... CODE = 1234 >>> message = TooManyColumns(table='foo', limit=30) >>> message.format() 'table "foo" has > 30 columns' >>> message.explain() 'This may indicate poor design, consider multiple tables instead.' """ TEMPLATE = None CODE = None def __init__(self, **kwargs): self.kwargs = kwargs def __init_subclass__(cls, **kwargs): """Assign unique (locally, not globally) message codes.""" super().__init_subclass__(**kwargs) Registry.register(cls)
[docs] def format(self): return self.TEMPLATE.format(**self.kwargs)
[docs] @classmethod def explain(cls): """ Provide more context around this message. The purpose of this function is to explain to users _why_ the message was raised, and what they can do to resolve the issue. The base implementation will simply return the docstring for the class, but this can be overridden if more specialized behavior is necessary. >>> class NoDocString(Message): pass >>> NoDocString().explain() is None True """ if not cls.__doc__: return None # Remove the leading indentation on the docstring return inspect.cleandoc(cls.__doc__)
[docs] def asdict(self): """ Return dictionary representation of message, for formatting. >>> class SummaryMessage(Message): ... CODE = 90291 ... TEMPLATE = 'everything is {status}' ... >>> msg = SummaryMessage(status='wrong') >>> msg.asdict() == { ... 'message_id': 'SummaryMessage', ... 'message_text': 'everything is wrong', ... 'message_template': SummaryMessage.TEMPLATE, ... 'message_params': {'status': 'wrong'}, ... 'message_code': 90291 ... } True """ return { 'message_id': self.__class__.__name__, 'message_text': self.format(), 'message_template': self.TEMPLATE, 'message_params': self.kwargs, 'message_code': self.CODE }