From 0e1f6b24f3a543140dc97928a50cdf69ffab61a7 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 27 May 2015 19:29:06 -0400 Subject: [PATCH 1/3] Added basic notifier --- letsencrypt/cli.py | 7 +++++ letsencrypt/interfaces.py | 27 +++++++++++++++++++ letsencrypt/notifier.py | 57 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 letsencrypt/notifier.py diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index defa7633d..8cb8ec84f 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1,5 +1,6 @@ """Let's Encrypt CLI.""" # TODO: Sanity check all input. Be sure to avoid shell code etc... +import atexit import argparse import logging import os @@ -20,6 +21,7 @@ from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util from letsencrypt import log +from letsencrypt import notifier from letsencrypt.display import util as display_util from letsencrypt.display import ops as display_ops @@ -347,6 +349,11 @@ def main(args=sys.argv[1:]): displayer = display_util.NcursesDisplay() zope.component.provideUtility(displayer) + # Notifier + notify = notifier.Notifier() + zope.component.provideUtility(notify) + atexit.register(notify.print_messages) + # Logging level = -args.verbose_count * 10 logger = logging.getLogger() diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 609b9410a..b39737070 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -346,3 +346,30 @@ class IValidator(zope.interface.Interface): def hsts(name): """Verify HSTS header is enabled.""" + + +class INotify(zope.interface.Interface): + """Interface to collect and display information to the user.""" + + HIGH_PRIORITY = zope.interface.Attribute( + "Used to denote high priority messages") + MEDIUM_PRIORITY = zope.interface.Attribute( + "Used to denote medium priority messages") + LOW_PRIORITY = zope.interface.Attribute( + "Used to denote low priority messages") + + def add_message(self, msg, priority, on_crash=False): + """Adds msg to the list of messages to be printed. + + :param str msg: Message to be displayed to the user. + + :param int priority: One of HIGH_PRIORITY, MEDIUM_PRIORITY, or + LOW_PRIORITY. + + :param bool on_crash: Whether or not the message should be printed if + the program exits abnormally. + + """ + + def print_messages(self): + """Prints messages to the user and clears the message queue.""" diff --git a/letsencrypt/notifier.py b/letsencrypt/notifier.py new file mode 100644 index 000000000..fa795673c --- /dev/null +++ b/letsencrypt/notifier.py @@ -0,0 +1,57 @@ +"""Collects and displays information to the user.""" +import collections +import Queue +import sys +import textwrap + +import zope.interface + +from letsencrypt import interfaces + + +class Notifier(object): + """Collects and displays information to the user. + + :ivar `Queue.PriorityQueue` messages: Messages to be displayed to the user. + + """ + + zope.interface.implements(interfaces.INotify) + + HIGH_PRIORITY, MEDIUM_PRIORITY, LOW_PRIORITY = xrange(3) + _msg_type = collections.namedtuple('Msg', 'priority, text, on_crash') + + def __init__(self): + self.messages = Queue.PriorityQueue() + + def add_message(self, msg, priority, on_crash=False): + """Adds msg to the list of messages to be printed. + + :param str msg: Message to be displayed to the user. + + :param int priority: One of HIGH_PRIORITY, MEDIUM_PRIORITY, or + LOW_PRIORITY. + + :param bool on_crash: Whether or not the message should be printed if + the program exits abnormally. + + """ + assert priority >= self.HIGH_PRIORITY and priority <= self.LOW_PRIORITY + self.messages.put(self._msg_type(priority, msg, on_crash)) + + def print_messages(self): + """Prints messages to the user and clears the message queue. + + If there is an unhandled exception, only messages for which on_crash is + True are printed. + + """ + if not self.messages.empty(): + no_exception = sys.exc_info()[0] is None + print 'IMPORTANT NOTES:' + wrapper = textwrap.TextWrapper(initial_indent=' - ', + subsequent_indent=' '*3) + while not self.messages.empty(): + msg = self.messages.get() + if no_exception or msg.on_crash: + print wrapper.fill(msg.text) From 8bcc8f802431a89a8752f09d28ea114b88e3cbec Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 29 May 2015 11:33:11 -0700 Subject: [PATCH 2/3] Finished basic reporter --- letsencrypt/cli.py | 8 +-- letsencrypt/interfaces.py | 2 +- letsencrypt/{notifier.py => reporter.py} | 15 ++++- letsencrypt/tests/reporter_test.py | 73 ++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 7 deletions(-) rename letsencrypt/{notifier.py => reporter.py} (78%) create mode 100644 letsencrypt/tests/reporter_test.py diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 0b8ef5761..85c2d2536 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -21,7 +21,7 @@ from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util from letsencrypt import log -from letsencrypt import notifier +from letsencrypt import reporter from letsencrypt.display import util as display_util from letsencrypt.display import ops as display_ops @@ -363,9 +363,9 @@ def main(args=sys.argv[1:]): zope.component.provideUtility(displayer) # Notifier - notify = notifier.Notifier() - zope.component.provideUtility(notify) - atexit.register(notify.print_messages) + report = reporter.Reporter() + zope.component.provideUtility(report) + atexit.register(report.print_messages) # Logging level = -args.verbose_count * 10 diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 60cf358e5..1eea6b2d3 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -353,7 +353,7 @@ class IValidator(zope.interface.Interface): """Verify HSTS header is enabled.""" -class INotify(zope.interface.Interface): +class IReporter(zope.interface.Interface): """Interface to collect and display information to the user.""" HIGH_PRIORITY = zope.interface.Attribute( diff --git a/letsencrypt/notifier.py b/letsencrypt/reporter.py similarity index 78% rename from letsencrypt/notifier.py rename to letsencrypt/reporter.py index fa795673c..26269e3f8 100644 --- a/letsencrypt/notifier.py +++ b/letsencrypt/reporter.py @@ -9,16 +9,18 @@ import zope.interface from letsencrypt import interfaces -class Notifier(object): +class Reporter(object): """Collects and displays information to the user. :ivar `Queue.PriorityQueue` messages: Messages to be displayed to the user. """ - zope.interface.implements(interfaces.INotify) + zope.interface.implements(interfaces.IReporter) HIGH_PRIORITY, MEDIUM_PRIORITY, LOW_PRIORITY = xrange(3) + _RESET = '\033[0m' + _BOLD = '\033[1m' _msg_type = collections.namedtuple('Msg', 'priority, text, on_crash') def __init__(self): @@ -46,12 +48,21 @@ class Notifier(object): True are printed. """ + bold_on = False if not self.messages.empty(): no_exception = sys.exc_info()[0] is None + bold_on = sys.stdout.isatty() + if bold_on: + sys.stdout.write(self._BOLD) print 'IMPORTANT NOTES:' wrapper = textwrap.TextWrapper(initial_indent=' - ', subsequent_indent=' '*3) while not self.messages.empty(): msg = self.messages.get() if no_exception or msg.on_crash: + if bold_on and msg.priority > self.HIGH_PRIORITY: + sys.stdout.write(self._RESET) + bold_on = False print wrapper.fill(msg.text) + if bold_on: + sys.stdout.write(self._RESET) diff --git a/letsencrypt/tests/reporter_test.py b/letsencrypt/tests/reporter_test.py new file mode 100644 index 000000000..df9790256 --- /dev/null +++ b/letsencrypt/tests/reporter_test.py @@ -0,0 +1,73 @@ +"""Tests for letsencrypt/reporter.py""" +import StringIO +import sys +import unittest + + +class ReporterTest(unittest.TestCase): + def setUp(self): + from letsencrypt import reporter + self.reporter = reporter.Reporter() + + self.old_stdout = sys.stdout + sys.stdout = StringIO.StringIO() + + def tearDown(self): + sys.stdout = self.old_stdout + + def test_tty_print_empty(self): + sys.stdout.isatty = lambda: True + self.test_no_tty_print_empty() + + def test_no_tty_print_empty(self): + self.reporter.print_messages() + self.assertEqual(sys.stdout.getvalue(), "") + try: + raise ValueError + except ValueError: + self.reporter.print_messages() + self.assertEqual(sys.stdout.getvalue(), "") + + def test_tty_successful_exit(self): + sys.stdout.isatty = lambda: True + self._successful_exit_common() + + def test_no_tty_successful_exit(self): + self._successful_exit_common() + + def test_tty_unsuccessful_exit(self): + sys.stdout.isatty = lambda: True + self._unsuccessful_exit_common() + + def test_no_tty_unsuccessful_exit(self): + self._unsuccessful_exit_common() + + def _successful_exit_common(self): + self._add_messages() + self.reporter.print_messages() + output = sys.stdout.getvalue() + self.assertTrue("IMPORTANT NOTES:" in output) + self.assertTrue("High" in output) + self.assertTrue("Med" in output) + self.assertTrue("Low" in output) + + def _unsuccessful_exit_common(self): + self._add_messages() + try: + raise ValueError + except ValueError: + self.reporter.print_messages() + output = sys.stdout.getvalue() + self.assertTrue("IMPORTANT NOTES:" in output) + self.assertTrue("High" in output) + self.assertTrue("Med" not in output) + self.assertTrue("Low" not in output) + + def _add_messages(self): + self.reporter.add_message("High", self.reporter.HIGH_PRIORITY, True) + self.reporter.add_message("Med", self.reporter.MEDIUM_PRIORITY) + self.reporter.add_message("Low", self.reporter.LOW_PRIORITY) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover From 4f7d83d274c7b80aa6650cbede54fe4fea9a49fb Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 29 May 2015 11:45:06 -0700 Subject: [PATCH 3/3] Corrected comment in cli.py --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 8600ecc4f..59e99648d 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -361,7 +361,7 @@ def main(args=sys.argv[1:]): displayer = display_util.NcursesDisplay() zope.component.provideUtility(displayer) - # Notifier + # Reporter report = reporter.Reporter() zope.component.provideUtility(report) atexit.register(report.print_messages)