Skip to content

Commit fd227d2

Browse files
committed
fixes #771
1 parent ed4a368 commit fd227d2

3 files changed

Lines changed: 79 additions & 14 deletions

File tree

fastcore/_modidx.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,7 @@
743743
'fastcore.xtras.hl_md': ('xtras.html#hl_md', 'fastcore/xtras.py'),
744744
'fastcore.xtras.image_size': ('xtras.html#image_size', 'fastcore/xtras.py'),
745745
'fastcore.xtras.img_bytes': ('xtras.html#img_bytes', 'fastcore/xtras.py'),
746+
'fastcore.xtras.is_async_callable': ('xtras.html#is_async_callable', 'fastcore/xtras.py'),
746747
'fastcore.xtras.is_listy': ('xtras.html#is_listy', 'fastcore/xtras.py'),
747748
'fastcore.xtras.is_namedtuple': ('xtras.html#is_namedtuple', 'fastcore/xtras.py'),
748749
'fastcore.xtras.is_typeddict': ('xtras.html#is_typeddict', 'fastcore/xtras.py'),

fastcore/xtras.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
'autostart', 'EventTimer', 'stringfmt_names', 'PartialFormatter', 'partial_format', 'truncstr', 'utc2local',
1616
'local2utc', 'trace', 'modified_env', 'ContextManagers', 'shufflish', 'console_help', 'hl_md', 'type2str',
1717
'dataclass_src', 'Unset', 'nullable_dc', 'make_nullable', 'flexiclass', 'asdict', 'vars_pub', 'is_typeddict',
18-
'is_namedtuple', 'CachedIter', 'CachedAwaitable', 'reawaitable', 'flexicache', 'time_policy', 'mtime_policy',
19-
'timed_cache']
18+
'is_namedtuple', 'CachedIter', 'CachedAwaitable', 'reawaitable', 'is_async_callable', 'flexicache',
19+
'time_policy', 'mtime_policy', 'timed_cache']
2020

2121
# %% ../nbs/03_xtras.ipynb #3401d507
2222
from .imports import *
@@ -960,6 +960,14 @@ def reawaitable(func:callable):
960960
def _f(*args, **kwargs): return CachedAwaitable(func(*args, **kwargs))
961961
return _f
962962

963+
# %% ../nbs/03_xtras.ipynb #1fc78492
964+
def is_async_callable(obj):
965+
"Check if `obj` is an async callable, handling `partial` wrappers and callable instances"
966+
# Implementation from Starlette; Copyright © 2018, Encode OSS Ltd.
967+
from asyncio import iscoroutinefunction
968+
while isinstance(obj, partial): obj = obj.func
969+
return iscoroutinefunction(obj) or (callable(obj) and iscoroutinefunction(obj.__call__))
970+
963971
# %% ../nbs/03_xtras.ipynb #d2b4fe09
964972
def flexicache(*funcs, maxsize=128):
965973
"Like `lru_cache`, but customisable with policy `funcs`"

nbs/03_xtras.ipynb

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2261,7 +2261,7 @@
22612261
{
22622262
"data": {
22632263
"text/plain": [
2264-
"['h', 'g', 'c', 'f', 'e', 'a', 'd', 'b']"
2264+
"['e', 'b', 'g', 'd', 'h', 'c', 'f', 'a']"
22652265
]
22662266
},
22672267
"execution_count": null,
@@ -2657,7 +2657,7 @@
26572657
{
26582658
"data": {
26592659
"text/plain": [
2660-
"'_sKl4z921TkyyYSWEcGSsbQ'"
2660+
"'_z9sXygihSNeFX55bTEVs3A'"
26612661
]
26622662
},
26632663
"execution_count": null,
@@ -2752,7 +2752,7 @@
27522752
{
27532753
"data": {
27542754
"text/plain": [
2755-
"('5ca7845c', '8c7d7247')"
2755+
"('40e6f9cb', '8c7d7247')"
27562756
]
27572757
},
27582758
"execution_count": null,
@@ -3504,8 +3504,8 @@
35043504
"name": "stdout",
35053505
"output_type": "stream",
35063506
"text": [
3507-
"Num Events: 9, Freq/sec: 340.6\n",
3508-
"Most recent: ▅▃▇▁▅ 268.0 265.5 297.0 225.3 273.3\n"
3507+
"Num Events: 9, Freq/sec: 344.8\n",
3508+
"Most recent: ▅▃▆▁▇ 266.1 261.7 279.1 235.3 283.6\n"
35093509
]
35103510
}
35113511
],
@@ -4642,6 +4642,68 @@
46424642
"print(await r) # \"data\" (no delay)"
46434643
]
46444644
},
4645+
{
4646+
"cell_type": "code",
4647+
"execution_count": null,
4648+
"id": "1fc78492",
4649+
"metadata": {},
4650+
"outputs": [],
4651+
"source": [
4652+
"#| export\n",
4653+
"def is_async_callable(obj):\n",
4654+
" \"Check if `obj` is an async callable, handling `partial` wrappers and callable instances\"\n",
4655+
" # Implementation from Starlette; Copyright © 2018, Encode OSS Ltd.\n",
4656+
" from asyncio import iscoroutinefunction\n",
4657+
" while isinstance(obj, partial): obj = obj.func\n",
4658+
" return iscoroutinefunction(obj) or (callable(obj) and iscoroutinefunction(obj.__call__))"
4659+
]
4660+
},
4661+
{
4662+
"cell_type": "markdown",
4663+
"id": "c498168d",
4664+
"metadata": {},
4665+
"source": [
4666+
"`is_async_callable` detects whether an object can be called asynchronously. It goes beyond `asyncio.iscoroutinefunction` by also handling `functools.partial`-wrapped async functions (unwrapping through any number of layers) and callable objects whose `__call__` method is a coroutine. The implementation is thanks to [Starlette](https://github.com/encode/starlette)."
4667+
]
4668+
},
4669+
{
4670+
"cell_type": "code",
4671+
"execution_count": null,
4672+
"id": "0079316a",
4673+
"metadata": {},
4674+
"outputs": [],
4675+
"source": [
4676+
"async def f(): pass\n",
4677+
"assert is_async_callable(f)\n",
4678+
"assert is_async_callable(partial(f))\n",
4679+
"assert not is_async_callable(lambda: None)"
4680+
]
4681+
},
4682+
{
4683+
"cell_type": "code",
4684+
"execution_count": null,
4685+
"id": "cfb13929",
4686+
"metadata": {},
4687+
"outputs": [],
4688+
"source": [
4689+
"class AsyncObj:\n",
4690+
" async def __call__(self): pass\n",
4691+
"\n",
4692+
"class SyncObj:\n",
4693+
" def __call__(self): pass\n",
4694+
"\n",
4695+
"assert is_async_callable(AsyncObj())\n",
4696+
"assert not is_async_callable(SyncObj())"
4697+
]
4698+
},
4699+
{
4700+
"cell_type": "markdown",
4701+
"id": "81493a56",
4702+
"metadata": {},
4703+
"source": [
4704+
"### flexicache"
4705+
]
4706+
},
46454707
{
46464708
"cell_type": "code",
46474709
"execution_count": null,
@@ -4883,13 +4945,7 @@
48834945
]
48844946
}
48854947
],
4886-
"metadata": {
4887-
"kernelspec": {
4888-
"display_name": "python3",
4889-
"language": "python",
4890-
"name": "python3"
4891-
}
4892-
},
4948+
"metadata": {},
48934949
"nbformat": 4,
48944950
"nbformat_minor": 5
48954951
}

0 commit comments

Comments
 (0)