Skip to content

Commit bef67e6

Browse files
Clean up the s3fs-compatible client construction
- Import Connection at the top of the module; the runtime import guarded against an import cycle that no longer exists - Replace the bare s3fs source link with a docstring describing the accepted arguments - Honor use_ssl=False, which the previous truthiness check silently dropped, and stop injecting None credential values into the session and client arguments Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent eee98af commit bef67e6

2 files changed

Lines changed: 59 additions & 34 deletions

File tree

pyathena/filesystem/s3.py

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from datetime import datetime
1111
from multiprocessing import cpu_count
1212
from re import Pattern
13-
from typing import TYPE_CHECKING, Any, cast
13+
from typing import Any, cast
1414

1515
import botocore.exceptions
1616
from boto3 import Session
@@ -22,6 +22,7 @@
2222
from fsspec.utils import tokenize
2323

2424
import pyathena
25+
from pyathena.connection import Connection
2526
from pyathena.filesystem.s3_errors import S3ClientError
2627
from pyathena.filesystem.s3_executor import S3Executor, S3ThreadPoolExecutor
2728
from pyathena.filesystem.s3_object import (
@@ -37,9 +38,6 @@
3738
)
3839
from pyathena.util import RetryConfig, retry_api_call
3940

40-
if TYPE_CHECKING:
41-
from pyathena.connection import Connection
42-
4341
_logger = logging.getLogger(__name__)
4442

4543

@@ -173,55 +171,59 @@ def __init__(
173171
self.request_kwargs = {"RequestPayer": "requester"} if requester_pays else {}
174172

175173
def _get_client_compatible_with_s3fs(self, **kwargs) -> BaseClient:
176-
"""
177-
https://github.com/fsspec/s3fs/blob/2023.4.0/s3fs/core.py#L457-L535
178-
"""
179-
from pyathena.connection import Connection
174+
"""Build a boto3 S3 client from s3fs-compatible constructor arguments.
175+
176+
Accepts the constructor arguments that s3fs users pass through fsspec
177+
storage options — ``key``/``username``, ``secret``/``password``,
178+
``token``, ``anon``, ``use_ssl``, ``endpoint_url``,
179+
``connect_timeout``/``read_timeout``, and the ``client_kwargs`` /
180+
``config_kwargs`` dictionaries — in addition to boto3 session
181+
arguments such as ``region_name`` and ``profile_name``.
180182
183+
Args:
184+
**kwargs: The filesystem constructor arguments.
185+
186+
Returns:
187+
A boto3 S3 client configured from the arguments.
188+
"""
181189
config_kwargs = deepcopy(kwargs.pop("config_kwargs", {}))
190+
client_kwargs = deepcopy(kwargs.pop("client_kwargs", {}))
191+
182192
user_agent_extra = config_kwargs.pop("user_agent_extra", None)
183-
if user_agent_extra:
184-
if pyathena.user_agent_extra not in user_agent_extra:
185-
config_kwargs.update(
186-
{"user_agent_extra": f"{pyathena.user_agent_extra} {user_agent_extra}"}
187-
)
188-
else:
189-
config_kwargs.update({"user_agent_extra": user_agent_extra})
190-
else:
191-
config_kwargs.update({"user_agent_extra": pyathena.user_agent_extra})
192-
connect_timeout = kwargs.pop("connect_timeout", None)
193-
if connect_timeout:
193+
if user_agent_extra and pyathena.user_agent_extra not in user_agent_extra:
194+
user_agent_extra = f"{pyathena.user_agent_extra} {user_agent_extra}"
195+
config_kwargs.update({"user_agent_extra": user_agent_extra or pyathena.user_agent_extra})
196+
if connect_timeout := kwargs.pop("connect_timeout", None):
194197
config_kwargs.update({"connect_timeout": connect_timeout})
195-
read_timeout = kwargs.pop("read_timeout", None)
196-
if read_timeout:
198+
if read_timeout := kwargs.pop("read_timeout", None):
197199
config_kwargs.update({"read_timeout": read_timeout})
198200

199-
client_kwargs = deepcopy(kwargs.pop("client_kwargs", {}))
200201
use_ssl = kwargs.pop("use_ssl", None)
201-
if use_ssl:
202+
if use_ssl is not None:
202203
client_kwargs.update({"use_ssl": use_ssl})
203-
endpoint_url = kwargs.pop("endpoint_url", None)
204-
if endpoint_url:
204+
if endpoint_url := kwargs.pop("endpoint_url", None):
205205
client_kwargs.update({"endpoint_url": endpoint_url})
206-
anon = kwargs.pop("anon", False)
207-
if anon:
206+
if kwargs.pop("anon", False):
208207
config_kwargs.update({"signature_version": UNSIGNED})
209208
else:
210209
creds = {
211-
"aws_access_key_id": kwargs.pop("key", kwargs.pop("username", None)),
212-
"aws_secret_access_key": kwargs.pop("secret", kwargs.pop("password", None)),
213-
"aws_session_token": kwargs.pop("token", None),
210+
key: value
211+
for key, value in {
212+
"aws_access_key_id": kwargs.pop("key", kwargs.pop("username", None)),
213+
"aws_secret_access_key": kwargs.pop("secret", kwargs.pop("password", None)),
214+
"aws_session_token": kwargs.pop("token", None),
215+
}.items()
216+
if value is not None
214217
}
215-
kwargs.update(**creds)
216-
client_kwargs.update(**creds)
218+
kwargs.update(creds)
219+
client_kwargs.update(creds)
217220

218-
config = Config(**config_kwargs)
219221
session = Session(
220222
**{k: v for k, v in kwargs.items() if k in Connection._SESSION_PASSING_ARGS}
221223
)
222224
return session.client(
223225
"s3",
224-
config=config,
226+
config=Config(**config_kwargs),
225227
**{k: v for k, v in client_kwargs.items() if k in Connection._CLIENT_PASSING_ARGS},
226228
)
227229

tests/pyathena/filesystem/test_s3.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import pytest
1616
from fsspec import Callback
1717

18+
import pyathena
1819
from pyathena.filesystem import register_s3_filesystem
1920
from pyathena.filesystem.s3 import S3File, S3FileSystem
2021
from pyathena.filesystem.s3_object import S3Object, S3ObjectType, S3StorageClass
@@ -147,6 +148,28 @@ def _make_fs():
147148
fs.version_aware = False
148149
return fs
149150

151+
def test_get_client_compatible_with_s3fs(self):
152+
# Only constructs a boto3 client; no AWS access.
153+
fs = S3FileSystem(
154+
key="test_access_key",
155+
secret="test_secret_key",
156+
region_name="us-east-1",
157+
use_ssl=False,
158+
skip_instance_cache=True,
159+
)
160+
# use_ssl=False is honored (previously dropped by a truthiness check).
161+
assert fs._client.meta.endpoint_url.startswith("http://")
162+
assert pyathena.user_agent_extra in fs._client.meta.config.user_agent_extra
163+
164+
fs = S3FileSystem(
165+
key="test_access_key",
166+
secret="test_secret_key",
167+
region_name="us-east-1",
168+
endpoint_url="http://localhost:9000",
169+
skip_instance_cache=True,
170+
)
171+
assert fs._client.meta.endpoint_url == "http://localhost:9000"
172+
150173
def test_ls_from_cache_with_cached_object(self):
151174
fs = self._make_fs()
152175
obj = S3Object(

0 commit comments

Comments
 (0)