Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 11, 2025

Pytest 9.0.0 introduced native support for unittest's self.subTest() and stricter serialization requirements for subTest parameters when using pytest-xdist parallel execution. Non-serializable objects (mocks, datetime instances, exceptions) passed to subTest() now cause DumpError failures.

Changes

  • Upgraded pytest: 8.4.2 → 9.0.0 in pyproject.toml
  • Removed pytest-subtests: Removed pytest-subtests==0.14.1 from dev dependencies (pytest 9.0 has native support)
  • Migrated to new TOML config format: Changed from [tool.pytest.ini_options] to [tool.pytest] with list-based addopts
  • Fixed subTest serialization: Replaced non-serializable parameters with simple identifiers across 20+ test files

Pattern Applied

Before:

for case in test_cases:
    with self.subTest(error=case["error"], channel=MockTextChannel()):
        # test logic

After:

for i, case in enumerate(test_cases):
    with self.subTest(test_case=i, has_channel=True):
        # test logic

Configuration Migration

Before:

[tool.pytest.ini_options]
addopts = "-p no:nose"

After:

[tool.pytest]
addopts = ["-p", "no:nose"]

Additional Fixes

  • Fixed HTTPException initialization in test_utils.py (requires message argument)

All 442 tests pass with xdist parallel execution.

Original prompt
[gw1] linux -- Python 3.13.9 /home/runner/work/bot/bot/.venv/bin/python3

self = <tests.bot.exts.backend.test_error_handler.IndividualErrorHandlerTests testMethod=test_handle_api_error>
log_mock = <MagicMock name='log' id='140094977313488'>

    @patch("bot.exts.backend.error_handler.log")
    async def test_handle_api_error(self, log_mock):
        """Should `ctx.send` on HTTP error codes, and log at correct level."""
        test_cases = (
            {
                "error": ResponseCodeError(AsyncMock(status=400)),
                "log_level": "error"
            },
            {
                "error": ResponseCodeError(AsyncMock(status=404)),
                "log_level": "debug"
            },
            {
                "error": ResponseCodeError(AsyncMock(status=550)),
                "log_level": "warning"
            },
            {
                "error": ResponseCodeError(AsyncMock(status=1000)),
                "log_level": "warning"
            }
        )
    
        for case in test_cases:
>           with self.subTest(error=case["error"], log_level=case["log_level"]):
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/bot/exts/backend/test_error_handler.py:486: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../_temp/uv-python-dir/cpython-3.13.9-linux-x86_64-gnu/lib/python3.13/contextlib.py:148: in __exit__
    next(self.gen)
../../_temp/uv-python-dir/cpython-3.13.9-linux-x86_64-gnu/lib/python3.13/contextlib.py:148: in __exit__
    next(self.gen)
.venv/lib/python3.13/site-packages/_pytest/unittest.py:438: in addSubTest
    self.ihook.pytest_runtest_logreport(report=sub_report)
.venv/lib/python3.13/site-packages/pluggy/_hooks.py:512: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/pluggy/_manager.py:120: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/xdist/remote.py:255: in pytest_runtest_logreport
    self.sendevent("testreport", data=data)
.venv/lib/python3.13/site-packages/xdist/remote.py:98: in sendevent
    self.channel.send((name, kwargs))
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:912: in send
    self.gateway._send(Message.CHANNEL_DATA, self.id, dumps_internal(item))
                                                      ^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1629: in dumps_internal
    return _Serializer().save(obj)  # type: ignore[return-value]
           ^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1647: in save
    self._save(obj)
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1667: in _save
    dispatch(self, obj)
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1744: in save_tuple
    self._save(item)
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1667: in _save
    dispatch(self, obj)
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1740: in save_dict
    self._write_setitem(key, value)
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1734: in _write_setitem
    self._save(value)
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1667: in _save
    dispatch(self, obj)
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1740: in save_dict
    self._write_setitem(key, value)
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1734: in _write_setitem
    self._save(value)
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1667: in _save
    dispatch(self, obj)
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1740: in save_dict
    self._write_setitem(key, value)
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1734: in _write_setitem
    self._save(value)
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1667: in _save
    dispatch(self, obj)
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1740: in save_dict
    self._write_setitem(key, value)
.venv/lib/python3.13/site-packages/execnet/gateway_base.py:1734: in _write_setitem
    self._save(value)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <execnet.gateway_base._Serializer object at 0x7f6a67591eb0>
obj = ResponseCodeError(<AsyncMock id='140094976712656'>)

    def _save(self, obj: object) -> None:
        tp = type(obj)
        try:
            dispatch = self._dispatch[tp]
        except KeyError:
            methodname = "save_" + tp.__name__
 ...

</details>



<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Copilot AI assigned Copilot and jb3 Nov 11, 2025
Copilot AI changed the title [WIP] Update pytest to 9.0.0 and migrate to TOML configuration Upgrade pytest to 9.0.0 and fix subTest serialization errors with xdist Nov 11, 2025
Copilot AI requested a review from jb3 November 11, 2025 01:48
Copilot finished work on behalf of jb3 November 11, 2025 01:48
@jb3
Copy link
Member

jb3 commented Nov 11, 2025

@copilot - pytest 9.0 supports subtests natively now, and these should be used (and the pytest-subtests removed from the pyproject and project), they are documented here: https://docs.pytest.org/en/stable/how-to/subtests.html#subtests

Copy link
Contributor Author

Copilot AI commented Nov 11, 2025

@copilot - pytest 9.0 supports subtests natively now, and these should be used (and the pytest-subtests removed from the pyproject and project), they are documented here: https://docs.pytest.org/en/st...

Removed pytest-subtests plugin and migrated to the new TOML configuration format in commit e6029ef.

Changes:

  • Removed pytest-subtests==0.14.1 from dev dependencies (pytest 9.0 has native support for unittest's self.subTest())
  • Changed [tool.pytest.ini_options] to [tool.pytest]
  • Updated addopts from string to list format: ["-p", "no:nose"]

All 442 tests passing with the new configuration.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • publicsuffix.org
    • Triggering command: /home/REDACTED/work/bot/bot/.venv/bin/python -u -c import sys;exec(eval(sys.stdin.readline())) (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title Upgrade pytest to 9.0.0 and fix subTest serialization errors with xdist Upgrade pytest to 9.0.0, remove pytest-subtests, and migrate to new TOML config Nov 11, 2025
Copilot finished work on behalf of jb3 November 11, 2025 01:55
jb3 added 3 commits November 11, 2025 02:04
Prevents DumpError when test parameters are sent between test executors
but cannot be serialised (primarily a problem for out mock objects).
@jb3 jb3 force-pushed the copilot/update-pytest-and-fix-tests branch from e6029ef to e4ac857 Compare November 11, 2025 02:05
@jb3 jb3 marked this pull request as ready for review November 11, 2025 02:07
@jb3 jb3 requested a review from MarkKoz as a code owner November 11, 2025 02:07
@python-discord-policy-bot python-discord-policy-bot bot requested a review from a team November 11, 2025 02:07
Copy link
Contributor

@wookie184 wookie184 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not reviewed fully, this seems pretty meh.

Comment on lines +98 to +102
[tool.pytest]
# We don't use nose style tests so disable them in pytest.
# This stops pytest from running functions named `setup` in test files.
# See https://github.com/python-discord/bot/pull/2229#issuecomment-1204436420
addopts = "-p no:nose"
addopts = ["-p", "no:nose"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nose support was dropped in pytest 8.0.0 so this should no longer be necessary https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose

Comment on lines +436 to 441
for i, ((duration, message, was_silenced), target) in enumerate(itertools.product(test_cases, targets)):
with (
mock.patch.object(self.cog, "_set_silence_overwrites", return_value=was_silenced),
self.subTest(was_silenced=was_silenced, target=target, message=message),
self.subTest(test_case=i, was_silenced=was_silenced, has_target=target is not None),
mock.patch.object(self.cog, "send_message") as send_message
):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a test case ID seems silly here, as it's coming from an itertools.product and it would be a pain to work out the inputs that it's referring to.

Also the has_targets is not a replacement for the target because there are multiple not none targets.

I'd accept that the message can be removed though, since it's an expected output.

Comment on lines +725 to +726
for i, channel in enumerate((MockVoiceChannel(), MockTextChannel())):
with self.subTest(test_case=i):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno about just wrapping everything in enumerate in general, I feel like e.g. str(type(channel)) would be a more descriptive name for a test case.

Comment on lines +814 to +815
for i, overwrite_json in enumerate(('{"send_messages": true, "add_reactions": null}', None)):
with self.subTest(test_case=i, has_overwrite=overwrite_json is not None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

surely this change just isn't necessary?

@jb3
Copy link
Member

jb3 commented Nov 11, 2025

Will go through and have a proper look case-by-case, I agree there are probably ways in a lot of them where we can better serialize instead of using enumerate.

@jb3 jb3 marked this pull request as draft November 11, 2025 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants