Skip to content

Commit 29c79b8

Browse files
authored
Merge pull request #197 from liormizr/version_0_6_2_features
Version 0 6 2 features
2 parents e2166d6 + 228178d commit 29c79b8

File tree

7 files changed

+66
-80
lines changed

7 files changed

+66
-80
lines changed

Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ twine = "*"
1212
pytest-cov = "*"
1313
smart-open = "*"
1414
packaging = "*"
15+
mypy = "*"
1516

1617
[dev-packages]
1718
ipython = "*"

s3path/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pathlib import Path
66
from . import accessor
77

8-
__version__ = '0.6.2'
8+
__version__ = '0.6.3'
99
__all__ = (
1010
'Path',
1111
'register_configuration_parameter',

s3path/accessor.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,18 +155,14 @@ def mkdir(path, mode):
155155
def is_dir(path):
156156
if str(path) == path.root:
157157
return True
158-
with suppress(KeyError):
159-
return path._cache['is_dir']
160158

161159
resource, config = configuration_map.get_configuration(path)
162160
bucket = resource.Bucket(path.bucket)
163161
query = _boto3_method_with_parameters(
164162
bucket.objects.filter,
165163
kwargs={'Prefix': _generate_prefix(path)},
166164
config=config)
167-
is_dir = any(query)
168-
path._cache['is_dir'] = is_dir
169-
return is_dir
165+
return any(query)
170166

171167

172168
def exists(path):

s3path/current_version.py

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,32 @@
22

33
import re
44
import sys
5-
import typing
65
import fnmatch
76
import posixpath
87
from datetime import timedelta
98
from contextlib import suppress
109
from urllib.parse import unquote
1110
from pathlib import PurePath, Path
12-
from typing import Union, Literal, Optional
11+
from typing import TYPE_CHECKING, Literal, Self, Generator
1312
from io import DEFAULT_BUFFER_SIZE, TextIOWrapper
1413

1514
from botocore.exceptions import ClientError
1615

17-
if typing.TYPE_CHECKING:
16+
if TYPE_CHECKING:
17+
from os import PathLike
1818
import smart_open
19-
from boto3.resources.factory import ServiceResource
20-
KeyFileObjectType = Union[TextIOWrapper, smart_open.s3.Reader, smart_open.s3.MultipartWriter]
19+
from boto3.resources.base import ServiceResource
20+
KeyFileObjectType = TextIOWrapper | smart_open.s3.Reader | smart_open.s3.MultipartWriter
2121

2222
from . import accessor
2323

2424

2525
def register_configuration_parameter(
2626
path: PureS3Path,
2727
*,
28-
parameters: Optional[dict] = None,
29-
resource: Optional[ServiceResource] = None,
30-
glob_new_algorithm: Optional[bool] = None):
28+
parameters: dict | None = None,
29+
resource: ServiceResource | None = None,
30+
glob_new_algorithm: bool | None = None):
3131
if not isinstance(path, PureS3Path):
3232
raise TypeError(f'path argument have to be a {PurePath} type. got {type(path)}')
3333
if parameters and not isinstance(parameters, dict):
@@ -74,7 +74,7 @@ def __init__(self, *args):
7474
self._load_parts()
7575

7676
@classmethod
77-
def from_uri(cls, uri: str):
77+
def from_uri(cls, uri: str) -> PureS3Path:
7878
"""
7979
from_uri class method create a class instance from url
8080
@@ -88,7 +88,7 @@ def from_uri(cls, uri: str):
8888
return cls(unquoted_uri[4:])
8989

9090
@classmethod
91-
def from_bucket_key(cls, bucket: str, key: str):
91+
def from_bucket_key(cls, bucket: str | PathLike, key: str | PathLike) -> PureS3Path:
9292
"""
9393
from_bucket_key class method create a class instance from bucket, key pair's
9494
@@ -277,18 +277,9 @@ def is_mount(self) -> Literal[False]:
277277
return False
278278

279279

280-
class _PathCacheMixin:
281-
"""
282-
This is a mixin class to cache the results and path state.
283-
Note: this is experimental and will be more robust in the future.
284-
"""
285-
def __init__(self, *args, **kwargs):
286-
super().__init__(*args, **kwargs)
287-
self._cache = {}
288280

289-
290-
class S3Path(_PathNotSupportedMixin, _PathCacheMixin, PureS3Path, Path):
291-
def stat(self, *, follow_symlinks: bool = True) -> accessor.StatResult:
281+
class S3Path(_PathNotSupportedMixin, PureS3Path, Path):
282+
def stat(self, *, follow_symlinks: bool = True) -> accessor.StatResult | None:
292283
"""
293284
Returns information about this path (similarly to boto3's ObjectSummary).
294285
For compatibility with pathlib, the returned object some similar attributes like os.stat_result.
@@ -303,7 +294,7 @@ def stat(self, *, follow_symlinks: bool = True) -> accessor.StatResult:
303294
return None
304295
return accessor.stat(self, follow_symlinks=follow_symlinks)
305296

306-
def absolute(self) -> S3Path:
297+
def absolute(self) -> Self:
307298
"""
308299
Handle absolute method only if the path is already an absolute one
309300
since we have no way to compute an absolute path from a relative one in S3.
@@ -313,17 +304,19 @@ def absolute(self) -> S3Path:
313304
# We can't compute the absolute path from a relative one
314305
raise ValueError("Absolute path can't be determined for relative S3Path objects")
315306

316-
def owner(self) -> str:
307+
def owner(self, *, follow_symlinks: bool = False) -> str:
317308
"""
318309
Returns the name of the user owning the Bucket or key.
319310
Similarly to boto3's ObjectSummary owner attribute
320311
"""
321312
self._absolute_path_validation()
313+
if follow_symlinks:
314+
raise NotImplementedError(f'Setting follow_symlinks to {follow_symlinks} is unsupported on S3 service.')
322315
if not self.is_file():
323316
raise KeyError('file not found')
324317
return accessor.owner(self)
325318

326-
def rename(self, target):
319+
def rename(self, target) -> S3Path:
327320
"""
328321
Renames this file or Bucket / key prefix / key to the given target.
329322
If target exists and is a file, it will be replaced silently if the user has permission.
@@ -337,7 +330,7 @@ def rename(self, target):
337330
accessor.rename(self, target)
338331
return type(self)(target)
339332

340-
def replace(self, target):
333+
def replace(self, target) -> S3Path:
341334
"""
342335
Renames this Bucket / key prefix / key to the given target.
343336
If target points to an existing Bucket / key prefix / key, it will be unconditionally replaced.
@@ -355,13 +348,13 @@ def rmdir(self):
355348
raise FileNotFoundError()
356349
accessor.rmdir(self)
357350

358-
def samefile(self, other_path: Union[str, S3Path]) -> bool:
351+
def samefile(self, other_path: str | PathLike) -> bool:
359352
"""
360353
Returns whether this path points to the same Bucket key as other_path,
361354
Which can be either a Path object, or a string
362355
"""
363356
self._absolute_path_validation()
364-
if not isinstance(other_path, Path):
357+
if not isinstance(other_path, S3Path):
365358
other_path = type(self)(other_path)
366359
return self.bucket == other_path.bucket and self.key == other_path.key and self.is_file()
367360

@@ -402,58 +395,63 @@ def mkdir(self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False
402395
if not exist_ok:
403396
raise
404397

405-
def is_dir(self) -> bool:
398+
def is_dir(self, *, follow_symlinks: bool = False) -> bool:
406399
"""
407400
Returns True if the path points to a Bucket or a key prefix, False if it points to a full key path.
408401
False is also returned if the path doesn’t exist.
409402
Other errors (such as permission errors) are propagated.
410403
"""
411404
self._absolute_path_validation()
405+
if follow_symlinks:
406+
raise NotImplementedError(f'Setting follow_symlinks to {follow_symlinks} is unsupported on S3 service.')
412407
if self.bucket and not self.key:
413408
return True
414409
return accessor.is_dir(self)
415410

416-
def is_file(self) -> bool:
411+
def is_file(self, *, follow_symlinks: bool = False) -> bool:
417412
"""
418413
Returns True if the path points to a Bucket key, False if it points to Bucket or a key prefix.
419414
False is also returned if the path doesn’t exist.
420415
Other errors (such as permission errors) are propagated.
421416
"""
422417
self._absolute_path_validation()
418+
if follow_symlinks:
419+
raise NotImplementedError(f'Setting follow_symlinks to {follow_symlinks} is unsupported on S3 service.')
423420
if not self.bucket or not self.key:
424421
return False
425422
try:
426423
return bool(self.stat())
427424
except ClientError:
428425
return False
429426

430-
def exists(self) -> bool:
427+
def exists(self, *, follow_symlinks: bool = False) -> bool:
431428
"""
432429
Whether the path points to an existing Bucket, key or key prefix.
433430
"""
434431
self._absolute_path_validation()
432+
if follow_symlinks:
433+
raise NotImplementedError(f'Setting follow_symlinks to {follow_symlinks} is unsupported on S3 service.')
435434
if not self.bucket:
436435
return True
437436
return accessor.exists(self)
438437

439-
def iterdir(self):
438+
def iterdir(self) -> Generator['S3Path']:
440439
"""
441440
When the path points to a Bucket or a key prefix, yield path objects of the directory contents
442441
"""
443442
self._absolute_path_validation()
444443
with accessor.scandir(self) as scandir_iter:
445444
for entry in scandir_iter:
446445
path = self / entry.name
447-
path._cache['is_dir'] = entry.is_dir()
448446
yield path
449447

450448
def open(
451449
self,
452450
mode: Literal['r', 'w', 'rb', 'wb'] = 'r',
453451
buffering: int = DEFAULT_BUFFER_SIZE,
454-
encoding: Optional[str] = None,
455-
errors: Optional[str] = None,
456-
newline: Optional[str] = None) -> KeyFileObjectType:
452+
encoding: str | None = None,
453+
errors: str | None = None,
454+
newline: str | None = None) -> KeyFileObjectType:
457455
"""
458456
Opens the Bucket key pointed to by the path, returns a Key file object that you can read/write with
459457
"""
@@ -468,7 +466,11 @@ def open(
468466
errors=errors,
469467
newline=newline)
470468

471-
def glob(self, pattern: str, *, case_sensitive=None, recurse_symlinks=False):
469+
def glob(
470+
self,
471+
pattern: str, *,
472+
case_sensitive: bool | None = None,
473+
recurse_symlinks: bool = False) -> Generator['S3Path']:
472474
"""
473475
Glob the given relative pattern in the Bucket / key prefix represented by this path,
474476
yielding all matching files (of any kind)
@@ -491,7 +493,11 @@ def glob(self, pattern: str, *, case_sensitive=None, recurse_symlinks=False):
491493
selector = _Selector(self, pattern=pattern)
492494
yield from selector.select()
493495

494-
def rglob(self, pattern: str, *, case_sensitive=None, recurse_symlinks=False):
496+
def rglob(
497+
self,
498+
pattern: str, *,
499+
case_sensitive: bool | None = None,
500+
recurse_symlinks: bool = False) -> Generator['S3Path']:
495501
"""
496502
This is like calling S3Path.glob with "**/" added in front of the given relative pattern
497503
@@ -512,7 +518,7 @@ def rglob(self, pattern: str, *, case_sensitive=None, recurse_symlinks=False):
512518
selector = _Selector(self, pattern=pattern)
513519
yield from selector.select()
514520

515-
def get_presigned_url(self, expire_in: Union[timedelta, int] = 3600) -> str:
521+
def get_presigned_url(self, expire_in: timedelta | int = 3600) -> str:
516522
"""
517523
Returns a pre-signed url. Anyone with the url can make a GET request to get the file.
518524
You can set an expiration date with the expire_in argument (integer or timedelta object).
@@ -583,7 +589,11 @@ def unlink(self, missing_ok: bool = False):
583589
if not missing_ok:
584590
raise
585591

586-
def walk(self, top_down: bool = True, on_error:bool = None, follow_symlinks: bool = False):
592+
def walk(
593+
self,
594+
top_down: bool = True,
595+
on_error:bool = None,
596+
follow_symlinks: bool = False) -> Generator[tuple['S3Path', list[str], list[str]]]:
587597
if follow_symlinks:
588598
raise NotImplementedError(f'Setting follow_symlinks to {follow_symlinks} is unsupported on S3 service.')
589599

@@ -622,7 +632,7 @@ def __rtruediv__(self, key):
622632
return new_path
623633

624634
@classmethod
625-
def from_uri(cls, uri: str, *, version_id: str):
635+
def from_uri(cls, uri: str, *, version_id: str) -> PureVersionedS3Path:
626636
"""
627637
from_uri class method creates a class instance from uri and version id
628638
@@ -635,7 +645,7 @@ def from_uri(cls, uri: str, *, version_id: str):
635645
return cls(self, version_id=version_id)
636646

637647
@classmethod
638-
def from_bucket_key(cls, bucket: str, key: str, *, version_id: str):
648+
def from_bucket_key(cls, bucket: str, key: str, *, version_id: str) -> PureVersionedS3Path:
639649
"""
640650
from_bucket_key class method creates a class instance from bucket, key and version id
641651
@@ -647,7 +657,7 @@ def from_bucket_key(cls, bucket: str, key: str, *, version_id: str):
647657
self = PureS3Path.from_bucket_key(bucket=bucket, key=key)
648658
return cls(self, version_id=version_id)
649659

650-
def with_segments(self, *pathsegments):
660+
def with_segments(self, *pathsegments) -> PureVersionedS3Path:
651661
"""Construct a new path object from any number of path-like objects.
652662
Subclasses may override this method to customize how new path objects
653663
are created from methods like `iterdir()`.

s3path/old_versions.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -434,14 +434,13 @@ def _boto3_method_with_parameters(self, boto3_method, config=None, args=(), kwar
434434
return boto3_method(*args, **kwargs)
435435

436436
def _boto3_method_with_extraargs(
437-
self,
438-
boto3_method,
439-
config=None,
440-
args=(),
441-
kwargs=None,
442-
extra_args=None,
443-
allowed_extra_args=(),
444-
):
437+
self,
438+
boto3_method,
439+
config=None,
440+
args=(),
441+
kwargs=None,
442+
extra_args=None,
443+
allowed_extra_args=()):
445444
kwargs = kwargs or {}
446445
extra_args = extra_args or {}
447446
if config is not None:
@@ -459,8 +458,7 @@ def _update_smart_open_kwargs(
459458
resource,
460459
config,
461460
transport_params,
462-
smart_open_kwargs,
463-
):
461+
smart_open_kwargs):
464462
"""
465463
New Smart-Open (>=5.1.0) api
466464
Doc: https://github.com/RaRe-Technologies/smart_open/blob/develop/MIGRATING_FROM_OLDER_VERSIONS.rst
@@ -725,8 +723,7 @@ def register_configuration_parameter(
725723
*,
726724
parameters: Optional[dict] = None,
727725
resource: Optional[ServiceResource] = None,
728-
glob_new_algorithm: Optional[bool] = None,
729-
):
726+
glob_new_algorithm: Optional[bool] = None):
730727
if not isinstance(path, PureS3Path):
731728
raise TypeError(f'path argument have to be a {PurePath} type. got {type(path)}')
732729
if parameters and not isinstance(parameters, dict):
@@ -964,8 +961,6 @@ def open(
964961
Opens the Bucket key pointed to by the path, returns a Key file object that you can read/write with
965962
"""
966963
self._absolute_path_validation()
967-
if smart_open.__version__ < '4.0.0' and mode.startswith('b'):
968-
mode = ''.join(reversed(mode))
969964
return self._accessor.open(
970965
self,
971966
mode=mode,

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
long_description = fh.read()
66
setup(
77
name='s3path',
8-
version='0.6.2',
8+
version='0.6.3',
99
url='https://github.com/liormizr/s3path',
1010
author='Lior Mizrahi',
1111
author_email='li.mizr@gmail.com',

0 commit comments

Comments
 (0)