Skip to content

Commit f7e83fc

Browse files
bbtfrliyang
andauthored
fix: fix megfile ls for alias paths (#423)
* fix megfile ls * fix unit-tests * fix unit-tests in Python 3.12 * make lint happy * add more tests --------- Co-authored-by: liyang <[email protected]>
1 parent f542b6f commit f7e83fc

File tree

7 files changed

+140
-32
lines changed

7 files changed

+140
-32
lines changed

megfile/cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
smart_open,
3030
smart_path_join,
3131
smart_readlink,
32+
smart_relpath,
3233
smart_remove,
3334
smart_rename,
3435
smart_scan_stat,
@@ -78,7 +79,7 @@ def get_echo_path(file_stat, base_path: str = "", full_path: bool = False):
7879
elif full_path:
7980
path = file_stat.path
8081
else:
81-
path = os.path.relpath(file_stat.path, start=base_path)
82+
path = smart_relpath(file_stat.path, start=base_path)
8283
return path
8384

8485

megfile/pathlike.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -636,8 +636,8 @@ def relative_to(self: Self, *other: str) -> Self:
636636
relative = path[len(other_path) :]
637637
relative = relative.lstrip("/")
638638
return type(self)(relative) # pyre-ignore[19]
639-
else:
640-
raise ValueError("%r does not start with %r" % (path, other))
639+
640+
raise ValueError("%r does not start with %r" % (path, other))
641641

642642
def with_name(self: Self, name: str) -> Self:
643643
"""Return a new path with the name changed"""
@@ -655,6 +655,21 @@ def with_suffix(self: Self, suffix: str) -> Self:
655655
raw_suffix = self.suffix
656656
return self.from_path(path[: len(path) - len(raw_suffix)] + suffix)
657657

658+
def relpath(self, start=None):
659+
"""Return the relative path."""
660+
if start is None:
661+
raise TypeError("start is required")
662+
663+
other_path = self.from_path(start).path_with_protocol
664+
path = self.path_with_protocol
665+
666+
if path.startswith(other_path):
667+
relative = path[len(other_path) :]
668+
relative = relative.lstrip("/")
669+
return relative
670+
671+
raise ValueError("%r does not start with %r" % (path, other_path))
672+
658673
def is_absolute(self) -> bool:
659674
return True
660675

megfile/smart_path.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import os
22
from configparser import ConfigParser
33
from pathlib import PurePath
4-
from typing import Dict, Tuple, Union
4+
from typing import Dict, Optional, Tuple, Union
55

66
from megfile.lib.compat import fspath
77
from megfile.lib.url import get_url_scheme
8-
from megfile.utils import classproperty
8+
from megfile.utils import cached_classproperty
99

1010
from .errors import ProtocolExistsError, ProtocolNotFoundError
1111
from .interfaces import BasePath, BaseURIPath, PathLike
@@ -54,15 +54,15 @@ def __init__(self, path: Union[PathLike, int], *other_paths: PathLike):
5454
self.path = str(pathlike)
5555
self.pathlike = pathlike
5656

57-
@classproperty
57+
@cached_classproperty
5858
def _aliases(cls) -> Dict[str, Dict[str, str]]:
5959
config_path = os.path.expanduser(aliases_config)
60-
aliases = _load_aliases_config(config_path)
61-
setattr(cls, "_aliases", aliases)
62-
return aliases
60+
return _load_aliases_config(config_path)
6361

64-
@staticmethod
65-
def _extract_protocol(path: Union[PathLike, int]) -> Tuple[str, Union[str, int]]:
62+
@classmethod
63+
def _extract_protocol(
64+
cls, path: Union[PathLike, int]
65+
) -> Tuple[str, Union[str, int]]:
6666
if isinstance(path, int):
6767
protocol = "file"
6868
path_without_protocol = path
@@ -74,27 +74,26 @@ def _extract_protocol(path: Union[PathLike, int]) -> Tuple[str, Union[str, int]]
7474
else:
7575
path_without_protocol = path[len(protocol) + 3 :]
7676
elif isinstance(path, (BaseURIPath, SmartPath)):
77-
protocol = path.protocol
78-
path_without_protocol = str(path)
77+
return str(path.protocol), str(path)
7978
elif isinstance(path, (PurePath, BasePath)):
80-
protocol, path_without_protocol = SmartPath._extract_protocol(fspath(path))
79+
return SmartPath._extract_protocol(fspath(path))
8180
else:
8281
raise ProtocolNotFoundError("protocol not found: %r" % path)
83-
return protocol, path_without_protocol
84-
85-
@classmethod
86-
def _create_pathlike(cls, path: Union[PathLike, int]) -> BaseURIPath:
87-
protocol, path_without_protocol = cls._extract_protocol(path)
8882
aliases: Dict[str, Dict[str, str]] = cls._aliases # pyre-ignore[9]
8983
if protocol in aliases:
9084
protocol = aliases[protocol]["protocol"]
91-
path = protocol + "://" + str(path_without_protocol)
85+
path = "%s://%s" % (protocol, path_without_protocol)
86+
return protocol, path
87+
88+
@classmethod
89+
def _create_pathlike(cls, path: Union[PathLike, int]) -> BaseURIPath:
90+
protocol, unaliased_path = cls._extract_protocol(path)
9291
if protocol.startswith("s3+"):
9392
protocol = "s3"
9493
if protocol not in cls._registered_protocols:
9594
raise ProtocolNotFoundError("protocol %r not found: %r" % (protocol, path))
9695
path_class = cls._registered_protocols[protocol]
97-
return path_class(path)
96+
return path_class(unaliased_path)
9897

9998
@classmethod
10099
def register(cls, path_class, override_ok: bool = False):
@@ -137,7 +136,6 @@ def register(cls, path_class, override_ok: bool = False):
137136
joinpath = _bind_function("joinpath")
138137
abspath = _bind_function("abspath")
139138
realpath = _bind_function("realpath")
140-
relpath = _bind_function("relpath")
141139
is_absolute = _bind_function("is_absolute")
142140
is_mount = _bind_function("is_mount")
143141
md5 = _bind_function("md5")
@@ -150,6 +148,16 @@ def protocol(self) -> str:
150148
def from_uri(cls, path: str):
151149
return cls(path)
152150

151+
def relpath(self, start: Optional[str] = None) -> str:
152+
"""Return the relative path of given path
153+
154+
:param start: Given start directory
155+
:returns: Relative path from start
156+
"""
157+
if start is not None:
158+
_, start = SmartPath._extract_protocol(fspath(start))
159+
return self.pathlike.relpath(start=start)
160+
153161
as_uri = _bind_function("as_uri")
154162
as_posix = _bind_function("as_posix")
155163
__lt__ = _bind_function("__lt__")
@@ -170,7 +178,6 @@ def from_uri(cls, path: str):
170178
is_mount = _bind_function("is_mount")
171179
abspath = _bind_function("abspath")
172180
realpath = _bind_function("realpath")
173-
relpath = _bind_function("relpath")
174181
iterdir = _bind_function("iterdir")
175182
cwd = _bind_function("cwd")
176183
home = _bind_function("home")

megfile/utils/__init__.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import os
55
import uuid
66
from copy import copy
7-
from functools import wraps
7+
from functools import cached_property, wraps
88
from io import (
99
BufferedIOBase,
1010
BufferedRandom,
@@ -15,6 +15,7 @@
1515
TextIOBase,
1616
TextIOWrapper,
1717
)
18+
from threading import RLock
1819
from typing import IO, Callable, Optional
1920

2021
from megfile.utils.mutex import ProcessLocal, ThreadLocal
@@ -298,3 +299,50 @@ def __delete__(self, cls_or_obj) -> None:
298299
"""
299300
# call this method only on the class, not the instance
300301
super(classproperty, self).__delete__(_get_class(cls_or_obj))
302+
303+
304+
class cached_classproperty(cached_property):
305+
"""
306+
The use this class as a decorator for your class property with cache.
307+
Example:
308+
@cached_classproperty
309+
def prop(cls):
310+
return "value"
311+
"""
312+
313+
def __init__(self, func: Callable) -> None:
314+
"""
315+
This method initializes the cached_classproperty instance.
316+
@param func: The function to be called when the property value is requested.
317+
"""
318+
super().__init__(func)
319+
# Python 3.12 removed the lock attribute from cached_property.
320+
# Maybe we should remove this in the future.
321+
# See also: https://github.com/python/cpython/pull/101890
322+
# https://github.com/python/cpython/issues/87634
323+
if not hasattr(func, "lock"):
324+
self.lock = RLock()
325+
326+
def __get__( # pyre-ignore[14]
327+
self,
328+
_,
329+
cls, # pytype: disable=signature-mismatch
330+
) -> object:
331+
"""
332+
This method gets called when a property value is requested.
333+
@param cls: The class type of the above instance.
334+
@return: The value of the property.
335+
"""
336+
if self.attrname is None:
337+
raise TypeError(
338+
"Cannot use cached_classproperty instance without calling "
339+
"__set_name__ on it."
340+
)
341+
with self.lock:
342+
# check if another thread filled cache while we awaited lock
343+
# cannot use getattr since it will cause RecursionError
344+
val = cls.__dict__[self.attrname]
345+
if val is self:
346+
val = self.func(cls)
347+
setattr(cls, self.attrname, val)
348+
return val

tests/test_smart.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,11 @@ def s3_path():
931931
yield "s3://bucket/test"
932932

933933

934+
@pytest.fixture
935+
def s3_bucket():
936+
yield "s3://bucket"
937+
938+
934939
@pytest.fixture
935940
def abs_path(fs):
936941
fs.create_file(os.path.join(os.path.dirname(__file__), "test"))
@@ -979,11 +984,14 @@ def test_smart_realpath(s3_path, abs_path, link_path):
979984
assert smart.smart_realpath(link_path) == abs_path
980985

981986

982-
def test_smart_relpath(mocker, s3_path, abs_path, rel_path):
987+
def test_smart_relpath(mocker, s3_path, s3_bucket, abs_path, rel_path):
983988
mocker.patch("os.getcwd", return_value=os.path.dirname(__file__))
984-
with pytest.raises(NotImplementedError):
989+
with pytest.raises(TypeError):
985990
assert smart.smart_relpath(s3_path) == s3_path
991+
with pytest.raises(ValueError):
992+
smart.smart_relpath(s3_path, abs_path)
986993
assert smart.smart_relpath(abs_path, os.path.dirname(__file__)) == rel_path
994+
assert smart.smart_relpath(s3_path, s3_bucket) == rel_path
987995

988996

989997
def test_smart_open_stdin(mocker):

tests/test_smart_path.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,31 +113,31 @@ def test_extract_protocol():
113113
)
114114
assert SmartPath._extract_protocol(FS_TEST_ABSOLUTE_PATH_WITH_PROTOCOL) == (
115115
FSPath.protocol,
116-
FS_TEST_ABSOLUTE_PATH,
116+
FS_TEST_ABSOLUTE_PATH_WITH_PROTOCOL,
117117
)
118118
assert SmartPath._extract_protocol(FS_TEST_RELATIVE_PATH) == (
119119
FSPath.protocol,
120120
FS_TEST_RELATIVE_PATH,
121121
)
122122
assert SmartPath._extract_protocol(FS_TEST_RELATIVE_PATH_WITH_PROTOCOL) == (
123123
FSPath.protocol,
124-
FS_TEST_RELATIVE_PATH,
124+
FS_TEST_RELATIVE_PATH_WITH_PROTOCOL,
125125
)
126126
assert SmartPath._extract_protocol(S3_TEST_PATH) == (
127127
S3Path.protocol,
128-
S3_TEST_PATH_WITHOUT_PROTOCOL,
128+
S3_TEST_PATH,
129129
)
130130
assert SmartPath._extract_protocol(HTTP_TEST_PATH) == (
131131
HttpPath.protocol,
132-
HTTP_TEST_PATH_WITHOUT_PROTOCOL,
132+
HTTP_TEST_PATH,
133133
)
134134
assert SmartPath._extract_protocol(HTTPS_TEST_PATH) == (
135135
HttpsPath.protocol,
136-
HTTPS_TEST_PATH_WITHOUT_PROTOCOL,
136+
HTTPS_TEST_PATH,
137137
)
138138
assert SmartPath._extract_protocol(STDIO_TEST_PATH) == (
139139
StdioPath.protocol,
140-
STDIO_TEST_PATH_WITHOUT_PROTOCOL,
140+
STDIO_TEST_PATH,
141141
)
142142

143143
fs_path = FSPath(FS_TEST_ABSOLUTE_PATH)

tests/utils/test_init.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
_get_class,
77
_is_pickle,
88
binary_open,
9+
cached_classproperty,
910
combine,
1011
get_human_size,
1112
necessary_params,
@@ -55,6 +56,34 @@ class Test:
5556
assert _get_class(Test) == _get_class(Test())
5657

5758

59+
def test_cached_classproperty():
60+
class Test1:
61+
count = 0
62+
63+
@cached_classproperty
64+
def test(cls):
65+
cls.count += 1
66+
return cls.count
67+
68+
assert Test1().test == 1
69+
assert Test1().test == 1
70+
assert Test1.test == 1
71+
assert Test1.test == 1
72+
73+
class Test2:
74+
count = 0
75+
76+
@cached_classproperty
77+
def test(cls):
78+
cls.count += 1
79+
return cls.count
80+
81+
assert Test2.test == 1
82+
assert Test2.test == 1
83+
assert Test2().test == 1
84+
assert Test2().test == 1
85+
86+
5887
def test__is_pickle():
5988
data = "test"
6089
fileObj = BytesIO(pickle.dumps(data))

0 commit comments

Comments
 (0)