-
Notifications
You must be signed in to change notification settings - Fork 248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
health: move to mypy #1130
health: move to mypy #1130
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1130 +/- ##
==========================================
+ Coverage 92.00% 92.01% +0.01%
==========================================
Files 195 195
Lines 6606 6616 +10
==========================================
+ Hits 6078 6088 +10
Misses 528 528 ☔ View full report in Codecov by Sentry. |
slack_bolt/app/app.py
Outdated
@@ -108,7 +109,7 @@ def __init__( | |||
user_facing_authorize_error_message: Optional[str] = None, | |||
installation_store: Optional[InstallationStore] = None, | |||
# for either only bot scope usage or v1.0.x compatibility | |||
installation_store_bot_only: Optional[bool] = None, | |||
installation_store_bot_only: bool = False, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is my biggest concern, I'm not sure why this parameter was optional
it is never used as a None
value always as a boolean
Let me know I should revert this to keep it nullable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems odd to have an optional boolean, unless the None state has a special meaning and so installation_store_bot_only
is meant to have one of three states?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@WilliamBergamin
This is intentional. When it's None, it means "unset" for the top-level parameter. L310 checks if it's explicitly set or not, and if it's set, then it compares the set value with "oauth_flow.settings.installation_store_bot_only." Developers could set both, thus implementing it this way to prioritize the top-level "installation_store_bot_only" if both are intentionally set. Don't change this logic. Adding some comments should be helpful for future maintainers!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see I've reverted these changes 🧠
@@ -191,7 +192,8 @@ def message_hello(message, say): | |||
listener_executor: Custom executor to run background tasks. If absent, the default `ThreadPoolExecutor` will | |||
be used. | |||
""" | |||
signing_secret = signing_secret or os.environ.get("SLACK_SIGNING_SECRET", "") | |||
if signing_secret is None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change allows mypy
to properly narrow the type of signing_secret
from Optional[str]
to str
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It sounds os.environ.get(name, default_value)
's typing is not smart enough but this makes sense
@@ -413,7 +417,7 @@ def _init_middleware_list( | |||
) | |||
else: | |||
raise BoltError(error_token_required()) | |||
else: | |||
elif self._authorize is not None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self._authorize
should never be None
in the following logic, this change enforces the behavior
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch (by mypy); if we change this branching to elif, having else clause, which raises an exception, would be a plus
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great suggestion, I've added a BoltError exception, but I'm unsure of its message
else:
raise BoltError("OAuthFlow or Authorize must be configured to make a Bolt app")
Let me know if this is sufficient 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message looks good to me. For consistency, move the string to slack_bolt.logger.messages
like other parts in this class do!
@@ -96,7 +96,7 @@ async def async_process( | |||
"Although the app should be installed into this workspace, " | |||
"the AuthorizeResult (returned value from authorize) for it was not found." | |||
) | |||
if req.context.response_url is not None: | |||
if req.context.response_url is not None and callable(req.context.respond): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this should change the current behavior of the code base and it should make it more resilient, but asserting callable is another option
if req.context.response_url is not None and callable(req.context.respond): | |
if req.context.response_url is not None: | |
assert callable(req.context.respond), "req.context.respond must be callable" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not a big fan of assert statements in production code because the error often does not provide sufficient information to library users. Also, I think it's fine to assume context.respond is propertly set up if response_url exists.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good 💯 I will remove the assert
changes and add type: ignore
statements when needed
@@ -69,15 +57,16 @@ def __init__( | |||
raise BoltError(error_oauth_settings_invalid_type_async()) | |||
self.settings = settings | |||
|
|||
self.settings.logger = self._logger | |||
if self._logger is not None: | |||
self.settings.logger = self._logger |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.settings.logger
should never be None
My goodness from 11 minutes to 40 seconds 🤯 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nicely done! The speed boosts are significant so it seems like a good change, but I will defer to Kaz's opinion on this - so will not put a checkmark on it 😄 sorry!
slack_bolt/app/app.py
Outdated
@@ -108,7 +109,7 @@ def __init__( | |||
user_facing_authorize_error_message: Optional[str] = None, | |||
installation_store: Optional[InstallationStore] = None, | |||
# for either only bot scope usage or v1.0.x compatibility | |||
installation_store_bot_only: Optional[bool] = None, | |||
installation_store_bot_only: bool = False, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems odd to have an optional boolean, unless the None state has a special meaning and so installation_store_bot_only
is meant to have one of three states?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@WilliamBergamin Great work!
The reasons why I chose pytype over mypy during the initial development of bolt-python a few years ago are:
- I compared mypy, pyright, and pytype; for this project, pytype was the most intelligent. It detected many type hint issues by analyzing method calls within bolt-python and test code. Slow execution was already a concern, but running it on CI builds was acceptable. Pyright looked good too, but it was not as helpful for this project in terms of the number of error/mistake detections.
- Back then, mypy displayed way more unhelpful errors than other tools. During active development, I didn't have time to handle that. Also, pytype was significantly smarter for many patterns then.
Pytype was incredibly helpful, especially for the initial development, but I notice that it does not detect many issues anymore because this project is very stable and there are fewer type hint errors now. For this stage, I am open to prioritizing execution speed over the linter's intelligence.
slack_bolt/app/app.py
Outdated
@@ -108,7 +109,7 @@ def __init__( | |||
user_facing_authorize_error_message: Optional[str] = None, | |||
installation_store: Optional[InstallationStore] = None, | |||
# for either only bot scope usage or v1.0.x compatibility | |||
installation_store_bot_only: Optional[bool] = None, | |||
installation_store_bot_only: bool = False, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@WilliamBergamin
This is intentional. When it's None, it means "unset" for the top-level parameter. L310 checks if it's explicitly set or not, and if it's set, then it compares the set value with "oauth_flow.settings.installation_store_bot_only." Developers could set both, thus implementing it this way to prioritize the top-level "installation_store_bot_only" if both are intentionally set. Don't change this logic. Adding some comments should be helpful for future maintainers!
@@ -191,7 +192,8 @@ def message_hello(message, say): | |||
listener_executor: Custom executor to run background tasks. If absent, the default `ThreadPoolExecutor` will | |||
be used. | |||
""" | |||
signing_secret = signing_secret or os.environ.get("SLACK_SIGNING_SECRET", "") | |||
if signing_secret is None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It sounds os.environ.get(name, default_value)
's typing is not smart enough but this makes sense
@@ -228,7 +230,7 @@ def message_hello(message, say): | |||
|
|||
self._before_authorize: Optional[Middleware] = None | |||
if before_authorize is not None: | |||
if isinstance(before_authorize, Callable): | |||
if callable(before_authorize): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. mypy does not like this. It's okay to change this pattern
@@ -413,7 +417,7 @@ def _init_middleware_list( | |||
) | |||
else: | |||
raise BoltError(error_token_required()) | |||
else: | |||
elif self._authorize is not None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch (by mypy); if we change this branching to elif, having else clause, which raises an exception, would be a plus
@@ -48,6 +48,7 @@ async def handle( | |||
) | |||
returned_response = await self.func(**kwargs) | |||
if returned_response is not None and isinstance(returned_response, BoltResponse): | |||
assert response is not None, "response should never be 'None' here" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this intentional? The change seems unrelated to mypy migration. let's do this in a different PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change was intentional, mypy
believes that response
can be None in this situation
I've reverted the assert
statements in favor of type: ignore
@@ -96,7 +96,7 @@ async def async_process( | |||
"Although the app should be installed into this workspace, " | |||
"the AuthorizeResult (returned value from authorize) for it was not found." | |||
) | |||
if req.context.response_url is not None: | |||
if req.context.response_url is not None and callable(req.context.respond): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not a big fan of assert statements in production code because the error often does not provide sufficient information to library users. Also, I think it's fine to assume context.respond is propertly set up if response_url exists.
@seratch thank you for sharing the context behind originally choosing pytype over mypy, now that I've used both libraries I see how pytype is more intelligent and covers more patterns then mypy, I assume this gap was larger in the past. I see how useful this can be in the early stages this project 🙇 Simultaneously mypy did catch a few edge cases that I was not aware of and may be worth addressing in the future. For example the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left one last comment about the error message string. After resolving it, you can merge this PR tomorrow in your timezone! Thanks for making these changes!
@@ -413,7 +417,7 @@ def _init_middleware_list( | |||
) | |||
else: | |||
raise BoltError(error_token_required()) | |||
else: | |||
elif self._authorize is not None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message looks good to me. For consistency, move the string to slack_bolt.logger.messages
like other parts in this class do!
slack_bolt/app/app.py
Outdated
@@ -422,6 +426,9 @@ def _init_middleware_list( | |||
user_facing_authorize_error_message=user_facing_authorize_error_message, | |||
) | |||
) | |||
else: | |||
raise BoltError("OAuthFlow or Authorize must be configured to make a Bolt app") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move the string to slack_bolt.logger.messages
slack_bolt/app/async_app.py
Outdated
@@ -421,6 +425,8 @@ def _init_async_middleware_list( | |||
user_facing_authorize_error_message=user_facing_authorize_error_message, | |||
) | |||
) | |||
else: | |||
raise BoltError("OAuthFlow or Authorize must be configured to make a Bolt app") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as above
@WilliamBergamin Can you merge #1132 first if it looks good to you? The changes by that PR may cause conflicts with this PR. If that's the case, please resolve it on this PR side 🙏 |
MyPy was found to execute much fast the PyType in this project, these changes adapt the project to use MyPy instead of PyType
From my short experience with mypy:
Concern: This tool can be strict if we let it, I think it will lead to "safer", more resilient, code. We should not let it get in the way of writing clear and efficient code.
Note: removing all the
type: ignore
comments from the project may cause breaking changes or have a ripple effect through the entire projectLet me know If we want more tests to cover these changes 🤔
Category
slack_bolt.App
and/or its core componentsslack_bolt.async_app.AsyncApp
and/or its core componentsslack_bolt.adapter
/docs
Requirements
Please read the Contributing guidelines and Code of Conduct before creating this issue or pull request. By submitting, you are agreeing to those rules.
./scripts/install_all_and_run_tests.sh
after making the changes.