Skip to content

Commit 8188956

Browse files
authored
Add listener_runner to context object to enable developers to leverage lazy listeners in middleware (#1142)
* Add listener_runner to context object to enable developers to leverage lazy listeners in middleware * mypy lint fix
1 parent 64eedee commit 8188956

12 files changed

Lines changed: 192 additions & 27 deletions

File tree

scripts/run_tests.sh

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,5 @@ then
1414
black slack_bolt/ tests/ && \
1515
pytest -vv $1
1616
else
17-
black slack_bolt/ tests/ && pytest
18-
fi
17+
black slack_bolt/ tests/ && pytest
1918
fi

slack_bolt/app/app.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,7 @@ def step(
735735
elif not isinstance(step, WorkflowStep):
736736
raise BoltError(f"Invalid step object ({type(step)})")
737737

738-
self.use(WorkflowStepMiddleware(step, self.listener_runner))
738+
self.use(WorkflowStepMiddleware(step))
739739

740740
# -------------------------
741741
# global error handler
@@ -1350,6 +1350,10 @@ def _init_context(self, req: BoltRequest):
13501350
)
13511351
req.context["client"] = client_per_request
13521352

1353+
# Most apps do not need this "listener_runner" instance.
1354+
# It is intended for apps that start lazy listeners from their custom global middleware.
1355+
req.context["listener_runner"] = self.listener_runner
1356+
13531357
@staticmethod
13541358
def _to_listener_functions(
13551359
kwargs: dict,

slack_bolt/app/async_app.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -761,7 +761,7 @@ def step(
761761
elif not isinstance(step, AsyncWorkflowStep):
762762
raise BoltError(f"Invalid step object ({type(step)})")
763763

764-
self.use(AsyncWorkflowStepMiddleware(step, self._async_listener_runner))
764+
self.use(AsyncWorkflowStepMiddleware(step))
765765

766766
# -------------------------
767767
# global error handler
@@ -1390,6 +1390,10 @@ def _init_context(self, req: AsyncBoltRequest):
13901390
)
13911391
req.context["client"] = client_per_request
13921392

1393+
# Most apps do not need this "listener_runner" instance.
1394+
# It is intended for apps that start lazy listeners from their custom global middleware.
1395+
req.context["listener_runner"] = self.listener_runner
1396+
13931397
@staticmethod
13941398
def _to_listener_functions(
13951399
kwargs: dict,

slack_bolt/context/async_context.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,29 @@ class AsyncBoltContext(BaseContext):
1717
def to_copyable(self) -> "AsyncBoltContext":
1818
new_dict = {}
1919
for prop_name, prop_value in self.items():
20-
if prop_name in self.standard_property_names:
20+
if prop_name in self.copyable_standard_property_names:
2121
# all the standard properties are copiable
2222
new_dict[prop_name] = prop_value
23+
elif prop_name in self.non_copyable_standard_property_names:
24+
# Do nothing with this property (e.g., listener_runner)
25+
continue
2326
else:
2427
try:
2528
copied_value = create_copy(prop_value)
2629
new_dict[prop_name] = copied_value
2730
except TypeError as te:
2831
self.logger.debug(
29-
f"Skipped settings '{prop_name}' to a copied request for lazy listeners "
32+
f"Skipped setting '{prop_name}' to a copied request for lazy listeners "
3033
f"as it's not possible to make a deep copy (error: {te})"
3134
)
3235
return AsyncBoltContext(new_dict)
3336

37+
# The return type is intentionally string to avoid circular imports
38+
@property
39+
def listener_runner(self) -> "AsyncioListenerRunner": # type: ignore[name-defined]
40+
"""The properly configured listener_runner that is available for middleware/listeners."""
41+
return self["listener_runner"]
42+
3443
@property
3544
def client(self) -> Optional[AsyncWebClient]:
3645
"""The `AsyncWebClient` instance available for this request.

slack_bolt/context/base_context.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
class BaseContext(dict):
88
"""Context object associated with a request from Slack."""
99

10-
standard_property_names = [
10+
copyable_standard_property_names = [
1111
"logger",
1212
"token",
1313
"enterprise_id",
@@ -35,6 +35,11 @@ class BaseContext(dict):
3535
"complete",
3636
"fail",
3737
]
38+
non_copyable_standard_property_names = [
39+
"listener_runner",
40+
]
41+
42+
standard_property_names = copyable_standard_property_names + non_copyable_standard_property_names
3843

3944
@property
4045
def logger(self) -> Logger:

slack_bolt/context/context.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ class BoltContext(BaseContext):
1717
def to_copyable(self) -> "BoltContext":
1818
new_dict = {}
1919
for prop_name, prop_value in self.items():
20-
if prop_name in self.standard_property_names:
20+
if prop_name in self.copyable_standard_property_names:
2121
# all the standard properties are copiable
2222
new_dict[prop_name] = prop_value
23+
elif prop_name in self.non_copyable_standard_property_names:
24+
# Do nothing with this property (e.g., listener_runner)
25+
continue
2326
else:
2427
try:
2528
copied_value = create_copy(prop_value)
@@ -32,6 +35,12 @@ def to_copyable(self) -> "BoltContext":
3235
)
3336
return BoltContext(new_dict)
3437

38+
# The return type is intentionally string to avoid circular imports
39+
@property
40+
def listener_runner(self) -> "ThreadListenerRunner": # type: ignore[name-defined]
41+
"""The properly configured listener_runner that is available for middleware/listeners."""
42+
return self["listener_runner"]
43+
3544
@property
3645
def client(self) -> Optional[WebClient]:
3746
"""The `WebClient` instance available for this request.

slack_bolt/listener/asyncio_runner.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,11 @@ def _start_lazy_function(self, lazy_func: Callable[..., Awaitable[None]], reques
174174
copied_request = self._build_lazy_request(request, func_name)
175175
self.lazy_listener_runner.start(function=lazy_func, request=copied_request)
176176

177-
@staticmethod
178-
def _build_lazy_request(request: AsyncBoltRequest, lazy_func_name: str) -> AsyncBoltRequest:
179-
copied_request = create_copy(request.to_copyable())
180-
copied_request.method = "NONE"
177+
def _build_lazy_request(self, request: AsyncBoltRequest, lazy_func_name: str) -> AsyncBoltRequest:
178+
copied_request: AsyncBoltRequest = create_copy(request.to_copyable())
181179
copied_request.lazy_only = True
182180
copied_request.lazy_function_name = lazy_func_name
181+
copied_request.context["listener_runner"] = self
183182
return copied_request
184183

185184
def _debug_log_completion(self, starting_time: float, response: BoltResponse) -> None:

slack_bolt/listener/thread_runner.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,11 @@ def _start_lazy_function(self, lazy_func: Callable[..., None], request: BoltRequ
185185
copied_request = self._build_lazy_request(request, func_name)
186186
self.lazy_listener_runner.start(function=lazy_func, request=copied_request)
187187

188-
@staticmethod
189-
def _build_lazy_request(request: BoltRequest, lazy_func_name: str) -> BoltRequest:
190-
copied_request = create_copy(request.to_copyable())
191-
copied_request.method = "NONE"
188+
def _build_lazy_request(self, request: BoltRequest, lazy_func_name: str) -> BoltRequest:
189+
copied_request: BoltRequest = create_copy(request.to_copyable())
192190
copied_request.lazy_only = True
193191
copied_request.lazy_function_name = lazy_func_name
192+
copied_request.context["listener_runner"] = self
194193
return copied_request
195194

196195
def _debug_log_completion(self, starting_time: float, response: BoltResponse) -> None:

slack_bolt/workflows/step/async_step_middleware.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from typing import Callable, Optional, Awaitable
33

44
from slack_bolt.listener.async_listener import AsyncListener
5-
from slack_bolt.listener.asyncio_runner import AsyncioListenerRunner
65
from slack_bolt.middleware.async_middleware import AsyncMiddleware
76
from slack_bolt.request.async_request import AsyncBoltRequest
87
from slack_bolt.response import BoltResponse
@@ -13,9 +12,8 @@
1312
class AsyncWorkflowStepMiddleware(AsyncMiddleware):
1413
"""Base middleware for step from app specific ones"""
1514

16-
def __init__(self, step: AsyncWorkflowStep, listener_runner: AsyncioListenerRunner):
15+
def __init__(self, step: AsyncWorkflowStep):
1716
self.step = step
18-
self.listener_runner = listener_runner
1917

2018
async def async_process(
2119
self,
@@ -40,8 +38,8 @@ async def async_process(
4038

4139
return await next()
4240

41+
@staticmethod
4342
async def _run(
44-
self,
4543
listener: AsyncListener,
4644
req: AsyncBoltRequest,
4745
resp: BoltResponse,
@@ -50,7 +48,7 @@ async def _run(
5048
if next_was_not_called:
5149
return None
5250

53-
return await self.listener_runner.run(
51+
return await req.context.listener_runner.run(
5452
request=req,
5553
response=resp,
5654
listener_name=get_name_for_callable(listener.ack_function),

slack_bolt/workflows/step/step_middleware.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from typing import Callable, Optional
33

44
from slack_bolt.listener import Listener
5-
from slack_bolt.listener.thread_runner import ThreadListenerRunner
65
from slack_bolt.middleware import Middleware
76
from slack_bolt.request import BoltRequest
87
from slack_bolt.response import BoltResponse
@@ -13,9 +12,8 @@
1312
class WorkflowStepMiddleware(Middleware):
1413
"""Base middleware for step from app specific ones"""
1514

16-
def __init__(self, step: WorkflowStep, listener_runner: ThreadListenerRunner):
15+
def __init__(self, step: WorkflowStep):
1716
self.step = step
18-
self.listener_runner = listener_runner
1917

2018
def process(
2119
self,
@@ -43,8 +41,8 @@ def process(
4341

4442
return next()
4543

44+
@staticmethod
4645
def _run(
47-
self,
4846
listener: Listener,
4947
req: BoltRequest,
5048
resp: BoltResponse,
@@ -53,7 +51,7 @@ def _run(
5351
if next_was_not_called:
5452
return None
5553

56-
return self.listener_runner.run(
54+
return req.context.listener_runner.run(
5755
request=req,
5856
response=resp,
5957
listener_name=get_name_for_callable(listener.ack_function),

0 commit comments

Comments
 (0)