Skip to content

Commit dc09c4c

Browse files
authored
Fix #545 Enable to use lazy listeners even when having any custom context data (#546)
1 parent bc094f0 commit dc09c4c

File tree

9 files changed

+218
-2
lines changed

9 files changed

+218
-2
lines changed

slack_bolt/context/async_context.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,29 @@
66
from slack_bolt.context.base_context import BaseContext
77
from slack_bolt.context.respond.async_respond import AsyncRespond
88
from slack_bolt.context.say.async_say import AsyncSay
9+
from slack_bolt.util.utils import create_copy
910

1011

1112
class AsyncBoltContext(BaseContext):
1213
"""Context object associated with a request from Slack."""
1314

15+
def to_copyable(self) -> "AsyncBoltContext":
16+
new_dict = {}
17+
for prop_name, prop_value in self.items():
18+
if prop_name in self.standard_property_names:
19+
# all the standard properties are copiable
20+
new_dict[prop_name] = prop_value
21+
else:
22+
try:
23+
copied_value = create_copy(prop_value)
24+
new_dict[prop_name] = copied_value
25+
except TypeError as te:
26+
self.logger.debug(
27+
f"Skipped settings '{prop_name}' to a copied request for lazy listeners "
28+
f"as it's not possible to make a deep copy (error: {te})"
29+
)
30+
return AsyncBoltContext(new_dict)
31+
1432
@property
1533
def client(self) -> Optional[AsyncWebClient]:
1634
"""The `AsyncWebClient` instance available for this request.

slack_bolt/context/base_context.py

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

10+
standard_property_names = [
11+
"logger",
12+
"token",
13+
"enterprise_id",
14+
"is_enterprise_install",
15+
"team_id",
16+
"user_id",
17+
"channel_id",
18+
"response_url",
19+
"matches",
20+
"authorize_result",
21+
"bot_token",
22+
"bot_id",
23+
"bot_user_id",
24+
"user_token",
25+
"client",
26+
"ack",
27+
"say",
28+
"respond",
29+
]
30+
1031
@property
1132
def logger(self) -> Logger:
1233
"""The properly configured logger that is available for middleware/listeners."""

slack_bolt/context/context.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,30 @@
77
from slack_bolt.context.base_context import BaseContext
88
from slack_bolt.context.respond import Respond
99
from slack_bolt.context.say import Say
10+
from slack_bolt.util.utils import create_copy
1011

1112

1213
class BoltContext(BaseContext):
1314
"""Context object associated with a request from Slack."""
1415

16+
def to_copyable(self) -> "BoltContext":
17+
new_dict = {}
18+
for prop_name, prop_value in self.items():
19+
if prop_name in self.standard_property_names:
20+
# all the standard properties are copiable
21+
new_dict[prop_name] = prop_value
22+
else:
23+
try:
24+
copied_value = create_copy(prop_value)
25+
new_dict[prop_name] = copied_value
26+
except TypeError as te:
27+
self.logger.warning(
28+
f"Skipped setting '{prop_name}' to a copied request for lazy listeners "
29+
"due to a deep-copy creation error. Consider passing the value not as part of context object "
30+
f"(error: {te})"
31+
)
32+
return BoltContext(new_dict)
33+
1534
@property
1635
def client(self) -> Optional[WebClient]:
1736
"""The `WebClient` instance available for this request.

slack_bolt/listener/asyncio_runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def _start_lazy_function(
198198
def _build_lazy_request(
199199
request: AsyncBoltRequest, lazy_func_name: str
200200
) -> AsyncBoltRequest:
201-
copied_request = create_copy(request)
201+
copied_request = create_copy(request.to_copyable())
202202
copied_request.method = "NONE"
203203
copied_request.lazy_only = True
204204
copied_request.lazy_function_name = lazy_func_name

slack_bolt/listener/thread_runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ def _start_lazy_function(
195195

196196
@staticmethod
197197
def _build_lazy_request(request: BoltRequest, lazy_func_name: str) -> BoltRequest:
198-
copied_request = create_copy(request)
198+
copied_request = create_copy(request.to_copyable())
199199
copied_request.method = "NONE"
200200
copied_request.lazy_only = True
201201
copied_request.lazy_function_name = lazy_func_name

slack_bolt/request/async_request.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,12 @@ def __init__(
7575
"x-slack-bolt-lazy-function-name", [None]
7676
)[0]
7777
self.mode = mode
78+
79+
def to_copyable(self) -> "AsyncBoltRequest":
80+
return AsyncBoltRequest(
81+
body=self.raw_body,
82+
query=self.query,
83+
headers=self.headers,
84+
context=self.context.to_copyable(),
85+
mode=self.mode,
86+
)

slack_bolt/request/request.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,12 @@ def __init__(
7272
"x-slack-bolt-lazy-function-name", [None]
7373
)[0]
7474
self.mode = mode
75+
76+
def to_copyable(self) -> "BoltRequest":
77+
return BoltRequest(
78+
body=self.raw_body,
79+
query=self.query,
80+
headers=self.headers,
81+
context=self.context.to_copyable(),
82+
mode=self.mode,
83+
)

tests/scenario_tests/test_lazy.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,73 @@ def async2(say):
138138
assert response.status == 200
139139
time.sleep(1) # wait a bit
140140
assert self.mock_received_requests["/chat.postMessage"] == 2
141+
142+
def test_issue_545_context_copy_failure(self):
143+
def just_ack(ack):
144+
ack()
145+
146+
class LazyClass:
147+
def __call__(self, context, say):
148+
assert context.get("foo") == "FOO"
149+
assert context.get("ssl_context") is None
150+
time.sleep(0.3)
151+
say(text="lazy function 1")
152+
153+
def async2(context, say):
154+
assert context.get("foo") == "FOO"
155+
assert context.get("ssl_context") is None
156+
time.sleep(0.5)
157+
say(text="lazy function 2")
158+
159+
app = App(
160+
client=self.web_client,
161+
signing_secret=self.signing_secret,
162+
)
163+
164+
@app.middleware
165+
def set_ssl_context(context, next_):
166+
from ssl import SSLContext
167+
168+
context["foo"] = "FOO"
169+
# This causes an error when starting lazy listener executions
170+
context["ssl_context"] = SSLContext()
171+
next_()
172+
173+
# 2021-12-13 11:14:29 ERROR Failed to run a middleware middleware (error: cannot pickle 'SSLContext' object)
174+
# Traceback (most recent call last):
175+
# File "/path/to/bolt-python/slack_bolt/app/app.py", line 545, in dispatch
176+
# ] = self._listener_runner.run(
177+
# File "/path/to/bolt-python/slack_bolt/listener/thread_runner.py", line 166, in run
178+
# self._start_lazy_function(lazy_func, request)
179+
# File "/path/to/bolt-python/slack_bolt/listener/thread_runner.py", line 193, in _start_lazy_function
180+
# copied_request = self._build_lazy_request(request, func_name)
181+
# File "/path/to/bolt-python/slack_bolt/listener/thread_runner.py", line 198, in _build_lazy_request
182+
# copied_request = create_copy(request)
183+
# File "/path/to/bolt-python/slack_bolt/util/utils.py", line 48, in create_copy
184+
# return copy.deepcopy(original)
185+
# File "/path/to/python/lib/python3.9/copy.py", line 172, in deepcopy
186+
# y = _reconstruct(x, memo, *rv)
187+
# File "/path/to/python/lib/python3.9/copy.py", line 270, in _reconstruct
188+
# state = deepcopy(state, memo)
189+
# File "/path/to/python/lib/python3.9/copy.py", line 146, in deepcopy
190+
# y = copier(x, memo)
191+
# File "/path/to/python/lib/python3.9/copy.py", line 230, in _deepcopy_dict
192+
# y[deepcopy(key, memo)] = deepcopy(value, memo)
193+
# File "/path/to/python/lib/python3.9/copy.py", line 172, in deepcopy
194+
# y = _reconstruct(x, memo, *rv)
195+
# File "/path/to/python/lib/python3.9/copy.py", line 296, in _reconstruct
196+
# value = deepcopy(value, memo)
197+
# File "/path/to/python/lib/python3.9/copy.py", line 161, in deepcopy
198+
# rv = reductor(4)
199+
# TypeError: cannot pickle 'SSLContext' object
200+
201+
app.action("a")(
202+
ack=just_ack,
203+
lazy=[LazyClass(), async2],
204+
)
205+
206+
request = self.build_valid_request()
207+
response = app.dispatch(request)
208+
assert response.status == 200
209+
time.sleep(1) # wait a bit
210+
assert self.mock_received_requests["/chat.postMessage"] == 2

tests/scenario_tests_async/test_lazy.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,73 @@ async def async2(say):
117117
assert response.status == 200
118118
await asyncio.sleep(1) # wait a bit
119119
assert self.mock_received_requests["/chat.postMessage"] == 2
120+
121+
@pytest.mark.asyncio
122+
async def test_issue_545_context_copy_failure(self):
123+
async def just_ack(ack):
124+
await ack()
125+
126+
async def async1(context, say):
127+
assert context.get("foo") == "FOO"
128+
assert context.get("ssl_context") is None
129+
await asyncio.sleep(0.3)
130+
await say(text="lazy function 1")
131+
132+
async def async2(context, say):
133+
assert context.get("foo") == "FOO"
134+
assert context.get("ssl_context") is None
135+
await asyncio.sleep(0.5)
136+
await say(text="lazy function 2")
137+
138+
app = AsyncApp(
139+
client=self.web_client,
140+
signing_secret=self.signing_secret,
141+
)
142+
143+
@app.middleware
144+
async def set_ssl_context(context, next_):
145+
from ssl import SSLContext
146+
147+
context["foo"] = "FOO"
148+
# This causes an error when starting lazy listener executions
149+
context["ssl_context"] = SSLContext()
150+
await next_()
151+
152+
# 2021-12-13 11:52:46 ERROR Failed to run a middleware function (error: cannot pickle 'SSLContext' object)
153+
# Traceback (most recent call last):
154+
# File "/path/to/bolt-python/slack_bolt/app/async_app.py", line 585, in async_dispatch
155+
# ] = await self._async_listener_runner.run(
156+
# File "/path/to/bolt-python/slack_bolt/listener/asyncio_runner.py", line 167, in run
157+
# self._start_lazy_function(lazy_func, request)
158+
# File "/path/to/bolt-python/slack_bolt/listener/asyncio_runner.py", line 194, in _start_lazy_function
159+
# copied_request = self._build_lazy_request(request, func_name)
160+
# File "/path/to/bolt-python/slack_bolt/listener/asyncio_runner.py", line 201, in _build_lazy_request
161+
# copied_request = create_copy(request)
162+
# File "/path/to/bolt-python/slack_bolt/util/utils.py", line 48, in create_copy
163+
# return copy.deepcopy(original)
164+
# File "/path/to/python/lib/python3.9/copy.py", line 172, in deepcopy
165+
# y = _reconstruct(x, memo, *rv)
166+
# File "/path/to/python/lib/python3.9/copy.py", line 270, in _reconstruct
167+
# state = deepcopy(state, memo)
168+
# File "/path/to/python/lib/python3.9/copy.py", line 146, in deepcopy
169+
# y = copier(x, memo)
170+
# File "/path/to/python/lib/python3.9/copy.py", line 230, in _deepcopy_dict
171+
# y[deepcopy(key, memo)] = deepcopy(value, memo)
172+
# File "/path/to/python/lib/python3.9/copy.py", line 172, in deepcopy
173+
# y = _reconstruct(x, memo, *rv)
174+
# File "/path/to/python/lib/python3.9/copy.py", line 296, in _reconstruct
175+
# value = deepcopy(value, memo)
176+
# File "/path/to/python/lib/python3.9/copy.py", line 161, in deepcopy
177+
# rv = reductor(4)
178+
# TypeError: cannot pickle 'SSLContext' object
179+
180+
app.action("a")(
181+
ack=just_ack,
182+
lazy=[async1, async2],
183+
)
184+
185+
request = self.build_valid_request()
186+
response = await app.async_dispatch(request)
187+
assert response.status == 200
188+
await asyncio.sleep(1) # wait a bit
189+
assert self.mock_received_requests["/chat.postMessage"] == 2

0 commit comments

Comments
 (0)