2

I am trying to use pytest to test some code that uses towerlib, an external Python package. Part of the functionality is that towerlib will dynamically add different attributes to its credential.py module, and then the class imports its own module and looks for that attribute:

This is in towerlib.entities.credential.py:

class Credential:  # pylint: disable=too-few-public-methods
    """Credential factory to handle the different credential types returned."""

    def __new__(cls, tower_instance, data):
        try:
            credential_type_name = tower_instance.get_credential_type_by_id(data.get('credential_type')).name
            credential_type_name = ''.join(credential_type_name.split())
            credential_type = f'{credential_type_name}Credential'
            credential_type_obj = getattr(importlib.import_module('towerlib.entities.credential'), credential_type)
            credential = credential_type_obj(tower_instance, data)
        except Exception:  # pylint: disable=broad-except
            LOGGER.exception(
                'Could not dynamically load credential with type : "%s", trying a generic one.', credential_type
            )
            credential = GenericCredential(tower_instance, data)
        return credential

I assume this pytest does some sort of caching or other behavior that doesn't get updated when the module dynamically adds attributes to itself and reimports itself. Does anyone know if that's how pytest works, and if that's configurable with a flag or decorator on my test?

The function that I'm trying to test works correctly when run outside of pytest, but when running it in a pytest function, I'm getting the "Could not dynamically load credential with type" exception.

Running in the REPL:

>>> e2e.prepare_and_run_e2e(["my_device"], None, None, False, True, True, False, True)

^this returns a full results set with no errors.

My pytest function:

@pytest.mark.parametrize("device", get_all_e2e_devices())
def test_e2e(device):
    results = e2e.prepare_and_run_e2e([device], None, None, False, True, True, False, True)
    ...

Ends up with:

ERROR    credentials:credential.py:189 Could not dynamically load credential with type : "NetworkCredential", trying a generic one.
Traceback (most recent call last):
  File "{my local path}\.venv\lib\site-packages\towerlib\entities\credential.py", line 186, in __new__
    credential_type_obj = getattr(importlib.import_module('towerlib.entities.credential'), credential_type)
AttributeError: module 'towerlib.entities.credential' has no attribute 'NetworkCredential'

I tried running pytest with -p no:cacheprovider, but that gives me the same result.

I don't want to patch/mock around the Credential because this is a special test in my application, and I want the test to actually get the towerlib Credential and talk to an external system. I also don't have the ability to change the somewhat confusing approach that towerlib is taking, since it's an external library, and I need to share this test across a team that will just be using the standard towerlib.

β.εηοιτ.βε
  • 33,893
  • 13
  • 69
  • 83
pythko
  • 114
  • 7
  • Can you update your question to include the test code along with the complete exception you're receiving? – Teejay Bruno Feb 16 '23 at 17:22
  • 1
    Thanks! Have you validated your assumption by trying with the cache disabled? This can be done via the cmd line arg `-p no:cacheprovider`. If this works, you could modify the specific cache entry via a fixture to include in your test. – Teejay Bruno Feb 16 '23 at 19:07
  • I just tried that, and I got the same failed test result. I updated the body of my question to mention I tried that. – pythko Feb 16 '23 at 19:28
  • Then the cache is not affecting the test. Have you tried inspecting the `device` object from your successful repl call and the failed pytest run to verify they're the same? – Teejay Bruno Feb 16 '23 at 19:32
  • 1
    It's probably not this but here it goes: Do you have your test file inside a directory structure matching `towerlib/entities/credential`? (as in: Do you have your working code under `towerlib/entities/credential` and your tests under something like `test/towerlib/entities/credential`?) I'm asking because PyTest will add directories "up" to the PYTHONPATH starting on your test file. So if you have your "workable" structure mirrored in the test structure, maaaybe you could be importing the module under `test/...` rather than the actual implementation? Maybe? (I know I've had this issue) – Savir Feb 16 '23 at 19:39
  • 1
    My test file structure is different from the towerlib structure, so I don't think that's it @BorrajaX, but thanks for the suggestion! @Teejay, the device parameter I'm passing in is just a string that is used to look up a different module in my code, and then test that module. I've seen some other questions where `importlib.import_module` doesn't seem to work exactly like people want in pytest, but those askers seem to have had the option to rewrite their code to not use `importlib.import_module` – pythko Feb 17 '23 at 14:02

0 Answers0