Skip to content

Commit

Permalink
Fix slackapi#545 Enable to use lazy listeners even when having any cu…
Browse files Browse the repository at this point in the history
…stom context data
  • Loading branch information
seratch committed Dec 13, 2021
1 parent e8556c1 commit d68418b
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 2 deletions.
18 changes: 18 additions & 0 deletions slack_bolt/context/async_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,29 @@
from slack_bolt.context.base_context import BaseContext
from slack_bolt.context.respond.async_respond import AsyncRespond
from slack_bolt.context.say.async_say import AsyncSay
from slack_bolt.util.utils import create_copy


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

def create_no_custom_prop_copy(self) -> "AsyncBoltContext":
new_dict = {}
for prop_name, prop_value in self.items():
if prop_name in self.standard_property_names:
# all the standard properties are copiable
new_dict[prop_name] = prop_value
else:
try:
copied_value = create_copy(prop_value)
new_dict[prop_name] = copied_value
except TypeError as te:
self.logger.debug(
f"Skipped settings '{prop_name}' to a copied request for lazy listeners "
f"as it's not possible to make a deep copy (error: {te})"
)
return AsyncBoltContext(new_dict)

@property
def client(self) -> Optional[AsyncWebClient]:
"""The `AsyncWebClient` instance available for this request.
Expand Down
21 changes: 21 additions & 0 deletions slack_bolt/context/base_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@
class BaseContext(dict):
"""Context object associated with a request from Slack."""

standard_property_names = [
"logger",
"token",
"enterprise_id",
"is_enterprise_install",
"team_id",
"user_id",
"channel_id",
"response_url",
"matches",
"authorize_result",
"bot_token",
"bot_id",
"bot_user_id",
"user_token",
"client",
"ack",
"say",
"respond",
]

@property
def logger(self) -> Logger:
"""The properly configured logger that is available for middleware/listeners."""
Expand Down
19 changes: 19 additions & 0 deletions slack_bolt/context/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,30 @@
from slack_bolt.context.base_context import BaseContext
from slack_bolt.context.respond import Respond
from slack_bolt.context.say import Say
from slack_bolt.util.utils import create_copy


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

def create_no_custom_prop_copy(self) -> "BoltContext":
new_dict = {}
for prop_name, prop_value in self.items():
if prop_name in self.standard_property_names:
# all the standard properties are copiable
new_dict[prop_name] = prop_value
else:
try:
copied_value = create_copy(prop_value)
new_dict[prop_name] = copied_value
except TypeError as te:
self.logger.warning(
f"Skipped setting '{prop_name}' to a copied request for lazy listeners "
"due to a deep-copy creation error. Consider passing the value not as part of context object "
f"(error: {te})"
)
return BoltContext(new_dict)

@property
def client(self) -> Optional[WebClient]:
"""The `WebClient` instance available for this request.
Expand Down
2 changes: 1 addition & 1 deletion slack_bolt/listener/asyncio_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def _start_lazy_function(
def _build_lazy_request(
request: AsyncBoltRequest, lazy_func_name: str
) -> AsyncBoltRequest:
copied_request = create_copy(request)
copied_request = create_copy(request.to_copiable())
copied_request.method = "NONE"
copied_request.lazy_only = True
copied_request.lazy_function_name = lazy_func_name
Expand Down
2 changes: 1 addition & 1 deletion slack_bolt/listener/thread_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def _start_lazy_function(

@staticmethod
def _build_lazy_request(request: BoltRequest, lazy_func_name: str) -> BoltRequest:
copied_request = create_copy(request)
copied_request = create_copy(request.to_copiable())
copied_request.method = "NONE"
copied_request.lazy_only = True
copied_request.lazy_function_name = lazy_func_name
Expand Down
9 changes: 9 additions & 0 deletions slack_bolt/request/async_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,12 @@ def __init__(
"x-slack-bolt-lazy-function-name", [None]
)[0]
self.mode = mode

def to_copiable(self) -> "AsyncBoltRequest":
return AsyncBoltRequest(
body=self.raw_body,
query=self.query,
headers=self.headers,
context=self.context.create_no_custom_prop_copy(),
mode=self.mode,
)
9 changes: 9 additions & 0 deletions slack_bolt/request/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,12 @@ def __init__(
"x-slack-bolt-lazy-function-name", [None]
)[0]
self.mode = mode

def to_copiable(self) -> "BoltRequest":
return BoltRequest(
body=self.raw_body,
query=self.query,
headers=self.headers,
context=self.context.create_no_custom_prop_copy(),
mode=self.mode,
)
70 changes: 70 additions & 0 deletions tests/scenario_tests/test_lazy.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,73 @@ def async2(say):
assert response.status == 200
time.sleep(1) # wait a bit
assert self.mock_received_requests["/chat.postMessage"] == 2

def test_issue_545_context_copy_failure(self):
def just_ack(ack):
ack()

class LazyClass:
def __call__(self, context, say):
assert context.get("foo") == "FOO"
assert context.get("ssl_context") is None
time.sleep(0.3)
say(text="lazy function 1")

def async2(context, say):
assert context.get("foo") == "FOO"
assert context.get("ssl_context") is None
time.sleep(0.5)
say(text="lazy function 2")

app = App(
client=self.web_client,
signing_secret=self.signing_secret,
)

@app.middleware
def set_ssl_context(context, next_):
from ssl import SSLContext

context["foo"] = "FOO"
# This causes an error when starting lazy listener executions
context["ssl_context"] = SSLContext()
next_()

# 2021-12-13 11:14:29 ERROR Failed to run a middleware middleware (error: cannot pickle 'SSLContext' object)
# Traceback (most recent call last):
# File "/path/to/bolt-python/slack_bolt/app/app.py", line 545, in dispatch
# ] = self._listener_runner.run(
# File "/path/to/bolt-python/slack_bolt/listener/thread_runner.py", line 166, in run
# self._start_lazy_function(lazy_func, request)
# File "/path/to/bolt-python/slack_bolt/listener/thread_runner.py", line 193, in _start_lazy_function
# copied_request = self._build_lazy_request(request, func_name)
# File "/path/to/bolt-python/slack_bolt/listener/thread_runner.py", line 198, in _build_lazy_request
# copied_request = create_copy(request)
# File "/path/to/bolt-python/slack_bolt/util/utils.py", line 48, in create_copy
# return copy.deepcopy(original)
# File "/path/to/python/lib/python3.9/copy.py", line 172, in deepcopy
# y = _reconstruct(x, memo, *rv)
# File "/path/to/python/lib/python3.9/copy.py", line 270, in _reconstruct
# state = deepcopy(state, memo)
# File "/path/to/python/lib/python3.9/copy.py", line 146, in deepcopy
# y = copier(x, memo)
# File "/path/to/python/lib/python3.9/copy.py", line 230, in _deepcopy_dict
# y[deepcopy(key, memo)] = deepcopy(value, memo)
# File "/path/to/python/lib/python3.9/copy.py", line 172, in deepcopy
# y = _reconstruct(x, memo, *rv)
# File "/path/to/python/lib/python3.9/copy.py", line 296, in _reconstruct
# value = deepcopy(value, memo)
# File "/path/to/python/lib/python3.9/copy.py", line 161, in deepcopy
# rv = reductor(4)
# TypeError: cannot pickle 'SSLContext' object

app.action("a")(
ack=just_ack,
lazy=[LazyClass(), async2],
)

request = self.build_valid_request()
response = app.dispatch(request)
assert response.status == 200
time.sleep(1) # wait a bit
assert self.mock_received_requests["/chat.postMessage"] == 2
70 changes: 70 additions & 0 deletions tests/scenario_tests_async/test_lazy.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,73 @@ async def async2(say):
assert response.status == 200
await asyncio.sleep(1) # wait a bit
assert self.mock_received_requests["/chat.postMessage"] == 2

@pytest.mark.asyncio
async def test_issue_545_context_copy_failure(self):
async def just_ack(ack):
await ack()

async def async1(context, say):
assert context.get("foo") == "FOO"
assert context.get("ssl_context") is None
await asyncio.sleep(0.3)
await say(text="lazy function 1")

async def async2(context, say):
assert context.get("foo") == "FOO"
assert context.get("ssl_context") is None
await asyncio.sleep(0.5)
await say(text="lazy function 2")

app = AsyncApp(
client=self.web_client,
signing_secret=self.signing_secret,
)

@app.middleware
async def set_ssl_context(context, next_):
from ssl import SSLContext

context["foo"] = "FOO"
# This causes an error when starting lazy listener executions
context["ssl_context"] = SSLContext()
await next_()

# 2021-12-13 11:52:46 ERROR Failed to run a middleware function (error: cannot pickle 'SSLContext' object)
# Traceback (most recent call last):
# File "/path/to/bolt-python/slack_bolt/app/async_app.py", line 585, in async_dispatch
# ] = await self._async_listener_runner.run(
# File "/path/to/bolt-python/slack_bolt/listener/asyncio_runner.py", line 167, in run
# self._start_lazy_function(lazy_func, request)
# File "/path/to/bolt-python/slack_bolt/listener/asyncio_runner.py", line 194, in _start_lazy_function
# copied_request = self._build_lazy_request(request, func_name)
# File "/path/to/bolt-python/slack_bolt/listener/asyncio_runner.py", line 201, in _build_lazy_request
# copied_request = create_copy(request)
# File "/path/to/bolt-python/slack_bolt/util/utils.py", line 48, in create_copy
# return copy.deepcopy(original)
# File "/path/to/python/lib/python3.9/copy.py", line 172, in deepcopy
# y = _reconstruct(x, memo, *rv)
# File "/path/to/python/lib/python3.9/copy.py", line 270, in _reconstruct
# state = deepcopy(state, memo)
# File "/path/to/python/lib/python3.9/copy.py", line 146, in deepcopy
# y = copier(x, memo)
# File "/path/to/python/lib/python3.9/copy.py", line 230, in _deepcopy_dict
# y[deepcopy(key, memo)] = deepcopy(value, memo)
# File "/path/to/python/lib/python3.9/copy.py", line 172, in deepcopy
# y = _reconstruct(x, memo, *rv)
# File "/path/to/python/lib/python3.9/copy.py", line 296, in _reconstruct
# value = deepcopy(value, memo)
# File "/path/to/python/lib/python3.9/copy.py", line 161, in deepcopy
# rv = reductor(4)
# TypeError: cannot pickle 'SSLContext' object

app.action("a")(
ack=just_ack,
lazy=[async1, async2],
)

request = self.build_valid_request()
response = await app.async_dispatch(request)
assert response.status == 200
await asyncio.sleep(1) # wait a bit
assert self.mock_received_requests["/chat.postMessage"] == 2

0 comments on commit d68418b

Please sign in to comment.