Skip to content

Commit

Permalink
${PYTHONOPTIMIZED} runtime detection.
Browse files Browse the repository at this point in the history
This commit generalizes @beartype's detection of Python optimization to
dynamically detect runtime changes by the external user (e.g., from
within an interactive REPL) to the `${PYTHONOPTIMIZED}` environment
variable governing Python optimization, resolving feature request #341
kindly submitted by global healthcare big brain @jaanli (Jaan Lı 李).
Notably, the `@beartype` decorator now silently reduces to a noop (i.e.,
avoids decorating things with runtime type-checking) when external users
manually set the `${PYTHONOPTIMIZED}` environment variable to a non-zero
integer from within REPLs like Jupyter. Of course, no one should do
that. Of course, everyone will now begin doing that. (*Centripetal force on centrifugal fungi!*)
  • Loading branch information
leycec committed Mar 16, 2024
1 parent 9f227bd commit a342229
Show file tree
Hide file tree
Showing 16 changed files with 218 additions and 38 deletions.
4 changes: 2 additions & 2 deletions beartype/_decor/_decornontype.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@
from beartype._decor.wrap.wrapmain import generate_code
from beartype._util.cache.pool.utilcachepoolobjecttyped import (
release_object_typed)
from beartype._util.func.mod.utilbeartypefunc import (
from beartype._util.func.module.utilfuncmodbear import (
is_func_unbeartypeable,
set_func_beartyped,
)
from beartype._util.func.mod.utilfuncmodtest import (
from beartype._util.func.module.utilfuncmodtest import (
is_func_contextlib_contextmanager,
is_func_functools_lru_cache,
)
Expand Down
12 changes: 7 additions & 5 deletions beartype/_decor/_decortype.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,12 @@ class variable or method annotated by this hint *or* :data:`None`).
# concrete "enum.Enum" subclasses with @beartype once provoked
# infinite recursion. Why? Because:
#
# * *All* :class:`enum.Enum` subclasses define a private
# "_member_type_" attribute whose value is the "object"
# superclass, which @beartype then decorated.
# * However, the :class:`object` superclass defines the "__class__"
# dunder attribute whose value is the "type" superclass, which
# * *ALL* "enum.Enum" subclasses define a private "_member_type_"
# attribute whose value is the "object" superclass, which
# @beartype then decorated.
# * However, the "object" superclass defines the "__class__" dunder
# attribute whose value is the "type" superclass, which @beartype
# then decorated.
# * However, the "type" superclass defines the "__base__" dunder
# attribute whose value is the "object" superclass, which
# @beartype then decorated.
Expand Down Expand Up @@ -271,6 +271,8 @@ class variable or method annotated by this hint *or* :data:`None`).

# Replace this undecorated attribute with this decorated attribute.
set_type_attr(cls, attr_name, attr_value_beartyped)
# print(f'Decorating {repr(cls)} attribute "{attr_name}"...')
# print(f'type: {type(attr_value)}; dir: {dir(attr_value)}')
# Else, this attribute is *NOT* beartypeable. In this case, silently
# ignore this attribute.

Expand Down
9 changes: 7 additions & 2 deletions beartype/_decor/decormain.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
BeartypeReturn,
BeartypeableT,
)
from beartype._util.py.utilpyinterpreter import is_python_optimized

# Intentionally import the standard mypy-friendly @typing.overload decorator
# rather than a possibly mypy-unfriendly @beartype.typing.overload decorator --
Expand Down Expand Up @@ -77,8 +78,12 @@ def beartype(*, conf: BeartypeConf) -> Callable[
# runtime context under which @beartype is only ever run. Nonetheless, this
# test is only performed once per process and is thus effectively free.
TYPE_CHECKING or
# Optimized (e.g., option "-O" was passed to this interpreter) *OR*...
not __debug__
# Optimized at process invocation time (e.g., at least one "-O" command-line
# option was set when this interpreter forked) *OR*...
not __debug__ or
# Optimized *AFTER* process invocation time (e.g., in an interactive REPL by
# the external user). Yes, our awesome userbase actually requested this.
is_python_optimized()
):
# Then unconditionally disable @beartype-based type-checking across the entire
# codebase by reducing the @beartype decorator to the identity decorator.
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
is_func_pep484_notypechecked)
from beartype._util.func.utilfuncget import get_func_annotations_or_none
from beartype._util.module.lib.utilsphinx import is_sphinx_autodocing
from beartype._util.py.utilpyinterpreter import is_python_optimized
from collections.abc import Callable

# ....................{ TESTERS }....................
Expand All @@ -33,7 +34,7 @@ def is_func_unbeartypeable(func: Callable) -> bool:
Callable to be inspected.
Returns
----------
-------
bool
:data:`True` only if that callable is unbeartypeable.
'''
Expand All @@ -48,6 +49,11 @@ def is_func_unbeartypeable(func: Callable) -> bool:
# That callable is a @beartype-specific wrapper previously generated by
# this decorator *OR*...
is_func_beartyped(func) or
# The active Python process was optimized *AFTER* process invocation
# time (e.g., in an interactive REPL by the external user manually
# setting the ${PYTHONOPTIMIZED} environment variable to a non-zero
# integer) *OR*...
is_python_optimized() or
# Sphinx is currently autogenerating documentation (i.e., if this
# decorator has been called from a Python call stack invoked by the
# "autodoc" extension bundled with the optional third-party build-time
Expand Down Expand Up @@ -75,7 +81,7 @@ def is_func_beartyped(func: Callable) -> bool:
Callable to be inspected.
Returns
----------
-------
bool
:data:`True` only if that callable is a beartype-generated wrapper
function.
Expand Down
File renamed without changes.
6 changes: 3 additions & 3 deletions beartype/_util/os/utilosshell.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@
parent shell does *not* define this variable).
Caveats
----------
-------
**This getter is a human-readable alias of the comparable**
:func:`os.getenv` **function and** :meth:`os.environ.get` **method.** This
getter exists only for disambiguity and clarity. This getter is *not* an alias
of the :meth:`os.environ.__getitem__` dunder method, which raises a
:exc:`KeyError` exception rather than returns :data:`None` if the parent shell
does *not* define this variable.
fails to define this variable.
See Also
----------
--------
https://stackoverflow.com/a/41626355/2809027
StackOverflow answer strongly inspiring this alias.
'''
63 changes: 62 additions & 1 deletion beartype/_util/py/utilpyinterpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

# ....................{ TESTERS }....................
@callable_cached
def is_py_pypy() -> bool:
def is_python_pypy() -> bool:
'''
:data:`True` only if the active Python interpreter is **PyPy**.
Expand All @@ -29,6 +29,67 @@ def is_py_pypy() -> bool:

return python_implementation() == 'PyPy'



def is_python_optimized() -> bool:
'''
:data:`True` only if the active Python interpreter is currently
**optimized** (i.e., either the current Python process was invoked with at
least one ``-O`` command-line option *or* the ``${PYTHONOPTIMIZE}``
environment variable is currently set to a non-zero integer).
This tester is intentionally *not* memoized (e.g., by the
``@callable_cached`` decorator), as doing so would prevent this tester from
detecting dynamic changes to the ``PYTHONOPTIMIZE`` environment variable
manually applied by the external user. Technically, Python itself detects
*no* such changes. Pragmatically, there's *no* demonstrable justification
for :mod:`beartype` itself to behave similarly; since testing environment
variable values is both trivial *and* yields a better outcome for users,
this tester does so. Indeed, our userbase `explicitly requested that we do
so <beartype issue_>`__.
.. _beartype issue:
https://github.com/beartype/beartype/issues/341
'''

# If Python disabled the "__debug__" dunder global, either the current
# Python process was invoked with at least one ``-O`` command-line option
# *OR* the "${PYTHONOPTIMIZE}" environment variable was set to a non-zero
# integer at process invocation time. In either case, return true.
if not __debug__: # pragma: no cover
return True
# Else, Python enabled the "__debug__" dunder global. Although the
# "${PYTHONOPTIMIZE}" environment variable was *NOT* set to a non-zero
# integer at process invocation time, that variable *COULD* have since been
# set by the external user (e.g., in an interactive REPL). Let's decide.

# Avoid circular import dependencies.
from beartype._util.os.utilosshell import get_shell_var_value_or_none

# String value of this environment variable if set *OR* "None" otherwise.
PYTHONOPTIMIZE_str = get_shell_var_value_or_none('PYTHONOPTIMIZE')

# If this environment variable is set...
if PYTHONOPTIMIZE_str is not None:
# print(f'Detecting ${{PYTHONOPTIMIZE}} value {PYTHONOPTIMIZE_str}...')

# Attempt to coerce this string into an integer.
try:
PYTHONOPTIMIZE_int = int(PYTHONOPTIMIZE_str)
# If doing so raises *ANY* exception whatsoever, return false.
except:
return False

# If this integer is non-zero, this environment variable has since been
# set to a non-zero integer by the user. In this case, return true.
if PYTHONOPTIMIZE_int > 0:
return True
# Else, this environment variable remains zeroed and thus disabled.
# Else, this environment variable is unset.

# Return false as a fallback.
return False

# ....................{ GETTERS ~ path }....................
@callable_cached
def get_interpreter_command_words() -> CommandWords:
Expand Down
10 changes: 5 additions & 5 deletions beartype/_util/py/utilpyword.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@
# ....................{ BOOLEANS }....................
IS_WORD_SIZE_64 = maxsize > SHORT_MAX_32_BIT
'''
``True`` only if the active Python interpreter is **64-bit** (i.e., was
:data:`True` only if the active Python interpreter is **64-bit** (i.e., was
compiled with a 64-bit toolchain into a 64-bit executable).
Equivalently, this is ``True`` only if the maximum value of Python shorts under
this interpreter is larger than the maximum value of 32-bit Python shorts.
While obtuse, this test is well-recognized by the Python community as the
best means of testing this portably. Valid but worse alternatives include:
Equivalently, this is :data:`True` only if the maximum value of Python shorts
under this interpreter is larger than the maximum value of 32-bit Python shorts.
While obtuse, this test is well-recognized by the Python community as the best
means of testing this portably. Valid but worse alternatives include:
* ``'PROCESSOR_ARCHITEW6432' in os.environ``, which depends upon optional
environment variables and hence is clearly unreliable.
Expand Down
4 changes: 2 additions & 2 deletions beartype_test/_util/mark/pytskip.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,10 @@ def skip_if_pypy():
'''

# Defer test-specific imports.
from beartype._util.py.utilpyinterpreter import is_py_pypy
from beartype._util.py.utilpyinterpreter import is_python_pypy

# Skip this test if the active Python interpreter is PyPy.
return skip_if(is_py_pypy(), reason='Incompatible with PyPy.')
return skip_if(is_python_pypy(), reason='Incompatible with PyPy.')


def skip_if_python_version_greater_than_or_equal_to(version: str):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
**Beartype-generated wrapper function utility unit tests.**
This submodule unit tests the public API of the private
:mod:`beartype._util.utilfunc.lib.utilbeartypefunc` submodule.
:mod:`beartype._util.utilfunc.lib.utilfuncmodbear` submodule.
'''

# ....................{ IMPORTS }....................
Expand All @@ -20,12 +20,12 @@
def test_is_func_beartyped() -> None:
'''
Test the
:func:`beartype._util.func.mod.utilbeartypefunc.is_func_beartyped` tester.
:func:`beartype._util.func.module.utilfuncmodbear.is_func_beartyped` tester.
'''

# Defer test-specific imports.
from beartype import beartype
from beartype._util.func.mod.utilbeartypefunc import is_func_beartyped
from beartype._util.func.module.utilfuncmodbear import is_func_beartyped

@beartype
def where_that_or() -> str:
Expand Down Expand Up @@ -54,11 +54,11 @@ def thou_art_no_unbidden_guest() -> str:
def test_set_func_beartyped() -> None:
'''
Test the
:func:`beartype._util.func.mod.utilbeartypefunc.set_func_beartyped` tester.
:func:`beartype._util.func.module.utilfuncmodbear.set_func_beartyped` tester.
'''

# Defer test-specific imports.
from beartype._util.func.mod.utilbeartypefunc import (
from beartype._util.func.module.utilfuncmodbear import (
is_func_beartyped,
set_func_beartyped,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
def test_is_func_contextlib_contextmanager() -> None:
'''
Test the
:func:`beartype._util.func.mod.utilfuncmodtest.is_func_contextlib_contextmanager`
:func:`beartype._util.func.module.utilfuncmodtest.is_func_contextlib_contextmanager`
tester.
'''

# Defer test-specific imports.
from beartype._util.func.mod.utilfuncmodtest import (
from beartype._util.func.module.utilfuncmodtest import (
is_func_contextlib_contextmanager)
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_11
from beartype_test.a00_unit.data.data_type import (
Expand Down Expand Up @@ -62,12 +62,12 @@ def test_is_func_contextlib_contextmanager() -> None:
def test_is_func_functools_lru_cache() -> None:
'''
Test the
:func:`beartype._util.func.mod.utilfuncmodtest.is_func_functools_lru_cache`
:func:`beartype._util.func.module.utilfuncmodtest.is_func_functools_lru_cache`
tester.
'''

# Defer test-specific imports.
from beartype._util.func.mod.utilfuncmodtest import (
from beartype._util.func.module.utilfuncmodtest import (
is_func_functools_lru_cache)
from beartype_test.a00_unit.data.data_type import (
lru_cache_func,
Expand Down
52 changes: 48 additions & 4 deletions beartype_test/a00_unit/a20_util/py/test_utilpyinterpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,62 @@
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

# ....................{ TESTS ~ testers }....................
def test_is_py_pypy() -> None:
def test_is_python_pypy() -> None:
'''
Test the :func:`beartype._util.py.utilpyinterpreter.is_py_pypy` tester.
Test the :func:`beartype._util.py.utilpyinterpreter.is_python_pypy` tester.
'''

# Defer test-specific imports.
from beartype._util.py.utilpyinterpreter import is_py_pypy
from beartype._util.py.utilpyinterpreter import is_python_pypy

# Assert this tester returns a boolean.
IS_PY_PYPY = is_py_pypy()
IS_PY_PYPY = is_python_pypy()
assert isinstance(IS_PY_PYPY, bool)


def test_is_python_optimized(monkeypatch: 'pytest.MonkeyPatch') -> None:
'''
Test the :func:`beartype._util.py.utilpyinterpreter.is_python_optimized`
tester.
Parameters
----------
monkeypatch : MonkeyPatch
:mod:`pytest` fixture allowing various state associated with the active
Python process to be temporarily changed for the duration of this test.
'''

# ....................{ IMPORTS }....................
# Defer test-specific imports.
from beartype._util.py.utilpyinterpreter import is_python_optimized

# ....................{ ASSERT }....................
# Assert that the active Python interpreter is currently unoptimized. In
# theory, tests should *ALWAYS* be run unoptimized. Why? Because
# optimization destructively elides away (i.e., silently reduces to noops)
# all "assert" statements, obstructing testing.
assert is_python_optimized() is False

# Temporarily zero the ${PYTHONOPTIMIZE} environment variable.
monkeypatch.setenv('PYTHONOPTIMIZE', '0')

# Assert that the active Python interpreter remains unoptimized.
assert is_python_optimized() is False

# Temporarily set this variable to an arbitrary positive integer.
monkeypatch.setenv('PYTHONOPTIMIZE', '1')

# Assert that the active Python interpreter is now kinda "optimized."
assert is_python_optimized() is True

# Temporarily set this variable to an arbitrary string that *CANNOT* be
# coerced into an integer.
monkeypatch.setenv('PYTHONOPTIMIZE', (
'Bright in the lustre of their own fond joy.'))

# Assert that the active Python interpreter is yet again unoptimized.
assert is_python_optimized() is False

# ....................{ TESTS ~ getters }....................
def test_get_interpreter_command() -> None:
'''
Expand Down
2 changes: 1 addition & 1 deletion beartype_test/a00_unit/a40_api/conf/test_confcls.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ def __bool__(self) -> bool:
# ....................{ TESTS ~ arg }....................
def test_conf_is_color(monkeypatch: 'pytest.MonkeyPatch') -> None:
'''
Test the``is_color`` parameter accepted by the
Test the ``is_color`` parameter accepted by the
:class:`beartype.BeartypeConf` class with respect to the external
``${BEARTYPE_IS_COLOR}`` shell environment variable also respected by that
class, which interact in non-trivial ways.
Expand Down

0 comments on commit a342229

Please sign in to comment.