Skip to content

Commit

Permalink
beartype.claw x 19.
Browse files Browse the repository at this point in the history
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @ZeevoX. Specifically, this commit improves unit tests
exercising our `beartype._util.text.utiltextlabel.label_callable()`
function with improved coverage over noteworthy edge cases – including
coroutine, asynchronous generator, and synchronous generator factory
callables. (*Fractuous fractalizations distil the tillerman's generalizations!*)
  • Loading branch information
leycec committed Jun 4, 2022
1 parent a5c5541 commit f30aafd
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 66 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python_release.yml
Expand Up @@ -40,7 +40,7 @@ jobs:

steps:
- name: "Checking out repository..."
uses: 'actions/checkout@v2'
uses: 'actions/checkout@v3'
- name: "Publishing GitHub release..."
uses: 'ncipollo/release-action@v1'
with:
Expand Down
45 changes: 25 additions & 20 deletions beartype/_util/func/utilfunctest.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2022 Beartype authors.
# See "LICENSE" for further details.

Expand All @@ -13,7 +13,7 @@
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS }....................
# ....................{ IMPORTS }....................
from beartype.roar._roarexc import _BeartypeUtilCallableException
from beartype._util.func.utilfunccodeobj import (
get_func_codeobj_or_none)
Expand All @@ -31,7 +31,7 @@
# See the "beartype.cave" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']

# ....................{ CONSTANTS }....................
# ....................{ CONSTANTS }....................
FUNC_NAME_LAMBDA = '<lambda>'
'''
Default name of all **pure-Python lambda functions** (i.e., function declared
Expand All @@ -53,7 +53,7 @@
edge cases, and false positives. If you must pick your poison, pick this one.
'''

# ....................{ VALIDATORS }....................
# ....................{ VALIDATORS }....................
#FIXME: Uncomment when needed.
# def die_unless_func_lambda(
# # Mandatory parameters.
Expand Down Expand Up @@ -151,7 +151,7 @@ def die_unless_func_python(
f'the __call__() dunder method).'
)

# ....................{ TESTERS }....................
# ....................{ TESTERS }....................
def is_func_lambda(func: Any) -> bool:
'''
``True`` only if the passed object is a **pure-Python lambda function**
Expand Down Expand Up @@ -209,13 +209,14 @@ def is_func_python(func: object) -> bool:
# C-based callables are associated with *NO* code objects.
return get_func_codeobj_or_none(func) is not None

# ....................{ TESTERS ~ async }....................
# ....................{ TESTERS ~ async }....................
def is_func_async(func: object) -> bool:
'''
``True`` only if the passed object is an **asynchronous callable**
(i.e., awaitable callable satisfying the :class:`collections.abc.Awaitable`
protocol by being declared via the ``async def`` syntax and thus callable
*only* when preceded by comparable ``await`` syntax).
``True`` only if the passed object is an **asynchronous callable** (i.e.,
awaitable factory callable implicitly creating and returning an awaitable
object (i.e., satisfying the :class:`collections.abc.Awaitable` protocol) by
being declared via the ``async def`` syntax and thus callable *only* when
preceded by comparable ``await`` syntax).
Parameters
----------
Expand Down Expand Up @@ -272,9 +273,10 @@ def is_func_async(func: object) -> bool:

def is_func_async_coroutine(func: object) -> bool:
'''
``True`` only if the passed object is an **asynchronous coroutine**
(i.e., awaitable callable containing *no* ``yield`` expressions satisfying
the :class:`collections.abc.Awaitable` protocol by being declared via the
``True`` only if the passed object is an **asynchronous coroutine factory**
(i.e., awaitable callable containing *no* ``yield`` expression implicitly
creating and returning an awaitable object (i.e., satisfying the
:class:`collections.abc.Awaitable` protocol) by being declared via the
``async def`` syntax and thus callable *only* when preceded by comparable
``await`` syntax).
Expand Down Expand Up @@ -309,11 +311,12 @@ def is_func_async_coroutine(func: object) -> bool:

def is_func_async_generator(func: object) -> bool:
'''
``True`` only if the passed object is an **asynchronous generator**
``True`` only if the passed object is an **asynchronous generator factory**
(i.e., awaitable callable containing one or more ``yield`` expressions
satisfying the :class:`collections.abc.Awaitable` protocol by being
declared via the ``async def`` syntax and thus callable *only* when
preceded by comparable ``await`` syntax).
implicitly creating and returning an awaitable object (i.e., satisfying the
:class:`collections.abc.Awaitable` protocol) by being declared via the
``async def`` syntax and thus callable *only* when preceded by comparable
``await`` syntax).
Parameters
----------
Expand Down Expand Up @@ -343,12 +346,14 @@ def is_func_async_generator(func: object) -> bool:
func_codeobj.co_flags & CO_ASYNC_GENERATOR != 0
)

# ....................{ TESTERS ~ sync }....................
# ....................{ TESTERS ~ sync }....................
def is_func_sync_generator(func: object) -> bool:
'''
``True`` only if the passed object is a **synchronous generator**
``True`` only if the passed object is an **synchronous generator factory**
(i.e., awaitable callable containing one or more ``yield`` expressions
declared with the ``def`` rather than ``async def`` keyword).
implicitly creating and returning a generator object (i.e., satisfying the
:class:`collections.abc.Generator` protocol) by being declared via the
``def`` rather than ``async def`` syntax).
Parameters
----------
Expand Down
22 changes: 11 additions & 11 deletions beartype/_util/hint/pep/utilpepget.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2022 Beartype authors.
# See "LICENSE" for further details.

Expand All @@ -11,7 +11,7 @@
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS }....................
# ....................{ IMPORTS }....................
from beartype.meta import URL_ISSUES
from beartype.roar import (
BeartypeDecorHintPepException,
Expand Down Expand Up @@ -52,7 +52,7 @@
# See the "beartype.cave" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']

# ....................{ GETTERS ~ args }....................
# ....................{ GETTERS ~ args }....................
# If the active Python interpreter targets Python >= 3.9, implement this
# getter to directly access the "__args__" dunder attribute.
if IS_PYTHON_AT_LEAST_3_9:
Expand Down Expand Up @@ -156,7 +156,7 @@ def get_hint_pep_args(hint: object) -> tuple:
(int, str, typing.Dict[str, str])
'''

# ....................{ GETTERS ~ typevars }....................
# ....................{ GETTERS ~ typevars }....................
# If the active Python interpreter targets Python >= 3.9, implement this
# function to either directly access the "__parameters__" dunder attribute for
# type hints that are not PEP 585-compliant generics *OR* to synthetically
Expand Down Expand Up @@ -250,7 +250,7 @@ def get_hint_pep_typevars(hint: object) -> TupleTypes:
(T, S)
'''

# ....................{ GETTERS ~ sign }....................
# ....................{ GETTERS ~ sign }....................
def get_hint_pep_sign(hint: Any) -> HintSign:
'''
**Sign** (i.e., :class:`HintSign` instance) uniquely identifying the passed
Expand Down Expand Up @@ -422,7 +422,7 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
# underlying all higher-level hint validation functions! Calling the latter
# here would thus induce infinite recursion, which would be very bad.
#
# ..................{ PHASE ~ classname }..................
# ..................{ PHASE ~ classname }..................
# This phase attempts to map from the fully-qualified classname of this
# hint to a sign identifying *ALL* hints that are instances of that class.
#
Expand Down Expand Up @@ -469,7 +469,7 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
return hint_sign
# Else, this hint is *NOT* identifiable by its classname.

# ..................{ PHASE ~ repr }..................
# ..................{ PHASE ~ repr }..................
# This phase attempts to map from the unsubscripted machine-readable
# representation of this hint to a sign identifying *ALL* hints of that
# representation.
Expand Down Expand Up @@ -518,7 +518,7 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
# representation.
# Else, this representation (and thus this hint) is unsubscripted.

# ..................{ PHASE ~ manual }..................
# ..................{ PHASE ~ manual }..................
# This phase attempts to manually identify the signs of all hints *NOT*
# efficiently identifiably by the prior phases.
#
Expand Down Expand Up @@ -587,11 +587,11 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
elif IS_PYTHON_AT_MOST_3_9 and is_hint_pep484_newtype_pre_python310(hint):
return HintSignNewType

# ..................{ ERROR }..................
# ..................{ ERROR }..................
# Else, this hint is unrecognized. In this case, return "None".
return None

# ....................{ GETTERS ~ origin }....................
# ....................{ GETTERS ~ origin }....................
def get_hint_pep_origin_or_none(hint: Any) -> Optional[Any]:
'''
**Unsafe origin object** (i.e., arbitrary object possibly related to the
Expand Down Expand Up @@ -672,7 +672,7 @@ class such that *all* objects satisfying this hint are instances of this
# Return this hint's origin object if any *OR* "None" otherwise.
return getattr(hint, '__origin__', None)

# ....................{ GETTERS ~ origin : type }....................
# ....................{ GETTERS ~ origin : type }....................
def get_hint_pep_origin_type_isinstanceable(hint: object) -> type:
'''
**Isinstanceable origin type** (i.e., class passable as the second argument
Expand Down
1 change: 0 additions & 1 deletion beartype/_util/text/utiltextlabel.py
Expand Up @@ -22,7 +22,6 @@
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']

# ....................{ LABELLERS ~ callable }....................
#FIXME: Unit test up the "is_contextualized" boolean, please.
def label_callable(
# Mandatory parameters.
func: Callable,
Expand Down
12 changes: 6 additions & 6 deletions beartype_test/a00_unit/a20_util/mod/test_utilmodget.py
Expand Up @@ -27,22 +27,22 @@ def test_die_unless_module_attr_name() -> None:
from beartype.roar._roarexc import _BeartypeUtilModuleException
from beartype._util.mod.utilmodget import (
get_object_module_line_number_begin)
from beartype_test.a00_unit.data.util.mod.data_utilmodget import (
from beartype_test.a00_unit.data.util.mod.data_utilmodule_line import (
SlowRollingOn,
like_snakes_that_watch_their_prey,
)
from pytest import raises

# Assert this getter returns the expected line number for this callable.
assert get_object_module_line_number_begin(
like_snakes_that_watch_their_prey) == 22
like_snakes_that_watch_their_prey) == 20

#FIXME: The inspect.findsource() function underlying this call incorrectly
#suggests this class to be declared at line 29 of its submodule, when in
#fact this class is declared at the following line 30 of its submodule.
#Sadly, there's nothing meaningful we can do about this. Just be aware!
#suggests this class to be declared at this line of its submodule, when in
#fact this class is declared at the following line of its submodule. Sadly,
#there's nothing meaningful we can do about this. Just be aware, please.
# Assert this getter returns the expected line number for this class.
assert get_object_module_line_number_begin(SlowRollingOn) == 29
assert get_object_module_line_number_begin(SlowRollingOn) == 39

# Assert this validator raises the expected exception when passed an object
# that is neither a callable nor class.
Expand Down
59 changes: 38 additions & 21 deletions beartype_test/a00_unit/a20_util/text/test_utiltextlabel.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2022 Beartype authors.
# See "LICENSE" for further details.

Expand All @@ -11,13 +11,13 @@
"""


# ....................{ IMPORTS }....................
# ....................{ IMPORTS }....................
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# WARNING: To raise human-readable test errors, avoid importing from
# package-specific submodules at module scope.
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

# ....................{ TESTS }....................
# ....................{ TESTS }....................
def test_label_callable():
'''
Test the :func:`beartype._util.text.utiltextlabel.prefix_callable`
Expand All @@ -26,31 +26,48 @@ def test_label_callable():

# Defer heavyweight imports.
from beartype._util.text.utiltextlabel import prefix_callable
from beartype_test.a00_unit.data.data_type import (
async_coroutine_factory,
async_generator_factory,
sync_generator_factory,
)
from beartype_test.a00_unit.data.util.mod.data_utilmodule_line import (
like_snakes_that_watch_their_prey,
ozymandias,
which_yet_survive,
)

# Arbitrary lambda function declared on-disk.
ozymandias = lambda: 'I met a traveller from an antique land,'

# Arbitrary lambda function declared in-memory.
which_yet_survive = eval("lambda: 'stamped on these lifeless things'")

# Arbitrary non-lambda function declared on-disk.
def half_sunk_a_shattered_visage_lies_whose_frown():
return 'And wrinkled lip, and sneer of cold command,'

# Assert this labeller labels an on-disk lambda.
# Assert this labeller labels an on-disk lambda function as expected.
two_vast_and_trunkless_legs_of_stone = prefix_callable(ozymandias)
assert isinstance(two_vast_and_trunkless_legs_of_stone, str)
assert bool(two_vast_and_trunkless_legs_of_stone)
assert 'lambda' in two_vast_and_trunkless_legs_of_stone

# Assert this labeller labels an in-memory lambda.
# Assert this labeller labels an in-memory lambda function as expected.
the_hand_that_mocked_them = prefix_callable(which_yet_survive)
assert isinstance(the_hand_that_mocked_them, str)
assert bool(the_hand_that_mocked_them)
assert 'lambda' in the_hand_that_mocked_them

# Assert this labeller labels an on-disk non-lambda with the name of that
# non-lambda.
# Assert this labeller labels an on-disk non-lambda callable as expected.
tell_that_its_sculptor_well_those_passions_read = prefix_callable(
half_sunk_a_shattered_visage_lies_whose_frown)
like_snakes_that_watch_their_prey)
assert isinstance(tell_that_its_sculptor_well_those_passions_read, str)
assert half_sunk_a_shattered_visage_lies_whose_frown.__name__ in (
assert like_snakes_that_watch_their_prey.__name__ in (
tell_that_its_sculptor_well_those_passions_read)

# Assert this labeller labels an on-disk coroutine as expected.
async_coroutine_label = prefix_callable(async_coroutine_factory)
assert isinstance(async_coroutine_label, str)
assert async_coroutine_factory.__name__ in async_coroutine_label
assert 'coroutine' in async_coroutine_label

# Assert this labeller labels an on-disk asynchronous generator as expected.
async_generator_label = prefix_callable(async_generator_factory)
assert isinstance(async_generator_label, str)
assert async_generator_factory.__name__ in async_generator_label
assert 'asynchronous generator' in async_generator_label

# Assert this labeller labels an on-disk synchronous generator as expected.
sync_generator_label = prefix_callable(sync_generator_factory)
assert isinstance(sync_generator_label, str)
assert sync_generator_factory.__name__ in sync_generator_label
assert 'generator' in sync_generator_label
Expand Up @@ -6,26 +6,36 @@
'''
**Beartype module getter data submodule.**
This submodule predefines low-level class constants exercising known edge
cases on behalf of the higher-level
:mod:`beartype_test.a00_unit.a20_util.mod.test_utilmodget` submodule. Unit
tests defined in that submodule are sufficiently fragile that *no* other
submodule should import from this submodule.
This submodule predefines module-scoped objects of various types with well-known
line numbers guaranteed to remain constant, exercising issues with respect to
line numbers in higher-level test submodules.
'''

# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# CAUTION: For completeness, unit tests test the *EXACT* contents of this file.
# Changes to this file must thus be synchronized with those tests.
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

# ....................{ CALLABLES }....................
# ....................{ CALLABLES ~ non-lambda }....................
def like_snakes_that_watch_their_prey():
'''
Arbitrary non-lambda function physically declared by this submodule.
'''

return 'from their far fountains,'

# ....................{ CALLABLES ~ lambda }....................
ozymandias = lambda: 'I met a traveller from an antique land,'
'''
Arbitrary lambda function declared on-disk.
'''


which_yet_survive = eval("lambda: 'stamped on these lifeless things'")
'''
Arbitrary lambda function declared in-memory.
'''

# ....................{ CLASSES }....................
class SlowRollingOn(object):
'''
Expand Down

0 comments on commit f30aafd

Please sign in to comment.