def test_plain_text_output_format(self): """Inspect the plain text output of coloredlogs.""" logger = VerboseLogger(random_string(25)) stream = StringIO() install(level=logging.NOTSET, logger=logger, stream=stream) # Test that filtering on severity works. logger.setLevel(logging.INFO) logger.debug("No one should see this message.") assert len(stream.getvalue().strip()) == 0 # Test that the default output format looks okay in plain text. logger.setLevel(logging.NOTSET) for method, severity in ((logger.debug, 'DEBUG'), (logger.info, 'INFO'), (logger.verbose, 'VERBOSE'), (logger.warning, 'WARN'), (logger.error, 'ERROR'), (logger.critical, 'CRITICAL')): # Prepare the text. text = "This is a message with severity %r." % severity.lower() # Log the message with the given severity. method(text) # Get the line of output generated by the handler. output = stream.getvalue() lines = output.splitlines() last_line = lines[-1] assert text in last_line assert severity in last_line assert PLAIN_TEXT_PATTERN.match(last_line)
def render_usage(text): """ Reformat a command line program's usage message to reStructuredText_. :param text: The plain text usage message (a string). :returns: The usage message rendered to reStructuredText_ (a string). """ meta_variables = find_meta_variables(text) introduction, options = parse_usage(text) output = [render_paragraph(p, meta_variables) for p in introduction] if options: output.append('\n'.join([ '.. csv-table::', ' :header: Option, Description', ' :widths: 30, 70', '', ])) csv_buffer = StringIO() csv_writer = csv.writer(csv_buffer) while options: variants = options.pop(0) description = options.pop(0) csv_writer.writerow([ render_paragraph(variants, meta_variables), '\n\n'.join(render_paragraph(p, meta_variables) for p in split_paragraphs(description)), ]) csv_lines = csv_buffer.getvalue().splitlines() output.append('\n'.join(' %s' % l for l in csv_lines)) logger.debug("Rendered output: %s", output) return '\n\n'.join(trim_empty_lines(o) for o in output)
def render_usage(text): """ Reformat a command line program's usage message to reStructuredText_. :param text: The plain text usage message (a string). :returns: The usage message rendered to reStructuredText_ (a string). """ meta_variables = find_meta_variables(text) introduction, options = parse_usage(text) output = [render_paragraph(p, meta_variables) for p in introduction] if options: output.append('\n'.join([ '.. csv-table::', ' :header: Option, Description', ' :widths: 30, 70', '', ])) csv_buffer = StringIO() csv_writer = csv.writer(csv_buffer) while options: variants = options.pop(0) description = options.pop(0) csv_writer.writerow([ render_paragraph(variants, meta_variables), ('\n\n'.join( render_paragraph(p, meta_variables) for p in split_paragraphs(description))).rstrip(), ]) csv_lines = csv_buffer.getvalue().splitlines() output.append('\n'.join(' %s' % l for l in csv_lines)) logger.debug("Rendered output: %s", output) return '\n\n'.join(trim_empty_lines(o) for o in output)
def test_plain_text_output_format(self): """Inspect the plain text output of coloredlogs.""" logger = VerboseLogger(random_string(25)) stream = StringIO() install(level=logging.NOTSET, logger=logger, stream=stream) # Test that filtering on severity works. logger.setLevel(logging.INFO) logger.debug("No one should see this message.") assert len(stream.getvalue().strip()) == 0 # Test that the default output format looks okay in plain text. logger.setLevel(logging.NOTSET) for method, severity in ((logger.debug, 'DEBUG'), (logger.info, 'INFO'), (logger.verbose, 'VERBOSE'), (logger.warning, 'WARNING'), (logger.error, 'ERROR'), (logger.critical, 'CRITICAL')): # Prepare the text. text = "This is a message with severity %r." % severity.lower() # Log the message with the given severity. method(text) # Get the line of output generated by the handler. output = stream.getvalue() lines = output.splitlines() last_line = lines[-1] assert text in last_line assert severity in last_line assert PLAIN_TEXT_PATTERN.match(last_line)
def test_support_for_milliseconds_directive(self): """Make sure milliseconds using the ``%f`` directive are supported.""" stream = StringIO() install(reconfigure=True, stream=stream, datefmt='%Y-%m-%dT%H:%M:%S.%f%z') logging.info("This should be timestamped according to #45.") assert re.match( r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{4}\s', stream.getvalue())
def test_support_for_milliseconds(self): """Make sure milliseconds are hidden by default but can be easily enabled.""" # Check that the default log format doesn't include milliseconds. stream = StringIO() install(reconfigure=True, stream=stream) logging.info("This should not include milliseconds.") assert all(map(PLAIN_TEXT_PATTERN.match, stream.getvalue().splitlines())) # Check that milliseconds can be enabled via a shortcut. stream = StringIO() install(milliseconds=True, reconfigure=True, stream=stream) logging.info("This should include milliseconds.") assert all(map(PATTERN_INCLUDING_MILLISECONDS.match, stream.getvalue().splitlines()))
def __init__(self, merged=False, input=''): """ Initialize a :class:`CaptureOutput` object. :param merged: :data:`True` to merge the streams, :data:`False` to capture them separately. :param input: The data that reads from :data:`sys.stdin` should return (a string). """ self.stdin = StringIO(input) self.stdout = StringIO() self.stderr = self.stdout if merged else StringIO() self.patched_attributes = [ PatchedAttribute(sys, name, getattr(self, name)) for name in ('stdin', 'stdout', 'stderr') ]
def test_spinner(self): """Test :func:`humanfriendly.Spinner`.""" stream = StringIO() spinner = humanfriendly.Spinner('test spinner', total=4, stream=stream, interactive=True) for progress in [1, 2, 3, 4]: spinner.step(progress=progress) time.sleep(0.2) spinner.clear() output = stream.getvalue() output = (output.replace(humanfriendly.show_cursor_code, '') .replace(humanfriendly.hide_cursor_code, '')) lines = [line for line in output.split(humanfriendly.erase_line_code) if line] self.assertTrue(len(lines) > 0) self.assertTrue(all('test spinner' in l for l in lines)) self.assertTrue(all('%' in l for l in lines)) self.assertEqual(sorted(set(lines)), sorted(lines))
def main(*args, **kw): """Utility function to test the command line interface without invoking a subprocess.""" returncode = 0 input_buffer = StringIO(kw.get('input', '')) output_buffer = StringIO() saved_argv = sys.argv saved_stdin = sys.stdin saved_stdout = sys.stdout try: sys.argv = [sys.argv[0]] + list(args) sys.stdin = input_buffer sys.stdout = output_buffer cli.main() except SystemExit as e: returncode = e.code or 1 finally: sys.argv = saved_argv sys.stdin = saved_stdin sys.stdout = saved_stdout return returncode, output_buffer.getvalue()
def test_find_terminal_size(self): """Test :func:`humanfriendly.terminal.find_terminal_size()`.""" lines, columns = find_terminal_size() # We really can't assert any minimum or maximum values here because it # simply doesn't make any sense; it's impossible for me to anticipate # on what environments this test suite will run in the future. assert lines > 0 assert columns > 0 # The find_terminal_size_using_ioctl() function is the default # implementation and it will likely work fine. This makes it hard to # test the fall back code paths though. However there's an easy way to # make find_terminal_size_using_ioctl() fail ... saved_stdin = sys.stdin saved_stdout = sys.stdout saved_stderr = sys.stderr try: # What do you mean this is brute force?! ;-) sys.stdin = StringIO() sys.stdout = StringIO() sys.stderr = StringIO() # Now find_terminal_size_using_ioctl() should fail even though # find_terminal_size_using_stty() might work fine. lines, columns = find_terminal_size() assert lines > 0 assert columns > 0 # There's also an ugly way to make `stty size' fail: The # subprocess.Popen class uses os.execvp() underneath, so if we # clear the $PATH it will break. saved_path = os.environ['PATH'] try: os.environ['PATH'] = '' # Now find_terminal_size_using_stty() should fail. lines, columns = find_terminal_size() assert lines > 0 assert columns > 0 finally: os.environ['PATH'] = saved_path finally: sys.stdin = saved_stdin sys.stdout = saved_stdout sys.stderr = saved_stderr
def main(*arguments, **options): """Wrap the command line interface to make it easier to test.""" capture = options.get('capture', False) saved_argv = sys.argv saved_stdout = sys.stdout try: sys.argv = arguments if capture: sys.stdout = StringIO() coloredlogs.cli.main() if capture: return sys.stdout.getvalue() finally: sys.argv = saved_argv sys.stdout = saved_stdout
def test_plain_text_output_format(self): """Inspect the plain text output of coloredlogs.""" logger = VerboseLogger(random_string(25)) stream = StringIO() install(level=logging.NOTSET, logger=logger, stream=stream) # Test that filtering on severity works. logger.setLevel(logging.INFO) logger.debug("No one should see this message.") assert len(stream.getvalue().strip()) == 0 # Test that the default output format looks okay in plain text. logger.setLevel(logging.NOTSET) for method, severity in ((logger.debug, 'DEBUG'), (logger.info, 'INFO'), (logger.verbose, 'VERBOSE'), (logger.warning, 'WARNING'), (logger.error, 'ERROR'), (logger.critical, 'CRITICAL')): # XXX Workaround for a regression in Python 3.7 caused by the # Logger.isEnabledFor() method using stale cache entries. If we # don't clear the cache then logger.isEnabledFor(logging.DEBUG) # returns False and no DEBUG message is emitted. try: logger._cache.clear() except AttributeError: pass # Prepare the text. text = "This is a message with severity %r." % severity.lower() # Log the message with the given severity. method(text) # Get the line of output generated by the handler. output = stream.getvalue() lines = output.splitlines() last_line = lines[-1] assert text in last_line assert severity in last_line assert PLAIN_TEXT_PATTERN.match(last_line)
class CaptureOutput(ContextManager): """Context manager that captures what's written to :data:`sys.stdout` and :data:`sys.stderr`.""" def __init__(self, merged=False, input=''): """ Initialize a :class:`CaptureOutput` object. :param merged: :data:`True` to merge the streams, :data:`False` to capture them separately. :param input: The data that reads from :data:`sys.stdin` should return (a string). """ self.stdin = StringIO(input) self.stdout = StringIO() self.stderr = self.stdout if merged else StringIO() self.patched_attributes = [ PatchedAttribute(sys, name, getattr(self, name)) for name in ('stdin', 'stdout', 'stderr') ] stdin = None """The :class:`~humanfriendly.compat.StringIO` object used to feed the standard input stream.""" stdout = None """The :class:`~humanfriendly.compat.StringIO` object used to capture the standard output stream.""" stderr = None """The :class:`~humanfriendly.compat.StringIO` object used to capture the standard error stream.""" def __enter__(self): """Start capturing what's written to :data:`sys.stdout` and :data:`sys.stderr`.""" super(CaptureOutput, self).__enter__() for context in self.patched_attributes: context.__enter__() return self def __exit__(self, exc_type=None, exc_value=None, traceback=None): """Stop capturing what's written to :data:`sys.stdout` and :data:`sys.stderr`.""" super(CaptureOutput, self).__exit__(exc_type, exc_value, traceback) for context in self.patched_attributes: context.__exit__(exc_type, exc_value, traceback) def getvalue(self): """Get the text written to :data:`sys.stdout`.""" return self.stdout.getvalue()
def reset(self): """ Reset the state of the HTML parser and ANSI converter. When `output` is a :class:`~python3:io.StringIO` object a new instance will be created (and the old one garbage collected). """ # Reset the state of the superclass. HTMLParser.reset(self) # Reset our instance variables. self.link_text = None self.link_url = None self.preformatted_text_level = 0 if self.output is None or isinstance(self.output, StringIO): # If the caller specified something like output=sys.stdout then it # doesn't make much sense to negate that choice here in reset(). self.output = StringIO() self.stack = []
def test_dynamic_stderr_lookup(self): """Make sure coloredlogs.install() uses StandardErrorHandler when possible.""" coloredlogs.install() # Redirect sys.stderr to a temporary buffer. initial_stream = StringIO() initial_text = "Which stream will receive this text?" with PatchedAttribute(sys, 'stderr', initial_stream): logging.info(initial_text) assert initial_text in initial_stream.getvalue() # Redirect sys.stderr again, to a different destination. subsequent_stream = StringIO() subsequent_text = "And which stream will receive this other text?" with PatchedAttribute(sys, 'stderr', subsequent_stream): logging.info(subsequent_text) assert subsequent_text in subsequent_stream.getvalue()
def __init__(self, merged=False, input='', enabled=True): """ Initialize a :class:`CaptureOutput` object. :param merged: :data:`True` to merge the streams, :data:`False` to capture them separately. :param input: The data that reads from :data:`sys.stdin` should return (a string). :param enabled: :data:`True` to enable capturing (the default), :data:`False` otherwise. This makes it easy to unconditionally use :class:`CaptureOutput` in a :keyword:`with` block while preserving the choice to opt out of capturing output. """ self.stdin = StringIO(input) self.stdout = CaptureBuffer() self.stderr = self.stdout if merged else CaptureBuffer() self.patched_attributes = [] if enabled: self.patched_attributes.extend( PatchedAttribute(sys, name, getattr(self, name)) for name in ('stdin', 'stdout', 'stderr'))
def test_support_for_milliseconds_directive(self): """Make sure milliseconds using the ``%f`` directive are supported.""" stream = StringIO() install(reconfigure=True, stream=stream, datefmt='%Y-%m-%dT%H:%M:%S.%f%z') logging.info("This should be timestamped according to #45.") assert re.match(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+\d{4}\s', stream.getvalue())