简体   繁体   English

如何将 doctest 与日志一起使用?

[英]How to use doctest with logging?

The following doctest fails:以下 doctest 失败:

import logging
logging.basicConfig(level=logging.DEBUG,format='%(message)s')

def say_hello():
  '''
  >>> say_hello()
  Hello!
  '''
  logging.info('Hello!')

if __name__ == '__main__':
    import doctest
    doctest.testmod()

These pages这些页面

seem to suggest logging.StreamHandler(sys.stdout) and logger.addHandler(handler) but my attempts failed in this respect.似乎建议logging.StreamHandler(sys.stdout)logger.addHandler(handler)但我的尝试在这方面失败了。 (I am new to python, if it wasn't obvious.) (如果不是很明显的话,我是 python 的新手。)

Please help me fix the above code so that the test passes.请帮我修复上面的代码,以便测试通过。


Update on Jun 4, 2017: To answer 00prometheus ' comments: The accepted answer to use doctest and logging in python program , when I asked this question, seemed unnecessarily complicated. 2017 年 6 月 4 日更新:回答00prometheus的评论:The accepted answer to use doctest and logging in python program ,当我问这个问题时,似乎不必要地复杂。 And indeed it is, as the accepted answer here gives a simpler solution.确实如此,因为此处接受的答案提供了一个更简单的解决方案。 In my highly biased opinion, my question is also clearer than the one I already linked in the original post.在我的高度偏见中,我的问题也比我在原始帖子中已经链接的问题更清楚。

You need to define a "logger" object.您需要定义一个“记录器”对象。 This is usually done after import with:这通常在导入后完成:

import sys
import logging
log = logging.getLogger(__name__)

When you want to log a message:当您想记录消息时:

log.info('Hello!')

In the code that gets run like a script you set the basicConfig:在像脚本一样运行的代码中,您设置了 basicConfig:

if __name__ == '__main__':
    import doctest
    logging.basicConfig(level=logging.DEBUG, stream=sys.stdout, format='%(message)s')
    doctest.testmod()

Edit:编辑:

Ok, you were right.好吧,你是对的。 It doesn't work, but I got it to work...BUT DO NOT DO THIS!它不起作用,但我让它起作用了……但不要这样做! Just use print statements or return what you actually need to check.只需使用打印语句或返回您实际需要检查的内容。 As your second link says this is just a bad idea.正如您的第二个链接所说,这只是一个坏主意。 You shouldn't be checking logging output (its for logging).您不应该检查日志输出(用于日志记录)。 Even the original poster for that second link said they got it to work by switching their logging to using print.甚至第二个链接的原始海报也说他们通过将日志记录切换为使用打印来使其工作。 But here is the evil code that seems to work:但这是似乎有效的邪恶代码:

class MyDocTestRunner(doctest.DocTestRunner):
    def run(self, test, compileflags=None, out=None, clear_globs=True):
        if out is None:
            handler = None
        else:
            handler = logging.StreamHandler(self._fakeout)
            out = sys.stdout.write
        logger = logging.getLogger() # root logger (say)
        if handler:
            logger.addHandler(handler)
        try:
            doctest.DocTestRunner.run(self, test, compileflags, out, clear_globs)
        finally:
            if handler:
                logger.removeHandler(handler)
                handler.close()
    
if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG, format='%(message)s')
    tests = doctest.DocTestFinder().find(say_hello, __name__)
    dt_runner = MyDocTestRunner()
    for t in tests:
        dt_runner.run(t, out=True)

Edit (continued):编辑(续):

My attempts also failed when trying what your second link suggested.在尝试您的第二个链接建议时,我的尝试也失败了。 This is because internally doctest reassigns sys.stdout to self._fakeout .这是因为内部 doctest 将 sys.stdout 重新分配给self._fakeout That's why nothing short of my hack will work.这就是为什么只有我的 hack 才能奏效的原因。 I actually tell the logger to write to this "fakeout".我实际上告诉记录器写这个“假冒”。

Edit (answer to comment):编辑(回答评论):

It's not exactly the code from the link.这不完全是链接中的代码。 If it was the code from the link I would say it's not that bad of an option because its not doing anything too complex.如果是链接中的代码,我会说这不是一个糟糕的选择,因为它没有做任何太复杂的事情。 My code, however, is using a "private" internal instance attribute that shouldn't be used by a normal user.但是,我的代码使用了普通用户不应使用的“私有”内部实例属性。 That is why it is evil.这就是为什么它是邪恶的。

And yes, logging can be used for testing output, but it does not make much sense to do so in a unittest/doctest and is probably why doctest doesn't include functionality like this out of the box.是的,日志记录可用于测试输出,但在 unittest/doctest 中这样做没有多大意义,这可能是为什么 doctest 不包含这样的开箱即用功能的原因。 The TextTest stuff you linked to is all about functional or integration testing.您链接到的 TextTest 内容都是关于功能或集成测试的。 Unittests (and doctests) should be testing small individual components.单元测试(和文档测试)应该测试小的单个组件。 If you have to capture logged output to make sure your unittest/doctest is correct then you should maybe think about separating things out or not doing these checks in a doctest.如果您必须捕获记录的输出以确保您的 unittest/doctest 是正确的,那么您可能应该考虑将事情分开或不在 doctest 中进行这些检查。

I personally only use doctests for simple examples and verifications.我个人只将 doctests 用于简单的示例和验证。 Mostly for usage examples since any user can see an inline doctest.主要用于使用示例,因为任何用户都可以看到内联 doctest。

Edit (ok last one):编辑(好最后一个):

Same solution, simpler code.相同的解决方案,更简单的代码。 This code doesn't require that you create a custom runner.此代码不需要您创建自定义运行程序。 You still have to create the default runner and stuff because you need to access the "_fakeout" attribute.您仍然必须创建默认运行程序和其他内容,因为您需要访问“_fakeout”属性。 There is no way to use doctest to check logging output without logging to this attribute as a stream.如果不将此属性作为流记录,则无法使用 doctest 来检查记录输出。

if __name__ == '__main__':
    dt_runner = doctest.DocTestRunner()
    tests = doctest.DocTestFinder().find(sys.modules[__name__])
    logging.basicConfig(level=logging.DEBUG, format='%(message)s', stream=dt_runner._fakeout)
    for t in tests:
        dt_runner.run(t)

One way to do this is by monkey-patching the logging module (my code; docstring contents from import logging are relevant to your question):一种方法是通过猴子修补logging模块(我的代码; import logging中的文档字符串内容与您的问题相关):

@classmethod
def yield_int(cls, field, text):
    """Parse integer values and yield (field, value)

    >>> test = lambda text: dict(Monster.yield_int('passive', text))
    >>> test(None)
    {}
    >>> test('42')
    {'passive': 42}
    >>> import logging
    >>> old_warning = logging.warning
    >>> warnings = []
    >>> logging.warning = lambda msg: warnings.append(msg)
    >>> test('seven')
    {}
    >>> warnings
    ['yield_int: failed to parse text "seven"']
    >>> logging.warning = old_warning
    """
    if text == None:
        return

    try:
        yield (field, int(text))
    except ValueError:
        logging.warning(f'yield_int: failed to parse text "{text}"')

However, a much cleaner approach uses the unittest module:但是,更简洁的方法是使用unittest模块:

    >>> from unittest import TestCase
    >>> with TestCase.assertLogs(_) as cm:
    ...     print(test('seven'))
    ...     print(cm.output)
    {}
    ['WARNING:root:yield_int: failed to parse text "seven"']

Technically you should probably instantiate a TestCase object rather than passing _ to assertLogs as self , since there's no guarantee that this method won't attempt to access the instance properties in the future.从技术上讲,您可能应该实例化一个TestCase对象,而不是将_作为self传递给assertLogs ,因为不能保证此方法将来不会尝试访问实例属性。

I use the following technique:我使用以下技术:

  1. Set the logging stream to a StringIO object.将日志流设置为 StringIO 对象。
  2. Log away...退出...
  3. Print the contents for the StringIO object and expect the output.打印 StringIO 对象的内容并期待输出。
  4. Or: Assert against the contents of the StringIO object.或者:对 StringIO 对象的内容进行断言。

This should do it.这应该这样做。

Here is some example code.这是一些示例代码。

First it just does the whole setup for the logging within the doctest - just to show how it's working.首先,它只是在 doctest 中进行日志记录的整个设置 - 只是为了展示它是如何工作的。

Then the code shows how the setup can be put into as seperate function setup_doctest_logging that does the setup ànd returns itself a function that prints the log.然后代码显示了如何将设置作为单独的函数setup_doctest_logging进行设置,并返回一个打印日志的函数。 This keeps the test code more focused and moves the ceremonial part out of the test.这使测试代码更加集中,并将仪式部分移出测试。

import logging


def func(s):
    """
    >>> import io
    >>> string_io = io.StringIO()
    >>> # Capture the log output to a StringIO object
    >>> # Use force=True to make this configuration stick
    >>> logging.basicConfig(stream=string_io, format='%(message)s', level=logging.INFO, force=True)

    >>> func('hello world')

    >>> # print the contents of the StringIO. I prefer that. Better visibility.
    >>> print(string_io.getvalue(), end='')
    hello world
    >>> # The above needs the end='' because print will otherwise add an new line to the
    >>> # one that is already in the string from logging itself

    >>> # Or you can just expect an extra empty line like this:
    >>> print(string_io.getvalue())
    hello world
    <BLANKLINE>

    >>> func('and again')

    >>> # Or just assert on the contents.
    >>> assert 'and again' in string_io.getvalue()
    """
    logging.info(s)


def setup_doctest_logging(format='%(levelname)s %(message)s', level=logging.WARNING):
    """ 
    This could be put into a separate module to make the logging setup easier
    """
    import io
    string_io = io.StringIO()
    logging.basicConfig(stream=string_io, format=format, level=level, force=True)

    def log_printer():
        s = string_io.getvalue()
        print(s, end='')
    return log_printer


def other_logging_func(s, e=None):
    """
    >>> print_whole_log = setup_doctest_logging(level=logging.INFO)
    >>> other_logging_func('no error')
    >>> print_whole_log()
    WARNING no error
    >>> other_logging_func('I try hard', 'but I make mistakes')
    >>> print_whole_log()
    WARNING no error
    WARNING I try hard
    ERROR but I make mistakes
    """
    logging.warning(s)
    if e is not None:
        logging.error(e)


if __name__ == '__main__':
    import doctest
    doctest.testmod()

As mentioned by others, the issue is that doctest modifies sys.stdout after basicConfig created a StreamHandler that holds its own copy.正如其他人所提到的,问题是 doctest 在basicConfig创建了一个拥有自己副本的StreamHandler之后修改了sys.stdout One way to deal with this is to create a stream object that dispatches write and flush to sys.stdout .处理此问题的一种方法是创建一个 stream object 将writeflush分派到sys.stdout Another is to bypass the issue altogether by creating your own handler:另一种方法是通过创建自己的处理程序来完全绕过这个问题:

class PrintHandler(logging.Handler):
  def emit(self, record):
    print(self.format(record))

logging.basicConfig(level=logging.DEBUG, format='%(message)s',
  handlers=[PrintHandler()])

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM