smtpdfix icon indicating copy to clipboard operation
smtpdfix copied to clipboard

`PytestUnraisableExceptionWarning` issued when using SSL with smtpdfix 0.5.3

Open jwodder opened this issue 1 month ago • 0 comments

When running the following code under pytest 9.0.1 using smtpdfix 0.5.3:

from smtplib import SMTP_SSL


def test_sendmail(smtpd):
    smtpd.config.use_ssl = True
    from_addr = "[email protected]"
    to_addrs = "[email protected]"
    msg = (
        f"From: {from_addr}\r\n"
        f"To: {to_addrs}\r\n"
        f"Subject: Foo\r\n\r\n"
        f"Foo bar"
    )
    with SMTP_SSL(smtpd.hostname, smtpd.port) as client:
        client.sendmail(from_addr, to_addrs, msg)
    assert len(smtpd.messages) == 1

with the following pytest configuration in tox.ini:

[pytest]
filterwarnings = error

the following error results:

============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.0.1, pluggy-1.6.0
rootdir: /Users/jwodder/work/dev/tmp/smtpdfix-bug
configfile: tox.ini
plugins: smtpdfix-0.5.3
collected 1 item

test_bug.py F                                                            [100%]

=================================== FAILURES ===================================
________________________________ test_sendmail _________________________________

self = <smtpdfix.controller.AuthController object at 0x105939d30>

    def start(self) -> None:
        """
        Start a thread and run the asyncio event loop in that thread
        """
        assert self._thread is None, "SMTP daemon already running"
        self._factory_invoked.clear()
    
        ready_event = threading.Event()
        self._thread = threading.Thread(target=self._run, args=(ready_event,))
        self._thread.daemon = True
        self._thread.start()
        # Wait a while until the server is responding.
        start = time.monotonic()
        if not ready_event.wait(self.ready_timeout):
            # An exception within self._run will also result in ready_event not set
            # So, we first test for that, before raising TimeoutError
            if self._thread_exception is not None:  # pragma: on-wsl
                # See comment about WSL1.0 in the _run() method
                raise self._thread_exception
            else:
                raise TimeoutError(
                    "SMTP server failed to start within allotted time. "
                    "This might happen if the system is too busy. "
                    "Try increasing the `ready_timeout` parameter."
                )
        respond_timeout = self.ready_timeout - (time.monotonic() - start)
    
        # Apparently create_server invokes factory() "lazily", so exceptions in
        # factory() go undetected. To trigger factory() invocation we need to open
        # a connection to the server and 'exchange' some traffic.
        try:
>           self._trigger_server()
E           ResourceWarning: unclosed <ssl.SSLSocket fd=19, family=30, type=1, proto=0, laddr=('::1', 63976, 0, 0), raddr=('::1', 63971, 0, 0)>

../../../../.local/virtualenvwrapper/venvs/tmp-92b91dc2895b205/lib/python3.14/site-packages/aiosmtpd/controller.py:287: ResourceWarning

The above exception was the direct cause of the following exception:

cls = <class '_pytest.runner.CallInfo'>
func = <function call_and_report.<locals>.<lambda> at 0x105975b10>
when = 'call'
reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    @classmethod
    def from_call(
        cls,
        func: Callable[[], TResult],
        when: Literal["collect", "setup", "call", "teardown"],
        reraise: type[BaseException] | tuple[type[BaseException], ...] | None = None,
    ) -> CallInfo[TResult]:
        """Call func, wrapping the result in a CallInfo.
    
        :param func:
            The function to call. Called without arguments.
        :type func: Callable[[], _pytest.runner.TResult]
        :param when:
            The phase in which the function is called.
        :param reraise:
            Exception or exceptions that shall propagate if raised by the
            function, instead of being wrapped in the CallInfo.
        """
        excinfo = None
        instant = timing.Instant()
        try:
>           result: TResult | None = func()
                                     ^^^^^^

../../../../.local/virtualenvwrapper/venvs/tmp-92b91dc2895b205/lib/python3.14/site-packages/_pytest/runner.py:353: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../../../.local/virtualenvwrapper/venvs/tmp-92b91dc2895b205/lib/python3.14/site-packages/_pytest/runner.py:245: in <lambda>
    lambda: runtest_hook(item=item, **kwds),
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../../.local/virtualenvwrapper/venvs/tmp-92b91dc2895b205/lib/python3.14/site-packages/pluggy/_hooks.py:512: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../../.local/virtualenvwrapper/venvs/tmp-92b91dc2895b205/lib/python3.14/site-packages/pluggy/_manager.py:120: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../../.local/virtualenvwrapper/venvs/tmp-92b91dc2895b205/lib/python3.14/site-packages/_pytest/logging.py:850: in pytest_runtest_call
    yield
../../../../.local/virtualenvwrapper/venvs/tmp-92b91dc2895b205/lib/python3.14/site-packages/_pytest/capture.py:900: in pytest_runtest_call
    return (yield)
            ^^^^^
../../../../.local/virtualenvwrapper/venvs/tmp-92b91dc2895b205/lib/python3.14/site-packages/_pytest/skipping.py:268: in pytest_runtest_call
    return (yield)
            ^^^^^
../../../../.local/virtualenvwrapper/venvs/tmp-92b91dc2895b205/lib/python3.14/site-packages/_pytest/unraisableexception.py:158: in pytest_runtest_call
    collect_unraisable(item.config)
../../../../.local/virtualenvwrapper/venvs/tmp-92b91dc2895b205/lib/python3.14/site-packages/_pytest/unraisableexception.py:79: in collect_unraisable
    raise errors[0]
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = <_pytest.config.Config object at 0x1049e81a0>

    def collect_unraisable(config: Config) -> None:
        pop_unraisable = config.stash[unraisable_exceptions].pop
        errors: list[pytest.PytestUnraisableExceptionWarning | RuntimeError] = []
        meta = None
        hook_error = None
        try:
            while True:
                try:
                    meta = pop_unraisable()
                except IndexError:
                    break
    
                if isinstance(meta, BaseException):
                    hook_error = RuntimeError("Failed to process unraisable exception")
                    hook_error.__cause__ = meta
                    errors.append(hook_error)
                    continue
    
                msg = meta.msg
                try:
>                   warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
E                   pytest.PytestUnraisableExceptionWarning: Exception ignored while finalizing socket <ssl.SSLSocket fd=19, family=30, type=1, proto=0, laddr=('::1', 63976, 0, 0), raddr=('::1', 63971, 0, 0)>: None

../../../../.local/virtualenvwrapper/venvs/tmp-92b91dc2895b205/lib/python3.14/site-packages/_pytest/unraisableexception.py:67: PytestUnraisableExceptionWarning
=========================== short test summary info ============================
FAILED test_bug.py::test_sendmail - pytest.PytestUnraisableExceptionWarning: ...
============================== 1 failed in 0.21s ===============================

If smtpdfix 0.5.2 is used instead, no error occurs.

Environment (please complete the following information):

  • OS: macOS Sonoma 14.7.8
  • Python version: Python 3.14.0

jwodder avatar Nov 21 '25 14:11 jwodder