Skip to content

[Feature Request] New beartype_conf option defining test-specific @beartype configurations 🤗 #21

@leycec

Description

@leycec

While demonstrably awesome, pytest-beartype currently enforces the default @beartype configuration on downstream users. That's... less awesome (albeit still demonstrable). Many third-party packages justifiably require custom @beartype configurations. @jorenham's super-fun optype package is one such. Since pytest-beartype is currently inapplicable there, pytest-beartype is probably inapplicable loads of other places too. My intestines groan audibly. This must be what "borborygmus" means.

In this feature request, I document a solution to my older self who is even balder. You rock, older self. Sorry about the hair. 👨‍🦲

What We Doin'

First, let's scope out the prospective solution I just cobbled together on the back of a bill I should probably be trying to pay instead of writing this:

# In "pyproject.toml":
[tool.pytest.ini_options]
beartype_conf = 'BeartypeConf(claw_is_pep526=False, is_debug=True)'

Reads sane, right? It's parsimonious, because it's literally valid Python code intended to by dynamically evaluated as such by pytest-beartype at plugin startup time. Well, almost valid Python code. BeartypeConf is undefined, obviously. Or is it!? We'll get to that shortly.

What We Ain't Doin'

Clearly, there are many semantically equivalent alternatives to expressing configuration like this in TOML. For example, we could instead opt for a pure-TOML solution whereby we literally define one unique TOML option for each corresponding BeartypeConf option (e.g., beartype_claw_is_pep526, beartype_conf_is_debug).

All of those semantically equivalent alternatives, though? Yeah. They're awful. Maintenance against the moving target that is the beartype.BeartypeConf API means constant breakage in pytest-beartype, defeating the whole point of QA in the first place: to solve bugs, not make them.

Thus, eval(). Whatevah. Everybody hates eval(), but I for one salute our new eval() overlords.


this, but eval()

How We Doin' This

Something like this should suffice for almost all possible use cases. Thus, this how we doin' this:

from beartype import BeartypeConf
from traceback import format_exc
from typing import Optional

beartype_conf = BeartypeConf()
'''
User-defined beartype configuration, defaulting to the default beartype configuration
in the common case the user fails to explicitly define a custom beartype configuration.
'''

_BEARTYPE_CONF_CODE_FORMATTER = '''
from beartype import (
    BeartypeConf,
    BeartypeDecorPlace,
    BeartypeStrategy,
    BeartypeViolationVerbosity,
    FrozenDict,
)

beartype_conf = {beartype_conf_code}
'''
'''
Pure-Python code snippet to format the user-defined ``"beartype_conf"`` code snippet into.
As a convenience, this snippet:

* Auto-imports *all* public :mod:`beartype` attributes of relevance to defining beartype
  configurations.
* Assigns the result to a deterministically named attribute under this plugin's control. 
'''

def pytest_configure(config: 'pytest.Config') -> None:
    global beartype_conf  # <-- fight me fam

    #FIXME: Gotta define a new get_pytest_option_str_or_none() getter, obviously.
    #Trivial. Trivial, I say! Somebody else do this for me, please. Gods...
    beartype_conf_code = get_pytest_option_str_or_none(
        config=config, option_name='beartype_conf')

    if beartype_conf_code:
        beartype_conf_globals = {}
        beartype_conf_locals = {}

        beartype_conf_code_formatted = _BEARTYPE_CONF_CODE_FORMATTER.format(
            beartype_conf_code=beartype_conf_code)

        try:
            beartype_conf_namespace = exec(
                beartype_conf_code_formatted,
                beartype_conf_globals,
                beartype_conf_locals,
            )
        except Exception as exception:
            raise PytestBeartypeOptionException(
                f'"pytest-beartype" option "beartype_conf" value '
                f'{repr(beartype_conf_code)} syntactically invalid:\n'
                f'{format_exc()}'
            ) from exception

        beartype_conf_value = beartype_conf_namespace['beartype_conf']

        if not isinstance(beartype_conf_value, BeartypeConf):
            raise PytestBeartypeOptionException(
                f'"pytest-beartype" option "beartype_conf" value '
                f'{repr(beartype_conf_code)} semantically invalid, as '
                f'{repr(beartype_conf)} not a beartype configuration '
                f'(i.e., "beartype.BeartypeConf" object).'
            )

        beartype_conf = beartype_conf_value

Looks brutal, but isn't. Mostly just validation to raise human-readable exceptions in the event the user defines an invalid BeartypeConf. The actual "real work" being done there is minimal at best and fun at most. Then just internally generalize this plugin's innards to pass around that global beartype_conf rather than assuming the default @beartype configuration as we currently do.

Success has now been achieved. This is how the QA was won. Not with words, but code! 🍸 🍷 🍺 🍻 <-- gods why so many alcoholism emoji

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions