Skip to content

Commit 9c4d37c

Browse files
authored
Enable testing for Python 3.12 and PyPy 3.10 on CI (#1435)
These changes also: * resolve/suppress some warnings * update classifiers at setup.py * bump pytest to 7.x * update constraints for flake8 to allow pip do its job and resolve compatibility issues * refactor timestamp conversion logic in order to resolve a bunch of E721 flake8 errors * fix some regexes with invalid escaping at setup.py * fix spontaneous failures of TestInteractionsWebsockets.test_interactions
1 parent 661eeec commit 9c4d37c

File tree

8 files changed

+119
-92
lines changed

8 files changed

+119
-92
lines changed

.github/workflows/ci-build.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,19 @@ jobs:
1010
build:
1111
# Avoiding -latest due to https://github.com/actions/setup-python/issues/162
1212
runs-on: ubuntu-20.04
13-
timeout-minutes: 10
13+
timeout-minutes: 15
1414
strategy:
15+
fail-fast: false
1516
matrix:
16-
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11']
17+
python-version:
18+
- '3.12'
19+
- '3.11'
20+
- '3.10'
21+
- '3.9'
22+
- '3.8'
23+
- '3.7'
24+
- '3.6'
25+
- 'pypy3.10'
1726
env:
1827
PYTHON_SLACK_SDK_MOCK_SERVER_MODE: 'threading'
1928
CI_LARGE_SOCKET_MODE_PAYLOAD_TESTING_DISABLED: '1'
@@ -23,12 +32,13 @@ jobs:
2332
uses: actions/setup-python@v4
2433
with:
2534
python-version: ${{ matrix.python-version }}
35+
cache: pip
2636
- name: Install dependencies
2737
run: |
2838
pip install -U pip wheel
2939
pip install -e ".[testing]"
3040
pip install -e ".[optional]"
31-
- name: Run validation
41+
- name: Run validation (black/flake8/pytest)
3242
run: |
3343
python setup.py validate
3444
- name: Run tests for SQLAlchemy v1.4 (backward-compatibility)

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ log_date_format = %Y-%m-%d %H:%M:%S
66
filterwarnings =
77
ignore:"@coroutine" decorator is deprecated since Python 3.8, use "async def" instead:DeprecationWarning
88
ignore:The loop argument is deprecated since Python 3.8, and scheduled for removal in Python 3.10.:DeprecationWarning
9+
ignore:slack.* package is deprecated. Please use slack_sdk.* package instead.*:UserWarning
910
asyncio_mode = auto

setup.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@
1717
long_description = readme.read()
1818

1919
validate_dependencies = [
20-
"pytest>=6.2.5,<7",
20+
"pytest>=7.0.1,<8",
2121
"pytest-asyncio<1", # for async
2222
"Flask-Sockets>=0.2,<1",
2323
"Flask>=1,<2", # TODO: Flask-Sockets is not yet compatible with Flask 2.x
2424
"Werkzeug<2", # TODO: Flask-Sockets is not yet compatible with Flask 2.x
2525
"itsdangerous==1.1.0", # TODO: Flask-Sockets is not yet compatible with Flask 2.x
2626
"Jinja2==3.0.3", # https://github.com/pallets/flask/issues/4494
2727
"pytest-cov>=2,<3",
28-
"flake8>=5,<6",
28+
# while flake8 5.x have issues with Python 3.12, flake8 6.x requires Python >= 3.8.1,
29+
# so 5.x should be kept in order to stay compatible with Python 3.6/3.7
30+
"flake8>=5.0.4,<7",
2931
# Don't change this version without running CI builds;
3032
# The latest version may not be available for older Python runtime
3133
"black==22.8.0",
@@ -123,7 +125,7 @@ def run(self):
123125
async_source = header + source
124126
async_source = re.sub(" def ", " async def ", async_source)
125127
async_source = re.sub("from asyncio import Future\n", "", async_source)
126-
async_source = re.sub("return self.api_call\(", "return await self.api_call(", async_source)
128+
async_source = re.sub(r"return self.api_call\(", "return await self.api_call(", async_source)
127129
async_source = re.sub("-> SlackResponse", "-> AsyncSlackResponse", async_source)
128130
async_source = re.sub(
129131
"from .base_client import BaseClient, SlackResponse",
@@ -132,7 +134,7 @@ def run(self):
132134
)
133135
# from slack_sdk import WebClient
134136
async_source = re.sub(
135-
"class WebClient\(BaseClient\):",
137+
r"class WebClient\(BaseClient\):",
136138
"class AsyncWebClient(AsyncBaseClient):",
137139
async_source,
138140
)
@@ -141,19 +143,19 @@ def run(self):
141143
"from slack_sdk.web.async_client import AsyncWebClient",
142144
async_source,
143145
)
144-
async_source = re.sub("= WebClient\(", "= AsyncWebClient(", async_source)
146+
async_source = re.sub(r"= WebClient\(", "= AsyncWebClient(", async_source)
145147
async_source = re.sub(
146-
" self.files_getUploadURLExternal\(",
148+
r" self.files_getUploadURLExternal\(",
147149
" await self.files_getUploadURLExternal(",
148150
async_source,
149151
)
150152
async_source = re.sub(
151-
" self.files_completeUploadExternal\(",
153+
r" self.files_completeUploadExternal\(",
152154
" await self.files_completeUploadExternal(",
153155
async_source,
154156
)
155157
async_source = re.sub(
156-
" self.files_info\(",
158+
r" self.files_info\(",
157159
" await self.files_info(",
158160
async_source,
159161
)
@@ -163,7 +165,7 @@ def run(self):
163165
async_source,
164166
)
165167
async_source = re.sub(
166-
" _attach_full_file_metadata_async\(",
168+
r" _attach_full_file_metadata_async\(",
167169
" await _attach_full_file_metadata_async(",
168170
async_source,
169171
)
@@ -178,7 +180,7 @@ def run(self):
178180
legacy_source,
179181
)
180182
legacy_source = re.sub(
181-
"class WebClient\(BaseClient\):",
183+
r"class WebClient\(BaseClient\):",
182184
"class LegacyWebClient(LegacyBaseClient):",
183185
legacy_source,
184186
)
@@ -187,7 +189,7 @@ def run(self):
187189
"from slack_sdk.web.legacy_client import LegacyWebClient",
188190
legacy_source,
189191
)
190-
legacy_source = re.sub("= WebClient\(", "= LegacyWebClient(", legacy_source)
192+
legacy_source = re.sub(r"= WebClient\(", "= LegacyWebClient(", legacy_source)
191193
with open(f"{here}/slack_sdk/web/legacy_client.py", "w") as output:
192194
output.write(legacy_source)
193195

@@ -212,8 +214,10 @@ def run(self):
212214
"Installing test dependencies ...",
213215
[sys.executable, "-m", "pip", "install"] + validate_dependencies,
214216
)
215-
self._run("Running black ...", [sys.executable, "-m", "black", f"{here}/slack"])
216-
self._run("Running black ...", [sys.executable, "-m", "black", f"{here}/slack_sdk"])
217+
218+
self._run("Running black for legacy packages ...", [sys.executable, "-m", "black", f"{here}/slack"])
219+
self._run("Running black for slack_sdk package ...", [sys.executable, "-m", "black", f"{here}/slack_sdk"])
220+
217221
self._run("Running flake8 for legacy packages ...", [sys.executable, "-m", "flake8", f"{here}/slack"])
218222
self._run("Running flake8 for slack_sdk package ...", [sys.executable, "-m", "flake8", f"{here}/slack_sdk"])
219223

@@ -301,13 +305,16 @@ def run(self):
301305
"License :: OSI Approved :: MIT License",
302306
"Programming Language :: Python",
303307
"Programming Language :: Python :: 3",
308+
"Programming Language :: Python :: 3 :: Only",
304309
"Programming Language :: Python :: 3.6",
305310
"Programming Language :: Python :: 3.7",
306311
"Programming Language :: Python :: 3.8",
307312
"Programming Language :: Python :: 3.9",
308313
"Programming Language :: Python :: 3.10",
309314
"Programming Language :: Python :: 3.11",
310315
"Programming Language :: Python :: 3.12",
316+
"Programming Language :: Python :: Implementation :: CPython",
317+
"Programming Language :: Python :: Implementation :: PyPy",
311318
],
312319
keywords="slack slack-api web-api slack-rtm websocket chat chatbot chatops",
313320
packages=find_packages(
Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,47 @@
1-
import platform
2-
import datetime
3-
4-
(major, minor, patch) = platform.python_version_tuple()
5-
is_python_3_6: bool = int(major) == 3 and int(minor) >= 6
6-
7-
utc_timezone = datetime.timezone.utc
8-
9-
10-
def _from_iso_format_to_datetime(iso_datetime_str: str) -> datetime.datetime:
11-
if is_python_3_6:
12-
elements = iso_datetime_str.split(" ")
13-
ymd = elements[0].split("-")
14-
hms = elements[1].split(":")
15-
return datetime.datetime(
16-
int(ymd[0]),
17-
int(ymd[1]),
18-
int(ymd[2]),
19-
int(hms[0]),
20-
int(hms[1]),
21-
int(hms[2]),
22-
0,
23-
utc_timezone,
24-
)
1+
import sys
2+
from datetime import datetime, timezone
3+
from typing import Type, TypeVar, Union
4+
5+
6+
def _from_iso_format_to_datetime(iso_datetime_str: str) -> datetime:
7+
if sys.version_info[:2] == (3, 6):
8+
format = "%Y-%m-%d %H:%M:%S"
9+
if "." in iso_datetime_str:
10+
format += ".%f"
11+
return datetime.strptime(iso_datetime_str, format).replace(tzinfo=timezone.utc)
2512
else:
2613
if "+" not in iso_datetime_str:
2714
iso_datetime_str += "+00:00"
28-
return datetime.datetime.fromisoformat(iso_datetime_str)
15+
return datetime.fromisoformat(iso_datetime_str)
2916

3017

3118
def _from_iso_format_to_unix_timestamp(iso_datetime_str: str) -> float:
3219
return _from_iso_format_to_datetime(iso_datetime_str).timestamp()
20+
21+
22+
TimestampType = TypeVar("TimestampType", float, int)
23+
24+
25+
def _timestamp_to_type(ts: Union[TimestampType, datetime, str], target_type: Type[TimestampType]) -> TimestampType:
26+
result: TimestampType
27+
28+
if isinstance(ts, target_type):
29+
# unnecessary type casting makes pytype happy
30+
result = target_type(ts)
31+
32+
# although a type of the timestamp is just checked,
33+
# pytype doesn't consider the following line valid:
34+
# result = ts
35+
# see https://github.com/google/pytype/issues/1012
36+
37+
elif isinstance(ts, datetime):
38+
result = target_type(ts.timestamp())
39+
elif isinstance(ts, str):
40+
try:
41+
result = target_type(ts)
42+
except ValueError:
43+
result = target_type(_from_iso_format_to_unix_timestamp(ts))
44+
else:
45+
raise ValueError(f"Unsupported data format for timestamp {ts}")
46+
47+
return result

slack_sdk/oauth/installation_store/models/bot.py

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import re
21
from datetime import datetime # type: ignore
32
from time import time
43
from typing import Optional, Union, Dict, Any, Sequence
54

6-
from slack_sdk.oauth.installation_store.internals import (
7-
_from_iso_format_to_unix_timestamp,
8-
)
5+
from slack_sdk.oauth.installation_store.internals import _timestamp_to_type
96

107

118
class Bot:
@@ -70,30 +67,17 @@ def __init__(
7067
else:
7168
self.bot_scopes = bot_scopes
7269
self.bot_refresh_token = bot_refresh_token
70+
7371
if bot_token_expires_at is not None:
74-
if type(bot_token_expires_at) == datetime:
75-
self.bot_token_expires_at = int(bot_token_expires_at.timestamp()) # type: ignore
76-
elif type(bot_token_expires_at) == str and not re.match("^\\d+$", bot_token_expires_at):
77-
self.bot_token_expires_at = int(_from_iso_format_to_unix_timestamp(bot_token_expires_at))
78-
else:
79-
self.bot_token_expires_at = int(bot_token_expires_at)
72+
self.bot_token_expires_at = _timestamp_to_type(bot_token_expires_at, int)
8073
elif bot_token_expires_in is not None:
8174
self.bot_token_expires_at = int(time()) + bot_token_expires_in
8275
else:
8376
self.bot_token_expires_at = None
77+
8478
self.is_enterprise_install = is_enterprise_install or False
8579

86-
if type(installed_at) == float:
87-
self.installed_at = installed_at # type: ignore
88-
elif type(installed_at) == datetime:
89-
self.installed_at = installed_at.timestamp() # type: ignore
90-
elif type(installed_at) == str:
91-
if re.match("^\\d+.\\d+$", installed_at):
92-
self.installed_at = float(installed_at)
93-
else:
94-
self.installed_at = _from_iso_format_to_unix_timestamp(installed_at)
95-
else:
96-
raise ValueError(f"Unsupported data format for installed_at {installed_at}")
80+
self.installed_at = _timestamp_to_type(installed_at, float)
9781

9882
self.custom_values = custom_values if custom_values is not None else {}
9983

slack_sdk/oauth/installation_store/models/installation.py

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import re
21
from datetime import datetime # type: ignore
32
from time import time
43
from typing import Optional, Union, Dict, Any, Sequence
54

6-
from slack_sdk.oauth.installation_store.internals import (
7-
_from_iso_format_to_unix_timestamp,
8-
)
5+
from slack_sdk.oauth.installation_store.internals import _timestamp_to_type
96
from slack_sdk.oauth.installation_store.models.bot import Bot
107

118

@@ -100,14 +97,9 @@ def __init__(
10097
else:
10198
self.bot_scopes = bot_scopes
10299
self.bot_refresh_token = bot_refresh_token
100+
103101
if bot_token_expires_at is not None:
104-
if type(bot_token_expires_at) == datetime:
105-
ts: float = bot_token_expires_at.timestamp() # type: ignore
106-
self.bot_token_expires_at = int(ts)
107-
elif type(bot_token_expires_at) == str and not re.match("^\\d+$", bot_token_expires_at):
108-
self.bot_token_expires_at = int(_from_iso_format_to_unix_timestamp(bot_token_expires_at))
109-
else:
110-
self.bot_token_expires_at = bot_token_expires_at # type: ignore
102+
self.bot_token_expires_at = _timestamp_to_type(bot_token_expires_at, int)
111103
elif bot_token_expires_in is not None:
112104
self.bot_token_expires_at = int(time()) + bot_token_expires_in
113105
else:
@@ -120,14 +112,9 @@ def __init__(
120112
else:
121113
self.user_scopes = user_scopes
122114
self.user_refresh_token = user_refresh_token
115+
123116
if user_token_expires_at is not None:
124-
if type(user_token_expires_at) == datetime:
125-
ts: float = user_token_expires_at.timestamp() # type: ignore
126-
self.user_token_expires_at = int(ts)
127-
elif type(user_token_expires_at) == str and not re.match("^\\d+$", user_token_expires_at):
128-
self.user_token_expires_at = int(_from_iso_format_to_unix_timestamp(user_token_expires_at))
129-
else:
130-
self.user_token_expires_at = user_token_expires_at # type: ignore
117+
self.user_token_expires_at = _timestamp_to_type(user_token_expires_at, int)
131118
elif user_token_expires_in is not None:
132119
self.user_token_expires_at = int(time()) + user_token_expires_in
133120
else:
@@ -143,17 +130,8 @@ def __init__(
143130

144131
if installed_at is None:
145132
self.installed_at = datetime.now().timestamp()
146-
elif type(installed_at) == float:
147-
self.installed_at = installed_at # type: ignore
148-
elif type(installed_at) == datetime:
149-
self.installed_at = installed_at.timestamp() # type: ignore
150-
elif type(installed_at) == str:
151-
if re.match("^\\d+.\\d+$", installed_at):
152-
self.installed_at = float(installed_at)
153-
else:
154-
self.installed_at = _from_iso_format_to_unix_timestamp(installed_at)
155133
else:
156-
raise ValueError(f"Unsupported data format for installed_at {installed_at}")
134+
self.installed_at = _timestamp_to_type(installed_at, float)
157135

158136
self.custom_values = custom_values if custom_values is not None else {}
159137

tests/slack_sdk/oauth/installation_store/test_internals.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import sys
12
import unittest
3+
from datetime import datetime, timezone
4+
5+
import pytest
26

37
from slack_sdk.oauth.installation_store import Installation, FileInstallationStore
4-
from slack_sdk.oauth.installation_store.internals import _from_iso_format_to_datetime
8+
from slack_sdk.oauth.installation_store.internals import _from_iso_format_to_datetime, _timestamp_to_type
59

610

711
class TestFile(unittest.TestCase):
@@ -14,3 +18,29 @@ def tearDown(self):
1418
def test_iso_format(self):
1519
dt = _from_iso_format_to_datetime("2021-07-14 08:00:17")
1620
self.assertEqual(dt.timestamp(), 1626249617.0)
21+
22+
23+
@pytest.mark.parametrize('ts,target_type,expected_result', [
24+
(1701209097, int, 1701209097),
25+
(datetime(2023, 11, 28, 22, 9, 7, tzinfo=timezone.utc), int, 1701209347),
26+
("1701209605", int, 1701209605),
27+
("2023-11-28 22:11:19", int, 1701209479),
28+
(1701209998.3429494, float, 1701209998.3429494),
29+
(datetime(2023, 11, 28, 22, 20, 25, 262571, tzinfo=timezone.utc), float, 1701210025.262571),
30+
("1701210054.4672053", float, 1701210054.4672053),
31+
("2023-11-28 22:21:14.745556", float, 1701210074.745556),
32+
])
33+
def test_timestamp_to_type(ts, target_type, expected_result):
34+
result = _timestamp_to_type(ts, target_type)
35+
assert result == expected_result
36+
37+
38+
def test_timestamp_to_type_invalid_str():
39+
match = "Invalid isoformat string" if sys.version_info[:2] > (3, 6) else "time data .* does not match format"
40+
with pytest.raises(ValueError, match=match):
41+
_timestamp_to_type('not-a-timestamp', int)
42+
43+
44+
def test_timestamp_to_type_unsupported_format():
45+
with pytest.raises(ValueError, match="Unsupported data format"):
46+
_timestamp_to_type({}, int)

0 commit comments

Comments
 (0)