Skip to content

INTERNALERROR: 'NoneType' object has no attribute '__globals__' when collecting non-Function items (e.g. sybil-driven docs tests) #172

Description

@adamtheturtle

Summary

pytest-run-parallel's _handle_collected_item assumes every collected item is a pytest.Function (or otherwise has an item.obj callable with a __globals__ mapping). Plugins that contribute non-Function items, such as sybil's pytest integration (Sybil(...).pytest()), fail collection with INTERNALERROR before any test runs, even when those items are not intended to be parallelized.

This makes pytest-run-parallel incompatible with any project that uses sybil to run examples extracted from documentation as pytest tests.

Versions:

  • pytest-run-parallel==0.8.2
  • pytest==9.0.3
  • sybil==10.0.1
  • CPython 3.13.9

Reproduction

project/
  conftest.py
  docs/example.rst

conftest.py:

from sybil import Sybil
from sybil.parsers.rest import CodeBlockParser


pytest_collect_file = Sybil(
    parsers=[CodeBlockParser("python", lambda example: None)],
    patterns=["*.rst"],
).pytest()

docs/example.rst:

Example
=======

.. code-block:: python

   x = 1

Run:

pytest --parallel-threads=8

Result:

INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File ".../pytest_run_parallel/plugin.py", line 254, in pytest_collection_finish
INTERNALERROR>     self._handle_collected_item(item)
INTERNALERROR>   File ".../pytest_run_parallel/plugin.py", line 302, in _handle_collected_item
INTERNALERROR>     original_globals = item.obj.__globals__
INTERNALERROR>                        ^^^^^^^^^^^^^^^^^^^^
INTERNALERROR> AttributeError: 'NoneType' object has no attribute '__globals__'

============================ no tests ran in 0.01s =============================

Without --parallel-threads, the same project collects and runs cleanly.

Root cause

pytest_run_parallel/plugin.py:302 (v0.8.2):

if n_workers > 1 or n_iterations > 1:
    original_globals = item.obj.__globals__
    item.obj = wrap_function_parallel(item.obj, n_workers, n_iterations)
    for name in original_globals:
        if name not in item.obj.__globals__:
            item.obj.__globals__[name] = original_globals[name]

The plugin reaches into item.obj.__globals__ for every collected item once n_workers > 1 (or n_iterations > 1). For pytest.Function items, item.obj is the underlying Python function and __globals__ is its module globals. For sybil's SybilItem (and likely any non-Function collector that subclasses pytest.Item directly), item.obj is None, so the attribute access raises during the pytest_collection_finish hook and aborts the whole run.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions