22
33import re
44import sys
5- import typing
65import fnmatch
76import posixpath
87from datetime import timedelta
98from contextlib import suppress
109from urllib .parse import unquote
1110from pathlib import PurePath , Path
12- from typing import Union , Literal , Optional
11+ from typing import TYPE_CHECKING , Literal , Self , Generator
1312from io import DEFAULT_BUFFER_SIZE , TextIOWrapper
1413
1514from 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
2222from . import accessor
2323
2424
2525def 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()`.
0 commit comments