Skip to content

Commit

Permalink
PyPy tests.
Browse files Browse the repository at this point in the history
This commit resolves unit tests locally failing under at least PyPy 3.6
(and probably PyPy 3.7) as well, pertaining to the most recent commit.
For reasons, PyPy appears to be dynamically reducing pure-Python
callables into C-based callables -- which is fascinating and horrifying.
This commit generalizes these tests to conditionally handle PyPy's
aberrant strangeness. (*Delectable luck!*)
  • Loading branch information
leycec committed Feb 9, 2021
1 parent 078d969 commit 647b4c7
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 153 deletions.
57 changes: 35 additions & 22 deletions beartype/_util/utilcallable.py → beartype/_util/func/utilfunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,12 @@
This private submodule is *not* intended for importation by downstream callers.
'''

#FIXME: Rename this entire submodule to the new "beartype._util.func.utilfunc".
#Once we start dynamically inspecting callables ourselves rather than relying
#on the "inspect" module, we're going to have a plethora of these sorts of
#submodules lying around. Huzzah!

# ....................{ IMPORTS }....................
from sys import modules

# ....................{ IMPORTS }....................
from collections.abc import Callable
from beartype.cave import ClassType, CallableCTypes, MethodBoundInstanceOrClassType

from sys import modules
from beartype.cave import CallableCTypes, MethodBoundInstanceOrClassType
from beartype.roar import _BeartypeUtilCallableException

# ....................{ GETTERS }....................
def get_callable_filename_or_placeholder(func: Callable) -> str:
'''
Expand Down Expand Up @@ -55,30 +49,49 @@ def get_callable_filename_or_placeholder(func: Callable) -> str:
which has been highly optimized for use by the performance-sensitive
:func:`beartype.beartype` decorator.
'''
if callable(func) is False:
raise _BeartypeUtilCallableException(
f"Attempted to pass non-callable: {repr(func)}")


# If this callable is *NOT*, raise an exception.
if not callable(func):
raise _BeartypeUtilCallableException(f'{repr(func)} not callable.')

# Absolute filename of this callable if this callable was physically
# declared by an on-disk file *OR* a default placeholder otherwise. For
# simplicity, initialize this filename to that placeholder.
filename = '<string>'

# Grab location of the module declaring class.
# If this callable is a callable class defining the __call__() method...
if isinstance(func, type):

# Fully-qualified name of the module declaring this class if this class
# was physically declared by an on-disk module *OR* "None" otherwise.
func_module_name = func.__module__

# Check the Class has a module thats registered with the system.
# If this class was physically declared by an on-disk module, defer to
# the absolute filename of that module.
#
# Note that arbitrary modules need *NOT* declare the "__file__" dunder
# attribute. Unlike most other core Python objects, modules are simply
# arbitrary objects that reside in the "sys.modules" dictionary.
if func_module_name:
filename = getattr(modules[func_module_name],'__file__', filename)

filename = getattr(modules[func_module_name], '__file__', filename)
# Else, this callable is *NOT* a callable class. In this case, this
# callable *MUST* be either...
else:
# Built-in types are callables with no .py source file.
# If this callable is implemented in C, this callable has no code
# object with which to inspect the filename declaring this callable. In
# this case, defer to a C-specific placeholder string.
if isinstance(func, CallableCTypes):
filename = "<builtin>"

filename = '<C-based>'
# Else, this callable is implemented in Python. In this case...
else:
# If this callable is a bound method wrapping an unbound function,
# unwrap this method into the function it wraps. Why? Because only
# the latter provides the code object for this callable.
if isinstance(func, MethodBoundInstanceOrClassType):
func = func.__func__

# Defer to the absolute filename of the Python file declaring this
# callable, dynamically retrieved from this callable's code object.
filename = func.__code__.co_filename

# Return this filename.
return filename
22 changes: 22 additions & 0 deletions beartype/_util/py/utilpyinterpreter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2021 Cecil Curry.
# See "LICENSE" for further details.

'''
**Beartype Python interpreter utilities.**
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS }....................
from platform import python_implementation

# See the "beartype.__init__" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']

# ....................{ TESTERS }....................
IS_PYPY = python_implementation() == 'PyPy'
'''
``True`` only if the current Python interpreter is PyPy.
'''
26 changes: 15 additions & 11 deletions beartype/cave.py
Original file line number Diff line number Diff line change
Expand Up @@ -1386,8 +1386,21 @@ class _UnavailableTypesTuple(tuple):
CallableTypes = tuple(set(FunctionTypes) | set(MethodTypes))
'''
Tuple of all **callable types** (i.e., types whose instances are callable
objects, including both built-in and user-defined functions, lambdas, methods,
and method descriptors).
objects implemented in either low-level C or high-level Python, including both
built-in and user-defined functions, lambdas, methods, and method descriptors).
'''


CallableCTypes = (
FunctionOrMethodCType,
MethodBoundInstanceDunderCType,
MethodUnboundInstanceDunderCType,
MethodUnboundInstanceNondunderCType,
MethodUnboundClassCType,
)
'''
Tuple of all **C-based callable types** (i.e., types whose instances are
callable objects implemented in low-level C rather than high-level Python).
'''


Expand Down Expand Up @@ -1657,15 +1670,6 @@ class _UnavailableTypesTuple(tuple):
tuple).
'''

CallableCTypes = (FunctionOrMethodCType, MethodBoundInstanceDunderCType,
MethodUnboundInstanceDunderCType, MethodUnboundInstanceNondunderCType,
MethodUnboundClassCType,
)
'''
Tuple of all callable **C-based types**
(i.e. callable objects implemented in low-level C.
'''

# ....................{ TUPLES ~ post-init : version }....................
VersionTypes = (StrType,) + VersionComparableTypes
'''
Expand Down
190 changes: 190 additions & 0 deletions beartype_test/a00_unit/a00_util/func/test_utilfunc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright 2014-2021 by Cecil Curry.
# See "LICENSE" for further details.

'''
**Beartype callable utility unit tests.**
This submodule unit tests the public API of the private
:mod:`beartype._util.func.utilfunc` submodule.
'''

# ....................{ IMPORTS }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# WARNING: To raise human-readable test errors, avoid importing from
# package-specific submodules at module scope.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
from beartype_test.util.mark.pytskip import skip_if_pypy
from pytest import raises

# ....................{ DATA }....................
#FIXME: Shift into a new "beartype_test.a00_unit.data.data_type" submodule for
#general-purpose reuse throughout unit tests.

def _function():
'''
Arbitrary function.
'''

pass


class _Class:
'''
Arbitrary class defining an arbitrary method.
'''

def _method(self):
'''Arbitrary method.'''

pass


def _cell_factory():
'''
Arbitrary closure factory function.
'''

a = 1
def f():
nonlocal a
return f.__closure__[0]

def _generator():
'''
Arbitrary generator factory function.
'''

yield 1


async def _coroutine():
'''
Arbitrary coroutine factory function.
'''

pass

_coroutine = _coroutine()
_coroutine.close() # Prevent ResourceWarning


async def _async_generator():
'''
Arbitrary asynchronous generator factory function.
'''

yield

_async_generator = _async_generator()

# ....................{ TESTS ~ filename }....................
def test_get_callable_filename_or_placeholder_pass_filename() -> None:
'''
Test successful usage of the
:func:`beartype._util.func.utilfunc.get_callable_filename_or_placeholder`
function returning absolute filenames.
'''

# Defer test-specific imports.
import pathlib
from beartype.roar import _BeartypeUtilCallableException
from beartype._util.func.utilfunc import (
get_callable_filename_or_placeholder)
from beartype._util.py.utilpyinterpreter import IS_PYPY

# Tuple of all pure-Python callables to be tested.
CALLABLES_PYTHON = (_function, _Class._method)

# Assert for pure-Python callables...
for callable_python in CALLABLES_PYTHON:
assert get_callable_filename_or_placeholder(callable_python) == (
# If the current interpreter is PyPy, that PyPy internally reduced
# this pure-Python callable to a C-based callable;
'<C-based>' if IS_PYPY else
# Else, that CPython preserved this pure-Python callable as is.
str(pathlib.Path(__file__))
)

# Assert this function returns the expected filename for a class.
assert get_callable_filename_or_placeholder(_Class) == str(
pathlib.Path(__file__))

# Assert this function returns the expected filename for a module function.
assert get_callable_filename_or_placeholder(pathlib.Path) == (
pathlib.__file__)


def test_get_callable_filename_or_placeholder_pass_placeholder() -> None:
'''
Test successful usage of the
:func:`beartype._util.func.utilfunc.get_callable_filename_or_placeholder`
function returning placeholder strings.
'''

# Defer test-specific imports.
from collections import namedtuple
from beartype._util.func.utilfunc import (
get_callable_filename_or_placeholder)

#FIXME: Nice! Let's globalize this into the module namespace and then reuse
#this approach in the
#test_get_callable_filename_or_placeholder_pass_filename() test as well,
#where it should simplify things nicely.
Test = namedtuple('Test', ['case', 'result'])

# Need to test for dynamically declared callable and class.
ret_placeholder = [
Test(case=len, result = "<C-based>"), # Built-in FunctionType
Test(case=[].append, result = "<C-based>"), # Built-in Method Type
Test(case=object.__init__, result = "<C-based>"), # Wrapper Descriptor Type
Test(case=object().__str__, result = "<C-based>"), # Method Wrapper Type
Test(case=str.join, result = "<C-based>"), # Method Descriptor Type

#FIXME: *UGH.* This probably should be callable under PyPy 3.6, but
#it's not, which is why we've currently disabled this. That's clearly a
#PyPy bug. Uncomment this *AFTER* we drop support for PyPy 3.6 (and any
#newer PyPy versions also failing to implement this properly). We
#should probably also consider filing an upstream issue with PyPy,
#because this is non-ideal and non-orthogonal behaviour with CPython.
# Test(case=dict.__dict__['fromkeys'], result = "<C-based>"), # Class Method Descriptor
]

for test in ret_placeholder:
assert(get_callable_filename_or_placeholder(test.case) == test.result)


def test_get_callable_filename_or_placeholder_fail() -> None:
'''
Test unsuccessful usage of the
:func:`beartype._util.func.utilfunc.get_callable_filename_or_placeholder`
function.
'''

# Defer test-specific imports.
import sys
from beartype.roar import _BeartypeUtilCallableException
from beartype._util.func.utilfunc import (
get_callable_filename_or_placeholder)

try:
raise TypeError
except TypeError:
_trace_back = sys.exc_info()[2]

throw_err = [
_function.__code__, # CodeType
type.__dict__, # Mapping Proxy Type
sys.implementation, # Simple Namespace Type
_cell_factory(), # Cell Type
_generator(),
_coroutine,
_async_generator,
_trace_back,
_trace_back.tb_frame,
]

for case in throw_err:
with raises(_BeartypeUtilCallableException):
get_callable_filename_or_placeholder(case)

0 comments on commit 647b4c7

Please sign in to comment.