base.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. # Copyright 2013 The Distro Tracker Developers
  2. # See the COPYRIGHT file at the top-level directory of this distribution and
  3. # at http://deb.li/DTAuthors
  4. #
  5. # This file is part of Distro Tracker. It is subject to the license terms
  6. # in the LICENSE file found in the top-level directory of this
  7. # distribution and at http://deb.li/DTLicense. No part of Distro Tracker,
  8. # including this file, may be copied, modified, propagated, or distributed
  9. # except according to the terms contained in the LICENSE file.
  10. """
  11. The module defining common functionality and base classes for all email control
  12. commands.
  13. """
  14. from __future__ import unicode_literals
  15. from django.utils import six
  16. import re
  17. from django.conf import settings
  18. DISTRO_TRACKER_CONTROL_EMAIL = settings.DISTRO_TRACKER_CONTROL_EMAIL
  19. class MetaCommand(type):
  20. """
  21. Meta class for Distro Tracker Commands.
  22. Transforms the :py:attr:`Command.REGEX_LIST` given in a Command sublclass
  23. to include all aliases of the command. When implementing a
  24. :py:class:`Command` subclass, it is not necessary to include a separate
  25. regex for each command alias or a long one listing every option.
  26. """
  27. def __init__(cls, name, bases, dct): # noqa
  28. if not getattr(cls, 'META', None):
  29. return
  30. joined_aliases = '|'.join(
  31. alias
  32. for alias in [cls.META['name']] + cls.META.get('aliases', [])
  33. )
  34. cls.REGEX_LIST = tuple(
  35. '^(?:' + joined_aliases + ')' + regex
  36. for regex in cls.REGEX_LIST
  37. )
  38. class Command(six.with_metaclass(MetaCommand)):
  39. """
  40. Base class for commands. Instances of this class can be used for no-op
  41. commands.
  42. """
  43. __metaclass__ = MetaCommand
  44. META = {}
  45. """
  46. Meta information about the command, given as key/value pairs. Expected
  47. keys are:
  48. - ``description`` - Description of the command which will be shown in the
  49. help output
  50. - ``name`` - Name of the command. Makes it possible to match command lines
  51. in control messages to command classes since each command line starts
  52. with the name of the command.
  53. - ``aliases`` - List of alternative names for the command
  54. - ``position`` - Preferred position in the help output
  55. """
  56. REGEX_LIST = ()
  57. """
  58. A list of regular expressions which, when matched to a string, identify
  59. a command. Additionally, any named group in the regular expression should
  60. exactly match the name of the parameter in the constructor of the command.
  61. If unnamed groups are used, their order must be the same as the order of
  62. parameters in the constructor of the command.
  63. This is very similar to how Django handles linking views and URLs.
  64. It is only necessary to list the part of the command's syntax to
  65. capture the parameters, while the name and all aliases given in the META
  66. dict are automatically assumed when matching a string to the command.
  67. """
  68. def __init__(self, *args):
  69. self._sent_mails = []
  70. self.out = []
  71. def __call__(self):
  72. """
  73. The base class delegates execution to the appropriate :py:meth:`handle`
  74. method and handles the reply.
  75. """
  76. self.handle()
  77. return self.render_reply()
  78. def handle(self):
  79. """
  80. Performs the necessary steps to execute the command.
  81. """
  82. pass
  83. def is_valid(self):
  84. return True
  85. def get_command_text(self, *args):
  86. """
  87. Returns a string representation of the command.
  88. """
  89. return ' '.join((self.META.get('name', '#'), ) + args)
  90. @classmethod
  91. def match_line(cls, line):
  92. """
  93. Class method to check whether the given line matches the command.
  94. :param line: The line to check whether it matches the command.
  95. """
  96. for pattern in cls.REGEX_LIST:
  97. match = re.match(pattern, line, re.IGNORECASE)
  98. if match:
  99. return match
  100. def render_reply(self):
  101. """
  102. Returns a string representing the command's reply.
  103. """
  104. return '\n'.join(self.out)
  105. def reply(self, message):
  106. """
  107. Adds a message to the command's reply.
  108. :param message: Message to include in the reply
  109. :type message: string
  110. """
  111. self.out.append(message)
  112. def warn(self, message):
  113. """
  114. Adds a warning to the command's reply.
  115. :param message: Message to include in the reply
  116. :type message: string
  117. """
  118. self.out.append('Warning: ' + message)
  119. def error(self, message):
  120. """
  121. Adds an error message to the command's reply.
  122. :param message: Message to include in the reply
  123. :type message: string
  124. """
  125. self.out.append("Error: " + message)
  126. def list_reply(self, items, bullet='*'):
  127. """
  128. Includes a list of items in the reply. Each item is converted to a
  129. string before being output.
  130. :param items: An iterable of items to be included in the form of a list
  131. in the reply.
  132. :param bullet: The character to be used as the "bullet" of the list.
  133. """
  134. for item in items:
  135. self.reply(bullet + ' ' + str(item))