Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FAQ] Document how to disable @beartype #94

Closed
langfield opened this issue Jan 30, 2022 · 13 comments
Closed

[FAQ] Document how to disable @beartype #94

langfield opened this issue Jan 30, 2022 · 13 comments

Comments

@langfield
Copy link

langfield commented Jan 30, 2022

For example, do we ever intend it to detect the issue in either of these:

from beartype import beartype

@beartype
def fn(n: int) -> None:
    n = "waffle"

@beartype
def fn2() -> None: 
    n = 5
    n = "waffle"

Or is that out of the scope of the project?

Basically, I would like to be able to completely ditch mypy in favor of this project, and this is what's holding me back.

@leycec
Copy link
Member

leycec commented Jan 31, 2022

This question can be succinctly rephrased as follows:

Would we like to destroy mypy and every other static type checker?

The answer to this and many other questions is:

...yes. Why, yes. We would like to do that thing.

We sympathesize with your urge to dump mypy into a ditch while wearing vinyl gloves which you then quietly incinerate. Dumping mypy into a ditch is entirely in-scope for beartype. That's why we're here, after all – to realize that utopian dream we all share.

Basically, we regard:

  • Static type-checking as the ignoble past we all put up with because we have to.
  • Runtime type-checking as the Halcyon Golden Age we are rocketing towards at risky speeds that may not be sustainable, safe, or a good idea. But it sure is fun.

So How We Do This, Bro?

We go full AST on this mutha. I even have internal #FIXME: comments hidden somewhere within the beartype codebase documenting a probable route to pragmatically achieving this.

Of course, ASTs are a fathomless black hole from which few return alive and even fewer return sane. This may not necessarily happen within the expected lifetime of my life is what I'm saying. A drip-feed of corporate and/or governmental sponsorship would probably get us there sooner than later. Until Money Shangri La materializes, however, this is lower on the priority list than you'd like. Ugh!

tl;dr

Money. That's what can make this happen. 🤑

@leycec leycec changed the title Does/can beartype enforce static typing within the body of a function? [Feature Request] Optionally enforce static typing Jan 31, 2022
@matanox
Copy link

matanox commented Feb 6, 2022

Disclaimer: I got a little lost on the verbosity, and will nonetheless comment here to avoid potentially opening a duplicate, as this came up a little close in a search of the issues, but knowing that maybe my post in this comment is actually the opposite (or complementary aspect) of this request.

I'm not sure how to interpret the idea of static typing which is mentioned in this discussion, as I didn't really get its example code (apologies). I thought that since beartype is PEP compliant and all then when used in an IDE that does static type checking it does result in static type checking, when code is type checked by a static type checker such as in PyCharm or otherwise.

As to my own question or feature request, from reading the code to an extent, I may come to understand that if __debug__ is turned off using python's python -O mode, then the runtime checking is simply turned off. At least that's how I implemented my own similarly purposed numpy oriented code that I have created instead of using a library:

from typing import Dict, Callable, NamedTuple, Tuple
import numpy as np
import functools
import pytest

""" this module defins a decorator for functions that return a numpy tensor, a decorater that asserts the type, 
shape and arbitrary properties of a function's return value when the function is supposed to return a numpy tensor """

# (optional) dict of callables per tensor dimension, which may verify that dimension's length,
# which is a feature that for example allows to only verify some of the dimensions but not all.
ShapeSpec = Dict[int, Callable]


# verification specification type
class NPVerifySpec(NamedTuple):
    ndim: int = None               # number of dimensions
    dtype: np.dtype = None         # data type of the elements
    shape: Tuple[int, ...] = None  # length of each dimension
    shape_spec: ShapeSpec = None
    allow_none: bool = False


def np_verify_do(arr, spec: NPVerifySpec):

    # noinspection PyUnreachableCode
    if __debug__:

        print(spec)

        # allow a None return value
        if spec.allow_none and arr is None:
            return

        assert isinstance(arr, np.ndarray), f"{arr} is not a numpy array"

        if spec.ndim is not None:
            assert arr.ndim == spec.ndim, f'unexpected number of dimensions {arr.ndim}, expected {spec.ndim}'
        if spec.dtype is not None:
            assert arr.dtype == spec.dtype, f'unexpected dtype {arr.dtype}, expected {spec.dtype}'
        if spec.shape is not None:
            assert arr.shape == spec.shape, f'unexpected shape {arr.shape}, expected {spec.shape}'
        if spec.shape_spec is not None:
            for dim in spec.shape_spec:
                fn = spec.shape_spec[dim]
                assert fn(arr.shape[dim]) is True, f'dimension {dim} fails validation with validation function: {fn}'


def test_np_verify_do():
    arr = np.zeros((3, 4))
    # verify number of dimensions, data type, and length of each dimension
    np_verify_do(arr, NPVerifySpec(ndim = 2, dtype = np.float64, shape = (3, 4)))
    # verify that each dimension has length greater than 2
    np_verify_do(arr, NPVerifySpec(shape_spec = {0: lambda dim: dim > 2, 1: lambda dim: dim > 3}))



""" following is a decorator wrapper for the above np_verify function, and some tests demonstrating it
    (tecnique adopted from https://realpython.com/primer-on-python-decorators/#decorators-with-arguments) """


def np_verify(np_verify_spec: NPVerifySpec):
    def decorator(fn):
        @functools.wraps(fn)
        def with_output_verification(*args, **kwargs):
            result = fn(*args, **kwargs)
            np_verify_do(result, np_verify_spec)
            return result
        return with_output_verification
    return decorator


def test_np_verify_positive():
    """ verifies that verification via the decorator succeeds when it should """
    @np_verify(NPVerifySpec(ndim=2, dtype=np.float64, shape_spec={0: lambda dim: dim > 2, 1: lambda dim: dim > 3}))
    def foo():
        return np.zeros((3, 4))

    foo()


def test_np_verify_negative():
    """ verifies that verification via the decorator fails when it should """
    with pytest.raises(AssertionError):
        @np_verify(NPVerifySpec(ndim=10))
        def bar():
            return np.zeros((3, 4))

        bar()

Anyway, this code does nothing that beartype doesn't already allow with its Is thing. I think that beartype behaves like I suggest above, regarding its behavior with regard to the python __debug__ flag.

So my question or feature request is twofold:

  1. Will python's -O flag skip runtime checking and reduce its cost to an empty function call?
  2. Is there any sense in adding a finer resolution way of turning off runtime checking, such as turning it off for some places/types/functions and not others? this would correspond to some scenarios mentioned in the docs/issues that speak to the need to sanitize runtime external input if you are an api project v.s. avoiding checking each and every other object in the api code. In short, there may seem to be some reasons for controlling very flexibly, the extent of checking, during a development and release process, in ways that do not require removing the annotations.

Glad to learn.

@TeamSpen210
Copy link

If -O is set, @beartype will indeed do absolutely nothing at all. Stuff will still end up imported, and annotations get evaluated, but that shouldn't be too significant. Since it's just a decorator run at runtime, you have the full power of Python in regards to where you apply it. If you wanted to only sometimes apply it to your library, conditionally either import it or define a dummy in one of your modules.

@matanox
Copy link

matanox commented Feb 9, 2022

Actually if annotations get evaluated when __debug__ is False it is significant with large data. Doesn't -O make beartype avoid itself altogether? Removing runtime type checking for making a release by going function by function in user code is not very desirable as part of the value proposition in this category.

@TeamSpen210
Copy link

What I mean by the annotations being evaluated is that as part of the function definition, the Union[str, dict[bool, float]] code would be run. That's not really something beartype can affect, it's all syntax. You can use from __future__ import annotations to make them strings again, but that might not work in all cases (since beartype has to call eval() and try to recreate the globals/locals environment). This is kinda an open problem, PEP 563 and PEP 649 are kinda competing.

@leycec
Copy link
Member

leycec commented Feb 12, 2022

Superb repeated saves by @TeamSpen210 – as expected of the greatest issue outfielder of all time.

So, we've dramatically leapt off of the original issue of static type checking and are now hip-deep in tangentially unrelated but nonetheless interesting waters: namely, "How do I curbstomp @beartype from doing what it wants to do?"

Let's see if we can't incrementally unpack this Gordion knot.

Will python's -O flag skip runtime checking and reduce its cost to an empty function call?

Yup. This is happening already.

Is there any sense in adding a finer resolution way of turning off runtime checking, such as turning it off for some places/types/functions and not others?

Yup. This is happening already. Specifically, our new configuration API released under beartype 0.10.0 and only documented here currently (just 'cause) provides a new O(0) no-time type-checking strategy that selectively disables specific @beartype decorations enabling that strategy.

Example or it didn't happen, so:

# Import the requisite machinery.
from beartype import (
    beartype,
    BeartypeConf,
    BeartypeStrategy,
)

# Global runtime type-checking strategy, where
# "I_AM_A_RELEASE_BUILD" is externally defined
# by your app stack to be True for release builds.
# Specifically:
# * If a release build, reduce @beartype to a noop
#   via the no-time type-checking strategy.
# * If a debug build, default @beartype to the usual
#   constant-time type-checking strategy.
_beartype_strategy = (
    BeartypeStrategy.O0
    if I_AM_A_RELEASE_BUILD else
    BeartypeStrategy.O1
)

# Beartype decorator configured with this strategy.
# Use this rather than the vanilla @beartype
# decorator everywhere throughout your app stack.
goodbeartype = beartype(
    conf=BeartypeConf(_beartype_strategy))

# Use @goodbeartype rather than @beartype everywhere.
@goodbeartype
def muh_performance_critical_func(big_list: list[int]) -> int:
    return len(big_list)

That's actually useful fodder for a new FAQ entry. 🤔

Of course, it's probably pertinent to note here that @beartype is so fast and adds so little runtime overhead that it will never be a performance bottleneck – unless you're implementing a real-time digital signal processing operating system in pure-Python, in which case I concede the point.

Seriously. We're unbelievably fast. Profile us if you don't believe us – which you shouldn't, because your app stack depends on us telling the truth. Never trust anyone. Not even me.

Actually if annotations get evaluated when __debug__ is False it is significant with large data.

Fascinating. Absolutely no snark here, because customer is king. You are customer, ergo king. Still... Have you actually profiled annotations as consuming non-trivial space for your use case or is this a bit of worst-case theory-crafting?

The reason I ask is that type hints originating from the standard typing module are self-memoizing and thus extremely space-efficient. For example, if you repeatedly annotate callables with the typing.List[str] type hint, only one instance of that hint is instantiated for the lifetime of the active Python process: e.g.,

>>> import typing
>>> typing.List[str] is typing.List[str]
True

Of course, PEP 585 deprecates typing.List by list. Is list[str] similarly self-caching? Nope. CPython devs completely dropped the ball here, because they're replacing something good (i.e., self-memoizing PEP 484 type hints) with something less good (i.e., non-self-memoizing PEP 585 type hints):

>>> list[str] is list[str]
False

You are now possibly cogitating: "Well, this is all a great ball of steaming matter of a nondescript nature." You're not wrong. Fortunately, @beartype solves that issue, too. Internally, @beartype caches all non-self-memoizing PEP 585 type hints. So, when you use @beartype, there's actually no space costs for either PEP 484 or 585 type hints.

Great, right? Almost. Remember that no-time type-checking strategy we enabled above? Okay. When we say "no-time," we mean literally that. @beartype immediately reduces to a noop when configured by that strategy. This means it does nothing, because doing something would violate the meaning of "no-time," right?

Unfortunately, doing something includes (wait for it) caching all non-self-memoizing PEP 585 type hints. That no longer happens, so PEP 585 type hints will suddenly begin consuming space when you disable @beartype.

Of course... that's why you shouldn't disable @beartype unless you're certain you need to. Again, please profile @beartype against your usual real-world use case. If we're surprisingly slow, we'll do our utmost to resolve that for you and the world.

Back to the question at hand. Let's suppose you actually have profiled:

  • @beartype and it's stupidly slow, so you need to disable it with the above no-time strategy in release builds.
  • Annotations and they're stupidly big, so you also need to disable them.

One or both of these are probably not going to be the case. But let's suppose they are. In other words, we are now on the worst historical timeline and the Four Arthritic Horsemen of the apocalypse are now rearing outside your bedroom window.

Can you do anything about that? You can – but you really don't want to. As @TeamSpen210 yet again correctly suggests, you can manually litter the top of every Python module across your entire app stack with a futures import enabling PEP 563:

from __future__ import annotations

Please don't do that. Although @beartype technically is the most PEP 563-compliant runtime type checker, enabling PEP 563 will:

  • (A) Break everything else that depends on annotations (e.g., pydantic).
  • (B) Introduce weird corner case subtleties that could conceivably break @beartype (when you do actually run it). Nested callables and closures are especially susceptible to breaking here.
  • (C) Dramatically slow down @beartype (when you do actually run it).

tl;dr

Please profile. We promise you'll be pleasantly surprised.

@TeamSpen210
Copy link

One thing I did notice while looking through the __debug__ handling is that since all of beartype is contained in submodules, you could move the __debug__ check into beartype.__init__, and then only import the actual core components if __debug__ is off. Then in "optimised" mode, most of beartype won't even need to be loaded, probably just the config if anything. It'd probably be negligible, but might as well.

@leycec
Copy link
Member

leycec commented Feb 12, 2022

That's... an unsurprisingly fantastic idea! But of course it is. It's Spen's. Type-checking QA 🐻 approves. This is clever, because @beartype internally consumes space due to various internal caches and frozen containers – not much, we swear! But still... Doing away with that when unneeded sounds swell.

I'll add an internal FIXME: comment to that effect straightaway.

leycec added a commit that referenced this issue Feb 13, 2022
This commit is the next in a commit chain documenting everything added
to the excessively corpulent `beartype 0.10.0` release cycle but left
undocument due to widespread laziness, corruption, and vice in the
@beartype organization. Specifically, this commit introduces our
functional API (i.e., `beartype.abby.is_bearable()`,
`beartype.abby.die_if_unbearable()`) in the leading example of our
front-facing `README.rst` file. Unrelatedly, this commit also internally
details several improvements to the design of our core `@beartype`
decorator – all thanks to incisive commentary at issue #94 by the
quasi-omniscient HAL-like entity known only as @TeamSpen210.
(*Unfathomable fetters of a feathered mobile!*)
@leycec
Copy link
Member

leycec commented Feb 23, 2022

Let's rename this issue to something I can actually accomplish within a single lifetime. 🥲

@leycec leycec changed the title [Feature Request] Optionally enforce static typing [Speed] Optimize "-O" optimization + [FAQ] Document how to disable @beartype Feb 23, 2022
@langfield
Copy link
Author

langfield commented Feb 23, 2022 via email

@leycec
Copy link
Member

leycec commented Feb 24, 2022

It makes sense, because you always do. Let's copy-and-paste my initial response elsewhere to avoid violating DRY... yet again. Ain't nobody got the money to repeat a money-grubbing rant. Once is enough, GitHub!

leycec added a commit that referenced this issue Mar 12, 2022
This commit improves compatibility with recently released mypy 0.940,
resolving issues #111 and #112 dual-reported concurrently by
cutting-edge Microsoft luminary @daxpryce and German typing bad-ass
@twoertwein. Specifically, this commit fundamentally refactors (and in
so doing mildly optimizes) our private `beartype._decor.main` submodule
to leverage conditional overloads under the astute tutelage
of mypy maestro @cdce8p; the `@beartype.beartype` decorator itself now
resides in a new private
`beartype._decor.cache.decor.cachedecor` submodule, because obfuscation
is the key to all successful open-source efforts. Relatedly, this
commit also partially resolves issue #94 (kindly requested by typing
monster @matanster) by optimizing the definition of the
`@beartype.beartype` decorator when that decorator reduces to a noop
(e.g., due to `python3 -O` optimization). Unrelatedly, this commit also
resolves a critical defect in our new functional API (i.e., the pair of
`beartype.abby.is_bearable()` and `beartype.abby.die_if_unbearable()`
functions), which previously reduced to a noop when the
`@beartype.beartype` decorator reduced to a noop; extricating the
`@beartype.beartype` decorator into the
`beartype._decor.cache.decor.cachedecor` submodule enables our
functional API to unconditionally depend upon that decorator regardless
of what we present to external users. Thanks so much for the rapid
response time, everyone! `beartype 0.10.4` will be en-route to a
cheeseshop near you shortly. (*Incorrigibly durable didgeridoo!*)
@leycec
Copy link
Member

leycec commented Mar 12, 2022

Partially resolved by ccac042. By following the spun gold of @TeamSpen210's prior comment, we've mildly optimized the @beartype decorator under -O optimization to avoid importing anything unnecessary (which is everything).

We still have yet to actually document any of this, however. Let's do that this year, shall we then? 😓

@leycec leycec changed the title [Speed] Optimize "-O" optimization + [FAQ] Document how to disable @beartype [FAQ] Document how to disable @beartype Mar 12, 2022
leycec added a commit that referenced this issue Mar 15, 2022
This patch release adumbrates with breathless support for **mypy ≥
0.940,** the static type checker formerly known as "The Static Type
Checker Whose Name Shall not Be Spoken."

This patch release resolves **5 issues** and merges **0 pull requests.**
Noteworthy changes include:

## Compatibility Improved

* **mypy ≥ 0.940.** The `beartype` codebase now sports improved
  compatibility with recently released mypy 0.94x series, which
  previously outted the `@beartype` decorator with a "Condition can't be
  inferred, unable to merge overloads [misc]" fatal error at static
  type-checking time. Specifically, this commit fundamentally refactors
  (and in so doing mildly optimizes) our private `beartype._decor.main`
  submodule to leverage conditional overloads under the astute tutelage
  of mypy maestro @cdce8p; the `@beartype.beartype` decorator itself now
  resides in a new private `beartype._decor.cache.cachedecor` submodule,
  because obfuscation is the key to all successful open-source efforts.
  Doing so resolves issues #111 and #112 dual-reported concurrently by
  cutting-edge Microsoft luminary @daxpryce and German typing bad-ass
  @twoertwein.

## Features Optimized

* **Importation time.** The first external importation from the
  `beartype` codebase is now significantly faster when the definition of
  the `@beartype.beartype` decorator reduces to a noop (e.g., due to
  `python3 -O` optimization), partially resolves issue #94 kindly
  requested by the well-tanned and -toned typing star @matanster.

## Issues Resolved

* **`beartype.abby` under `python3 -O`.** This release resolves an
  unreported critical defect in our new functional API (i.e., the pair
  of `beartype.abby.is_bearable()` and
  `beartype.abby.die_if_unbearable()` functions), which previously
  reduced to a noop when the `@beartype.beartype` decorator reduced to a
  noop (e.g., due to `python3 -O` optimization). By extricating the
  `@beartype.beartype` decorator into the
  `beartype._decor.cache.cachedecor` submodule (as described above),
  our functional API now directly defers to that decorator regardless of
  what the `beartype` package externally presents to third-party code.

(*Ironwrought irony untaught!*)
@leycec
Copy link
Member

leycec commented Nov 4, 2022

Resolved by f1e0cdc. The beartype configuration API is now exhaustively documented. I know this, because I'm exhausted. Relevantly, the beartype.BeartypeStrategy.O0 strategy is now documented. As suggested above, the no-time strategy trivializes dynamic disabling of @beartype:

# Import the requisite machinery.
from beartype import (
    beartype,
    BeartypeConf,
    BeartypeStrategy,
)

# Global runtime type-checking strategy, where
# "I_AM_A_RELEASE_BUILD" is externally defined
# by your app stack to be True for release builds.
# Specifically:
# * If a release build, reduce @beartype to a noop
#   via the no-time type-checking strategy.
# * If a debug build, default @beartype to the usual
#   constant-time type-checking strategy.
_beartype_strategy = (
    BeartypeStrategy.O0
    if I_AM_A_RELEASE_BUILD else
    BeartypeStrategy.O1
)

# Beartype decorator configured with this strategy.
# Use this rather than the vanilla @beartype
# decorator everywhere throughout your app stack.
goodbeartype = beartype(
    conf=BeartypeConf(_beartype_strategy))

# Use @goodbeartype rather than @beartype everywhere.
@goodbeartype
def muh_performance_critical_func(big_list: list[int]) -> int:
    return len(big_list)

Let's close out this ancient issue to widespread jubilation. 🥳

@leycec leycec closed this as completed Nov 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants