pytest-dev / pytest-dev/pytest

TypeError: A Message class can only inherit from Message when using pytester and a per-test import google.api_core.exceptions

Open
#9,981 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

plugin: pytester type: bug
Dominant language
Python
Stars
14.5k
Forks
3.4k
Avg merge
2d 9h
Merged PRs (30d)
35

Description

Python: 3.10.2 and 3.9.12
pytest: 7.1.2
google-api-core: 2.8.0

Brief description: There seems to be some compatibility issue when using both pytester and google.api_core.exceptions in a case where google.api_core.exceptions is loaded after initially module import such as when loading by a factory creating an instance of an object requiring it.

Detailed Description:
If you have a test using a factory that itself imports a module importing google.api_core.exceptions, subsequent tests using the same factory will fail without applying one of the two workarounds mentioned in details below. Offhand, there seems to be state maintained by _api_implementation.cp310-win_amd64.pyd about which Message base is allowed which becomes mismatched to the actual current Message base class utilized in calls to instantiate... and this seems to only happen when using pytester (not that pytester is at fault).

I do find some other search results out there for this exception... it seems rare and affects certain products using python and related imports that occur during their use, pytest being another. I do not have a native env setup or debug build of google.api_core.exceptions, pytester, etc., so at this point in time, I am not yet sure if it's an issue with google.api_core.exceptions or pytest. I could dig further to see the nature of the interactions but wanted to check here first, see if any pytest/pytester experts may already know something on this.

Steps:

  1. Create Python venv for test.
  2. pip install pytest
  3. pip install google-api-core
  4. In test directory, create conftest.py with pytest_plugins = "pytester"
  5. In test directory, create test_example.py to hold the tests leading to repro of issue.
  6. Create an initial/first test as follows (this simplification sufficiently causes the same effect as a factory situation):
def test_example(
    pytester: Pytester, # Comment this out as a second workaround.
):
    import google.api_core.exceptions
  1. Create a second test as follows:
def test_example2():
    import google.api_core.exceptions # Fails with: TypeError: A Message class can only inherit from Message
  1. Run all the tests at once, in typical sequential order of test_example, test_example2. Observe test_example2 fails upon import of google.api_core.exceptions.

Known workarounds:

  • Do not use pytester in the first test (before the first load of google.api_core.exceptions).
  • Directly import google.api_core.exceptions at the top of test_example.py.

Additional information:
protobuf/pyext/message.cc has the following check believed to be throwing the exception. I suspect PythonMessage_class has changed given the other checks seem to match what is passed (see details/stacks further below).

  // Check bases: only (), or (message.Message,) are allowed
  if (!(PyTuple_GET_SIZE(bases) == 0 ||
        (PyTuple_GET_SIZE(bases) == 1 &&
         PyTuple_GET_ITEM(bases, 0) == PythonMessage_class))) {
    PyErr_SetString(PyExc_TypeError,
                    "A Message class can only inherit from Message");
    return nullptr;
  }

In the case where the issue occurs, the following two stacks occur, one for each test causing the import...

First, during 'test_example' test:

BuildMessage (.\side-tests-venv-3.10.2\Lib\site-packages\google\protobuf\internal\builder.py:85)
BuildTopDescriptorsAndMessages (.\side-tests-venv-3.10.2\Lib\site-packages\google\protobuf\internal\builder.py:108)
 (.\side-tests-venv-3.10.2\Lib\site-packages\google\protobuf\duration_pb2.py:19)
 (.\side-tests-venv-3.10.2\Lib\site-packages\google\rpc\error_details_pb2.py:31)
 (.\side-tests-venv-3.10.2\Lib\site-packages\google\api_core\exceptions.py:29)
test_example (.\side_tests\test_example.py:31)
pytest_pyfunc_call (.\side-tests-venv-3.10.2\Lib\site-packages\_pytest\python.py:192)
...

Message ID check in debugger:
id(_message.Message)
1995227571856

Second, during 'test_example2' test:

BuildMessage (.\side-tests-venv-3.10.2\Lib\site-packages\google\protobuf\internal\builder.py:85)
BuildTopDescriptorsAndMessages (.\side-tests-venv-3.10.2\Lib\site-packages\google\protobuf\internal\builder.py:108)
 (.\side-tests-venv-3.10.2\Lib\site-packages\google\protobuf\duration_pb2.py:19)
 (.\side-tests-venv-3.10.2\Lib\site-packages\google\rpc\error_details_pb2.py:31)
 (.\side-tests-venv-3.10.2\Lib\site-packages\google\api_core\exceptions.py:29)
test_example2 (.\side_tests\test_example.py:34)
pytest_pyfunc_call (.\side-tests-venv-3.10.2\Lib\site-packages\_pytest\python.py:192)
...

Message ID check in debugger:
id(_message.Message)
1995227645776

pytest --version

pytest 7.1.2

pip list

Package                  Version
------------------------ -----------
astroid                  2.11.5
atomicwrites             1.4.0
attrs                    21.4.0
cachetools               5.1.0
certifi                  2022.5.18.1
charset-normalizer       2.0.12
colorama                 0.4.4
dill                     0.3.5
google-api-core          2.8.0
google-auth              2.6.6
googleapis-common-protos 1.56.1
idna                     3.3
iniconfig                1.1.1
isort                    5.10.1
lazy-object-proxy        1.7.1
mccabe                   0.7.0
packaging                21.3
pip                      22.1
platformdirs             2.5.2
pluggy                   1.0.0
protobuf                 3.20.1
py                       1.11.0
pyasn1                   0.4.8
pyasn1-modules           0.2.8
pylint                   2.13.9
pyparsing                3.0.9
pytest                   7.1.2
requests                 2.27.1
rsa                      4.8
setuptools               58.1.0
six                      1.16.0
tomli                    2.0.1
urllib3                  1.26.9
wrapt                    1.14.1

conftest.py:

pytest_plugins = "pytester"

test_example.py:

# Setup:
# pip install pylint
# pip install google-api-core

# When this is run, the following exception occurs after
# the first import google.api_core.exceptions
# TypeError: A Message class can only inherit from Message

# conftest.py should contain the following:
# pytest_plugins = "pytester"

# One workarund is to uncomment the following (import exceptions directly).
# import google.api_core.exceptions

# Another workaround is to comment out pytester as an argument to the
# test *before* the first import google.api_core.exceptions. 
# The issue seems to be some mixture of a test using pytester while
# it imports google.api_core.exceptions for the first time.

# pylint: disable=unused-import,wrong-import-position
from pytest import (
    Pytester,
)

def test_example(
    pytester: Pytester, # Comment this out as a second workaround.
):
    # The real scenario had the test calling a factory function
    # in a module that itself imported google.api_core.exceptions.
    # Having each test import as follows is a simplfication of the original issue.
    import google.api_core.exceptions

def test_example2():
    import google.api_core.exceptions # Fails with: TypeError: A Message class can only inherit from Message

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the failure using the supplied conftest.py and test_example.py with pytester, Python 3.9 or 3.10, and the listed package versions. Start by reading pytester's fixture entry point and tracing the two imports of google.api_core.exceptions, then compare the differing _message.Message identities. Done means the ownership and cause of the import-state mismatch are established and a regression test can distinguish the two test sequences.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.