Capturing custom formatwarning with warnings.catch_warnings(record=True)

I’m not sure if this is a bug or not, but it seems non-intuitive to me. Originally I had titled this “Is warnings.catch_warnings(record=True) expected to ignore warnings.formatwarning?”, but as I wrote and gathered examples to demonstrate the issue, I’m realizing that I just need to find a way to work around the behavior of catch_warnings.

If I was catching warnings with warnings.catch_warnings(record=True), and if I patched warnings.formatwarning, my intuition is that there would be some way in the caught warning log that I could see what the modification induced by the patched warnings.formatwarning would be. However, it seems like that is not the case.

I have a MWE:


def main():
    import warnings

    print()
    print('--- WARNING 1 ---')
    warnings.warn('warning_number_01')
    print('_________________')

    orig_formatwarning = warnings.formatwarning

    def my_formatwarning(msg, *args, **kwargs):
        text = orig_formatwarning(msg, *args, **kwargs)
        return 'I CHANGED IT TO UPPER: ' + text

    warnings.formatwarning = my_formatwarning

    print()
    print('--- WARNING 2 ---')
    warnings.warn('warning_number_02')
    print('_________________')

    with warnings.catch_warnings(record=True) as warn_list:
        print()
        print('--- WARNING 3 ---')
        warnings.warn('warning_number_03')
        print('_________________')

    for warn_item in warn_list:
        print(f'Captured: {warn_item!r}')

    with warnings.catch_warnings():
        print()
        print('--- WARNING 4 ---')
        warnings.warn('warning_number_04')
        print('_________________')


if __name__ == '__main__':
    """
    CommandLine:
        python catch_warning_issue_mwe.py
    """
    main()

This shows:

--- WARNING 1 ---
/home/joncrall/code/xdev/dev/mwe/catch_warning_issue_mwe.py:7: UserWarning: warning_number_01
  warnings.warn('warning_number_01')
_________________

--- WARNING 2 ---
I CHANGED IT TO UPPER: /home/joncrall/code/xdev/dev/mwe/catch_warning_issue_mwe.py:20: UserWarning: warning_number_02
  warnings.warn('warning_number_02')
_________________

--- WARNING 3 ---
_________________
Captured: <warnings.WarningMessage object at 0x7f1246b2c180>

--- WARNING 4 ---
I CHANGED IT TO UPPER: /home/joncrall/code/xdev/dev/mwe/catch_warning_issue_mwe.py:35: UserWarning: warning_number_04
  warnings.warn('warning_number_04')

Warning 1 and 2 just show the original warning message and the patched warning message. Warning 3 shows that just the WarningMessage object is captured, but it does nothing with format message. Warning 4 shows that if record=False, then the formatter is invoked before printing.

The issue is that I can’t see what the formatted message would have been had record been False. This is a problem in xdoctest, which I’ve noted in the following issue: Side effects in warnings are suppressed by the xdoctest runner. · Issue #169 · Erotemic/xdoctest · GitHub

Because xdoctest needs to know about the warnings that happened, I use catch_warnings(record=True), but that causes a side effect meaning that I can’t test anything that formats warnings from within an xdoctest context.

I’m thinking I need to just implement a new catch_warnings that calls formatwarning or _formatwarnmsg_impl and includes the formatted result in the log? But I also want to be careful to avoid non-public APIs any custom handling doesn’t just break on a future Python release.

I’m wondering if other projects like pytest have encountered this issue before? Or if there are alternative ideas for working around this issue. Lastly, should CPython’s implementation of catch_warnings be improved to reduce side effects (i.e. not calling the custom formatting code) like this?