bot icon indicating copy to clipboard operation
bot copied to clipboard

Upgrade pytest to 9.0.0, remove pytest-subtests, and migrate to new TOML config

Open Copilot opened this issue 1 month ago • 3 comments

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
  • Update pytest to 9.0.0
  • Migrate to using TOML configuration for pytest: https://docs.pytest.org/en/stable/reference/customize.html#config-file-formats
  • Additionally, fix the tests so these DumpErrors related to our mocks do not occur:
[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 avatar Nov 11 '25 01:11 Copilot

@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

  • The directive about moving to TOML specifically means the new non-INI method: https://docs.pytest.org/en/stable/changelog.html#new-features

jb3 avatar Nov 11 '25 01:11 jb3

@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 avatar Nov 11 '25 01:11 Copilot

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 avatar Nov 11 '25 20:11 jb3