dvc-objects 3.0.6__tar.gz → 4.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/.github/workflows/tests.yml +1 -0
  2. {dvc-objects-3.0.6/src/dvc_objects.egg-info → dvc-objects-4.0.0}/PKG-INFO +2 -2
  3. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/pyproject.toml +2 -2
  4. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/db.py +4 -2
  5. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/executors.py +1 -1
  6. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/fs/base.py +16 -12
  7. dvc-objects-4.0.0/src/dvc_objects/fs/callbacks.py +57 -0
  8. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/fs/generic.py +44 -25
  9. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/fs/utils.py +5 -2
  10. {dvc-objects-3.0.6 → dvc-objects-4.0.0/src/dvc_objects.egg-info}/PKG-INFO +2 -2
  11. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects.egg-info/SOURCES.txt +1 -0
  12. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects.egg-info/requires.txt +1 -1
  13. dvc-objects-4.0.0/tests/fs/test_callbacks.py +17 -0
  14. dvc-objects-4.0.0/tests/fs/test_generic.py +120 -0
  15. dvc-objects-3.0.6/src/dvc_objects/fs/callbacks.py +0 -178
  16. dvc-objects-3.0.6/tests/fs/test_callbacks.py +0 -163
  17. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/.cruft.json +0 -0
  18. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/.gitattributes +0 -0
  19. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/.github/dependabot.yml +0 -0
  20. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/.github/workflows/release.yml +0 -0
  21. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/.github/workflows/update-template.yaml +0 -0
  22. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/.gitignore +0 -0
  23. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/.pre-commit-config.yaml +0 -0
  24. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/CODE_OF_CONDUCT.rst +0 -0
  25. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/CONTRIBUTING.rst +0 -0
  26. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/LICENSE +0 -0
  27. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/README.rst +0 -0
  28. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/noxfile.py +0 -0
  29. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/setup.cfg +0 -0
  30. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/__init__.py +0 -0
  31. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/_tqdm.py +0 -0
  32. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/compat.py +0 -0
  33. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/errors.py +0 -0
  34. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/fs/__init__.py +0 -0
  35. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/fs/errors.py +0 -0
  36. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/fs/local.py +0 -0
  37. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/fs/memory.py +0 -0
  38. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/fs/scheme.py +0 -0
  39. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/fs/system.py +0 -0
  40. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/obj.py +0 -0
  41. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/py.typed +0 -0
  42. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects/transfer.py +0 -0
  43. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects.egg-info/dependency_links.txt +0 -0
  44. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/src/dvc_objects.egg-info/top_level.txt +0 -0
  45. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/tests/__init__.py +0 -0
  46. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/tests/benchmarks/__init__.py +0 -0
  47. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/tests/benchmarks/test_fs.py +0 -0
  48. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/tests/conftest.py +0 -0
  49. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/tests/fs/__init__.py +0 -0
  50. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/tests/fs/test_localfs.py +0 -0
  51. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/tests/fs/test_system.py +0 -0
  52. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/tests/fs/test_utils.py +0 -0
  53. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/tests/test_odb.py +0 -0
  54. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/tests/test_reflink.py +0 -0
  55. {dvc-objects-3.0.6 → dvc-objects-4.0.0}/tests/test_transfer.py +0 -0
@@ -24,6 +24,7 @@ jobs:
24
24
  pyv: ['3.8', '3.9', '3.10', '3.11', '3.12']
25
25
  include:
26
26
  - {os: ubuntu-latest, pyv: 'pypy3.8'}
27
+ - {os: macos-14, pyv: '3.12'}
27
28
 
28
29
  steps:
29
30
  - name: Check out the repository
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dvc-objects
3
- Version: 3.0.6
3
+ Version: 4.0.0
4
4
  Summary: dvc objects - filesystem and object-db level abstractions to use in dvc and dvc-data
5
5
  Author-email: Iterative <support@dvc.org>
6
6
  License: Apache-2.0
@@ -17,7 +17,7 @@ Requires-Python: >=3.8
17
17
  Description-Content-Type: text/x-rst
18
18
  License-File: LICENSE
19
19
  Requires-Dist: funcy>=1.14; python_version < "3.12"
20
- Requires-Dist: fsspec>=2022.10.0
20
+ Requires-Dist: fsspec>=2024.2.0
21
21
  Provides-Extra: tests
22
22
  Requires-Dist: pytest<8,>=7; extra == "tests"
23
23
  Requires-Dist: pytest-sugar; extra == "tests"
@@ -23,7 +23,7 @@ requires-python = ">=3.8"
23
23
  dynamic = ["version"]
24
24
  dependencies = [
25
25
  "funcy>=1.14; python_version < '3.12'",
26
- "fsspec>=2022.10.0",
26
+ "fsspec>=2024.2.0",
27
27
  ]
28
28
 
29
29
  [project.urls]
@@ -104,7 +104,7 @@ module = [
104
104
  ]
105
105
 
106
106
  [tool.codespell]
107
- ignore-words-list = " "
107
+ ignore-words-list = "cachable,"
108
108
 
109
109
  [tool.ruff]
110
110
  ignore = [
@@ -16,13 +16,15 @@ from typing import (
16
16
  cast,
17
17
  )
18
18
 
19
+ from fsspec.callbacks import DEFAULT_CALLBACK
20
+
19
21
  from .errors import ObjectDBPermissionError
20
- from .fs.callbacks import DEFAULT_CALLBACK
21
22
  from .obj import Object
22
23
 
23
24
  if TYPE_CHECKING:
25
+ from fsspec import Callback
26
+
24
27
  from .fs.base import AnyFSPath, FileSystem
25
- from .fs.callbacks import Callback
26
28
 
27
29
 
28
30
  logger = logging.getLogger(__name__)
@@ -18,7 +18,7 @@ from typing import (
18
18
  TypeVar,
19
19
  )
20
20
 
21
- from .fs.callbacks import Callback
21
+ from fsspec import Callback
22
22
 
23
23
  _T = TypeVar("_T")
24
24
 
@@ -27,15 +27,12 @@ from urllib.parse import urlsplit, urlunsplit
27
27
 
28
28
  import fsspec
29
29
  from fsspec.asyn import get_loop
30
+ from fsspec.callbacks import DEFAULT_CALLBACK
30
31
 
31
32
  from dvc_objects.compat import cached_property
32
33
  from dvc_objects.executors import ThreadPoolExecutor, batch_coros
33
34
 
34
- from .callbacks import (
35
- DEFAULT_CALLBACK,
36
- wrap_and_branch_callback,
37
- wrap_file,
38
- )
35
+ from .callbacks import wrap_file
39
36
  from .errors import RemoteMissingDepsError
40
37
 
41
38
  if TYPE_CHECKING:
@@ -706,9 +703,14 @@ class FileSystem:
706
703
 
707
704
  callback.set_size(len(from_infos))
708
705
  executor = ThreadPoolExecutor(max_workers=jobs, cancel_on_error=True)
706
+
707
+ def put_file(from_path, to_path):
708
+ with callback.branched(from_path, to_path) as child:
709
+ return self.put_file(from_path, to_path, callback=child)
710
+
709
711
  with executor:
710
- put_file = wrap_and_branch_callback(callback, self.put_file)
711
- list(executor.imap_unordered(put_file, from_infos, to_infos))
712
+ it = executor.imap_unordered(put_file, from_infos, to_infos)
713
+ list(callback.wrap(it))
712
714
 
713
715
  def get(
714
716
  self,
@@ -724,9 +726,8 @@ class FileSystem:
724
726
 
725
727
  def get_file(rpath, lpath, **kwargs):
726
728
  localfs.makedirs(localfs.parent(lpath), exist_ok=True)
727
- self.fs.get_file(rpath, lpath, **kwargs)
728
-
729
- get_file = wrap_and_branch_callback(callback, get_file)
729
+ with callback.branched(rpath, lpath) as child:
730
+ self.fs.get_file(rpath, lpath, callback=child, **kwargs)
730
731
 
731
732
  if isinstance(from_info, list) and isinstance(to_info, list):
732
733
  from_infos: List[AnyFSPath] = from_info
@@ -737,7 +738,9 @@ class FileSystem:
737
738
 
738
739
  if not self.isdir(from_info):
739
740
  callback.set_size(1)
740
- return get_file(from_info, to_info)
741
+ get_file(from_info, to_info)
742
+ callback.relative_update()
743
+ return
741
744
 
742
745
  from_infos = list(self.find(from_info))
743
746
  if not from_infos:
@@ -760,7 +763,8 @@ class FileSystem:
760
763
  callback.set_size(len(from_infos))
761
764
  executor = ThreadPoolExecutor(max_workers=jobs, cancel_on_error=True)
762
765
  with executor:
763
- list(executor.imap_unordered(get_file, from_infos, to_infos))
766
+ it = executor.imap_unordered(get_file, from_infos, to_infos)
767
+ list(callback.wrap(it))
764
768
 
765
769
  def ukey(self, path: AnyFSPath) -> str:
766
770
  return self.fs.ukey(path)
@@ -0,0 +1,57 @@
1
+ from functools import wraps
2
+ from typing import TYPE_CHECKING, BinaryIO, Optional, Type, cast
3
+
4
+ import fsspec
5
+ from fsspec.callbacks import DEFAULT_CALLBACK, Callback, NoOpCallback
6
+
7
+ if TYPE_CHECKING:
8
+ from typing import Union
9
+
10
+ from tqdm import tqdm
11
+
12
+
13
+ __all__ = ["Callback", "NoOpCallback", "TqdmCallback", "DEFAULT_CALLBACK"]
14
+
15
+
16
+ class CallbackStream:
17
+ def __init__(self, stream, callback: Callback):
18
+ self.stream = stream
19
+
20
+ @wraps(stream.read)
21
+ def read(*args, **kwargs):
22
+ data = stream.read(*args, **kwargs)
23
+ callback.relative_update(len(data))
24
+ return data
25
+
26
+ self.read = read
27
+
28
+ def __getattr__(self, attr):
29
+ return getattr(self.stream, attr)
30
+
31
+
32
+ class TqdmCallback(fsspec.callbacks.TqdmCallback):
33
+ def __init__(
34
+ self,
35
+ size: Optional[int] = None,
36
+ value: int = 0,
37
+ progress_bar: Optional["tqdm"] = None,
38
+ tqdm_cls: Optional[Type["tqdm"]] = None,
39
+ **tqdm_kwargs,
40
+ ):
41
+ from dvc_objects._tqdm import Tqdm
42
+
43
+ tqdm_kwargs.pop("total", None)
44
+ tqdm_cls = tqdm_cls or Tqdm
45
+ super().__init__(
46
+ tqdm_kwargs=tqdm_kwargs, tqdm_cls=tqdm_cls, size=size, value=value
47
+ )
48
+ if progress_bar is None:
49
+ self.tqdm = progress_bar
50
+
51
+ def branched(self, path_1: "Union[str, BinaryIO]", path_2: str, **kwargs):
52
+ desc = path_1 if isinstance(path_1, str) else path_2
53
+ return TqdmCallback(bytes=True, desc=desc)
54
+
55
+
56
+ def wrap_file(file, callback: Callback) -> BinaryIO:
57
+ return cast(BinaryIO, CallbackStream(file, callback))
@@ -7,16 +7,17 @@ from functools import wraps
7
7
  from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union
8
8
 
9
9
  from fsspec.asyn import get_loop
10
+ from fsspec.callbacks import DEFAULT_CALLBACK
10
11
 
11
12
  from dvc_objects.executors import ThreadPoolExecutor, batch_coros
12
13
 
13
- from .callbacks import DEFAULT_CALLBACK, wrap_and_branch_callback
14
14
  from .local import LocalFileSystem, localfs
15
15
  from .utils import as_atomic, umask
16
16
 
17
17
  if TYPE_CHECKING:
18
+ from fsspec import Callback
19
+
18
20
  from .base import AnyFSPath, FileSystem
19
- from .callbacks import Callback
20
21
 
21
22
  logger = logging.getLogger(__name__)
22
23
 
@@ -103,7 +104,6 @@ def copy(
103
104
  )
104
105
 
105
106
  jobs = batch_size or to_fs.jobs
106
- put_file = wrap_and_branch_callback(callback, to_fs.put_file)
107
107
  put_file_kwargs = {}
108
108
  if hasattr(to_fs.fs, "max_concurrency"):
109
109
  put_file_kwargs["max_concurrency"] = jobs if len(from_path) == 1 else 1
@@ -112,9 +112,10 @@ def copy(
112
112
  try:
113
113
  with from_fs.open(from_p, mode="rb") as fobj:
114
114
  size = from_fs.size(from_p)
115
- return put_file(
116
- fobj, to_p, size=size, callback=callback, **put_file_kwargs
117
- )
115
+ with callback.branched(from_p, to_p) as child:
116
+ return to_fs.put_file(
117
+ fobj, to_p, size=size, callback=child, **put_file_kwargs
118
+ )
118
119
  except Exception as exc: # noqa: BLE001
119
120
  if on_error is not None:
120
121
  on_error(from_p, to_p, exc)
@@ -122,14 +123,16 @@ def copy(
122
123
  raise
123
124
 
124
125
  if len(from_path) == 1:
125
- return _copy_one(from_path[0], to_path[0])
126
+ _copy_one(from_path[0], to_path[0])
127
+ return callback.relative_update()
126
128
 
127
129
  executor = ThreadPoolExecutor(max_workers=jobs, cancel_on_error=True)
128
130
  with executor:
129
- list(executor.imap_unordered(_copy_one, from_path, to_path))
131
+ it = executor.imap_unordered(_copy_one, from_path, to_path)
132
+ list(callback.wrap(it))
130
133
 
131
134
 
132
- def _put(
135
+ def _put( # noqa: C901
133
136
  from_paths: List["AnyFSPath"],
134
137
  to_fs: "FileSystem",
135
138
  to_paths: List["AnyFSPath"],
@@ -138,14 +141,16 @@ def _put(
138
141
  on_error: Optional[TransferErrorHandler] = None,
139
142
  ) -> None:
140
143
  jobs = batch_size or to_fs.jobs
141
- put_file = wrap_and_branch_callback(callback, to_fs.put_file)
142
144
  put_file_kwargs = {}
143
145
  if hasattr(to_fs.fs, "max_concurrency"):
144
146
  put_file_kwargs["max_concurrency"] = jobs if len(from_paths) == 1 else 1
145
147
 
146
148
  def _put_one(from_path: "AnyFSPath", to_path: "AnyFSPath"):
147
149
  try:
148
- return put_file(from_path, to_path, callback=callback, **put_file_kwargs)
150
+ with callback.branched(from_path, to_path) as child:
151
+ return to_fs.put_file(
152
+ from_path, to_path, callback=child, **put_file_kwargs
153
+ )
149
154
  except Exception as exc: # noqa: BLE001
150
155
  if on_error is not None:
151
156
  on_error(from_path, to_path, exc)
@@ -153,18 +158,27 @@ def _put(
153
158
  raise
154
159
 
155
160
  if len(from_paths) == 1:
156
- return _put_one(from_paths[0], to_paths[0])
161
+ _put_one(from_paths[0], to_paths[0])
162
+ return callback.relative_update()
157
163
 
158
164
  if to_fs.fs.async_impl:
159
- put_coro = wrap_and_branch_callback(callback, to_fs.fs._put_file)
165
+ to_fs_async = to_fs.fs
166
+
167
+ async def put_coro(from_path, to_path, **kwargs):
168
+ with callback.branched(from_path, to_path) as child:
169
+ return await to_fs_async._put_file(
170
+ from_path, to_path, callback=child, **kwargs
171
+ )
172
+
160
173
  loop = get_loop()
161
174
  fut = asyncio.run_coroutine_threadsafe(
162
175
  batch_coros(
163
176
  [
164
- put_coro(from_path, to_path, callback=callback, **put_file_kwargs)
177
+ put_coro(from_path, to_path, **put_file_kwargs)
165
178
  for from_path, to_path in zip(from_paths, to_paths)
166
179
  ],
167
180
  batch_size=jobs,
181
+ callback=callback,
168
182
  return_exceptions=True,
169
183
  ),
170
184
  loop,
@@ -179,7 +193,8 @@ def _put(
179
193
 
180
194
  executor = ThreadPoolExecutor(max_workers=jobs, cancel_on_error=True)
181
195
  with executor:
182
- list(executor.imap_unordered(_put_one, from_paths, to_paths))
196
+ it = executor.imap_unordered(_put_one, from_paths, to_paths)
197
+ list(callback.wrap(it))
183
198
 
184
199
 
185
200
  def _get( # noqa: C901
@@ -191,7 +206,6 @@ def _get( # noqa: C901
191
206
  on_error: Optional[TransferErrorHandler] = None,
192
207
  ) -> None:
193
208
  jobs = batch_size or from_fs.jobs
194
- get_file = wrap_and_branch_callback(callback, from_fs.get_file)
195
209
  get_file_kwargs = {}
196
210
  if hasattr(from_fs.fs, "max_concurrency"):
197
211
  get_file_kwargs["max_concurrency"] = jobs if len(from_paths) == 1 else 1
@@ -199,9 +213,10 @@ def _get( # noqa: C901
199
213
  def _get_one(from_path: "AnyFSPath", to_path: "AnyFSPath"):
200
214
  with as_atomic(localfs, to_path, create_parents=True) as tmp_file:
201
215
  try:
202
- return get_file(
203
- from_path, tmp_file, callback=callback, **get_file_kwargs
204
- )
216
+ with callback.branched(from_path, to_path) as child:
217
+ return from_fs.get_file(
218
+ from_path, tmp_file, callback=child, **get_file_kwargs
219
+ )
205
220
  except Exception as exc: # noqa: BLE001
206
221
  if on_error is not None:
207
222
  on_error(from_path, to_path, exc)
@@ -209,16 +224,18 @@ def _get( # noqa: C901
209
224
  raise
210
225
 
211
226
  if len(from_paths) == 1:
212
- return _get_one(from_paths[0], to_paths[0])
227
+ _get_one(from_paths[0], to_paths[0])
228
+ return callback.relative_update()
213
229
 
214
230
  if from_fs.fs.async_impl:
231
+ from_async_fs = from_fs.fs
215
232
 
216
233
  async def _get_one_coro(from_path: "AnyFSPath", to_path: "AnyFSPath"):
217
- get_coro = wrap_and_branch_callback(callback, from_fs.fs._get_file)
218
234
  with as_atomic(localfs, to_path, create_parents=True) as tmp_file:
219
- return await get_coro(
220
- from_path, tmp_file, callback=callback, **get_file_kwargs
221
- )
235
+ with callback.branched(from_path, to_path) as child:
236
+ return await from_async_fs._get_file(
237
+ from_path, tmp_file, callback=child, **get_file_kwargs
238
+ )
222
239
 
223
240
  loop = get_loop()
224
241
  fut = asyncio.run_coroutine_threadsafe(
@@ -229,6 +246,7 @@ def _get( # noqa: C901
229
246
  ],
230
247
  batch_size=jobs,
231
248
  return_exceptions=True,
249
+ callback=callback,
232
250
  ),
233
251
  loop,
234
252
  )
@@ -242,7 +260,8 @@ def _get( # noqa: C901
242
260
 
243
261
  executor = ThreadPoolExecutor(max_workers=jobs, cancel_on_error=True)
244
262
  with executor:
245
- list(executor.imap_unordered(_get_one, from_paths, to_paths))
263
+ it = executor.imap_unordered(_get_one, from_paths, to_paths)
264
+ list(callback.wrap(it))
246
265
 
247
266
 
248
267
  def _try_links(
@@ -10,14 +10,17 @@ from contextlib import contextmanager, suppress
10
10
  from secrets import token_urlsafe
11
11
  from typing import TYPE_CHECKING, Any, Collection, Dict, Iterator, Optional, Set, Union
12
12
 
13
+ from fsspec.callbacks import DEFAULT_CALLBACK
14
+
13
15
  from dvc_objects.executors import ThreadPoolExecutor
14
16
 
15
17
  from . import system
16
- from .callbacks import DEFAULT_CALLBACK, wrap_file
18
+ from .callbacks import wrap_file
17
19
 
18
20
  if TYPE_CHECKING:
21
+ from fsspec import Callback
22
+
19
23
  from .base import AnyFSPath, FileSystem
20
- from .callbacks import Callback
21
24
 
22
25
 
23
26
  logger = logging.getLogger(__name__)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dvc-objects
3
- Version: 3.0.6
3
+ Version: 4.0.0
4
4
  Summary: dvc objects - filesystem and object-db level abstractions to use in dvc and dvc-data
5
5
  Author-email: Iterative <support@dvc.org>
6
6
  License: Apache-2.0
@@ -17,7 +17,7 @@ Requires-Python: >=3.8
17
17
  Description-Content-Type: text/x-rst
18
18
  License-File: LICENSE
19
19
  Requires-Dist: funcy>=1.14; python_version < "3.12"
20
- Requires-Dist: fsspec>=2022.10.0
20
+ Requires-Dist: fsspec>=2024.2.0
21
21
  Provides-Extra: tests
22
22
  Requires-Dist: pytest<8,>=7; extra == "tests"
23
23
  Requires-Dist: pytest-sugar; extra == "tests"
@@ -45,6 +45,7 @@ tests/benchmarks/__init__.py
45
45
  tests/benchmarks/test_fs.py
46
46
  tests/fs/__init__.py
47
47
  tests/fs/test_callbacks.py
48
+ tests/fs/test_generic.py
48
49
  tests/fs/test_localfs.py
49
50
  tests/fs/test_system.py
50
51
  tests/fs/test_utils.py
@@ -1,4 +1,4 @@
1
- fsspec>=2022.10.0
1
+ fsspec>=2024.2.0
2
2
 
3
3
  [:python_version < "3.12"]
4
4
  funcy>=1.14
@@ -0,0 +1,17 @@
1
+ from fsspec.callbacks import Callback
2
+
3
+ from dvc_objects.fs.callbacks import wrap_file
4
+
5
+
6
+ def test_wrap_file(memfs):
7
+ memfs.pipe_file("/file", b"foo\n")
8
+
9
+ callback = Callback()
10
+
11
+ callback.set_size(4)
12
+ with memfs.open("/file", mode="rb") as f:
13
+ wrapped = wrap_file(f, callback)
14
+ assert wrapped.read() == b"foo\n"
15
+
16
+ assert callback.value == 4
17
+ assert callback.size == 4
@@ -0,0 +1,120 @@
1
+ import pytest
2
+ from fsspec import Callback
3
+ from fsspec.asyn import AsyncFileSystem
4
+ from fsspec.implementations.memory import MemoryFileSystem as MemoryFS
5
+
6
+ from dvc_objects.fs.generic import copy, transfer
7
+ from dvc_objects.fs.local import LocalFileSystem
8
+ from dvc_objects.fs.memory import MemoryFileSystem
9
+
10
+
11
+ def awrap(fn):
12
+ async def inner(self, *args, **kwargs):
13
+ return fn(self.fs, *args, **kwargs)
14
+
15
+ return inner
16
+
17
+
18
+ class AsyncMemoryFS(AsyncFileSystem):
19
+ cachable = False
20
+
21
+ def __init__(self, *args, **kwargs):
22
+ super().__init__(*args, **kwargs)
23
+ self.fs = MemoryFS()
24
+ self.fs.store = {}
25
+ self.fs.pseudo_dirs = [""]
26
+
27
+ def _open(self, *args, **kwargs):
28
+ return self.fs.open(*args, **kwargs)
29
+
30
+ _info = awrap(MemoryFS.info)
31
+ _ls = awrap(MemoryFS.ls)
32
+ _mkdir = awrap(MemoryFS.mkdir)
33
+ _makedirs = awrap(MemoryFS.makedirs)
34
+ _get_file = awrap(MemoryFS.get_file)
35
+ _put_file = awrap(MemoryFS.put_file)
36
+ _cat_file = awrap(MemoryFS.cat_file)
37
+ _pipe_file = awrap(MemoryFS.pipe_file)
38
+ _cp_file = awrap(MemoryFS.cp_file)
39
+ _rm_file = awrap(MemoryFS.rm_file)
40
+
41
+
42
+ fs_clses = [
43
+ LocalFileSystem,
44
+ lambda: MemoryFileSystem(global_store=False),
45
+ lambda: MemoryFileSystem(fs=AsyncMemoryFS()),
46
+ ]
47
+ fs_clses[1].__name__ = MemoryFileSystem.__name__ # type: ignore[attr-defined]
48
+ fs_clses[2].__name__ = "Async" + MemoryFileSystem.__name__ # type: ignore[attr-defined]
49
+
50
+
51
+ @pytest.mark.parametrize("files", [{"foo": b"foo"}, {"foo": b"foo", "bar": b"barr"}])
52
+ @pytest.mark.parametrize("fs_cls1", fs_clses)
53
+ @pytest.mark.parametrize("fs_cls2", fs_clses)
54
+ def test_copy(tmp_path, files, fs_cls1, fs_cls2, mocker):
55
+ fs1, fs2 = fs_cls1(), fs_cls2()
56
+ fs1_root = tmp_path if isinstance(fs1, LocalFileSystem) else fs1.root_marker
57
+ fs2_root = tmp_path if isinstance(fs2, LocalFileSystem) else fs2.root_marker
58
+ src_root = fs1.join(fs1_root, "src")
59
+ dest_root = fs2.join(fs2_root, "dest")
60
+
61
+ src_files = {fs1.join(src_root, f): c for f, c in files.items()}
62
+ dest_files = {fs2.join(dest_root, f): c for f, c in files.items()}
63
+ fs1.mkdir(src_root)
64
+ fs1.pipe(src_files)
65
+
66
+ callback = Callback()
67
+ spy_close = mocker.spy(Callback, "close")
68
+ child_callbacks = [Callback() for _ in files]
69
+
70
+ branched = mocker.patch.object(callback, "branched", side_effect=child_callbacks)
71
+ copy(fs1, list(src_files), fs2, list(dest_files), callback=callback)
72
+
73
+ assert fs2.cat(list(dest_files)) == dest_files
74
+
75
+ n = len(files)
76
+ # assert main callback works
77
+ assert callback.value == n
78
+ assert callback.size is None # does not set sizes
79
+ # assert child callbacks are handled correctly
80
+ assert branched.call_count == n, f"expected branched to be called {n} times"
81
+ assert spy_close.call_count == n, f"expected close to be called {n} times"
82
+
83
+ if isinstance(fs1, LocalFileSystem) and isinstance(fs2, LocalFileSystem):
84
+ # localfs copy avoids calling set_size or update if fs supports reflink
85
+ # or, file size is less than 1GB
86
+ return
87
+ assert {c.size for c in child_callbacks} == {len(c) for c in files.values()}
88
+ assert {c.value for c in child_callbacks} == {len(c) for c in files.values()}
89
+
90
+
91
+ @pytest.mark.parametrize("files", [{"foo": b"foo"}, {"foo": b"foo", "bar": b"barr"}])
92
+ @pytest.mark.parametrize(
93
+ "link_type",
94
+ [
95
+ pytest.param("reflink", marks=pytest.mark.xfail(reason="unsupported")),
96
+ "symlink",
97
+ "hardlink",
98
+ "copy",
99
+ ],
100
+ )
101
+ def test_transfer_between_local_fses(mocker, tmp_path, files, link_type):
102
+ fs = LocalFileSystem()
103
+ fs.mkdir(fs.join(tmp_path, "src"))
104
+ fs.mkdir(fs.join(tmp_path, "dest"))
105
+
106
+ src_files = {fs.join(tmp_path, "src", f): c for f, c in files.items()}
107
+ dest_files = {fs.join(tmp_path, "dest", f): c for f, c in files.items()}
108
+
109
+ fs.pipe(src_files)
110
+
111
+ callback = Callback()
112
+
113
+ branched = mocker.patch.object(callback, "branched")
114
+ transfer(
115
+ fs, list(src_files), fs, list(dest_files), callback=callback, links=[link_type]
116
+ )
117
+ assert fs.cat([str(tmp_path / "dest" / file) for file in files]) == dest_files
118
+ assert callback.value == len(files)
119
+ assert callback.size is None # does not set sizes
120
+ assert branched.call_count == (len(files) if link_type == "copy" else 0)
@@ -1,178 +0,0 @@
1
- import asyncio
2
- from functools import wraps
3
- from typing import TYPE_CHECKING, Any, BinaryIO, Callable, Dict, Optional, TypeVar, cast
4
-
5
- import fsspec
6
-
7
- if TYPE_CHECKING:
8
- from typing import Union
9
-
10
- from dvc_objects._tqdm import Tqdm
11
-
12
- F = TypeVar("F", bound=Callable)
13
-
14
-
15
- class CallbackStream:
16
- def __init__(self, stream, callback: fsspec.Callback):
17
- self.stream = stream
18
-
19
- @wraps(stream.read)
20
- def read(*args, **kwargs):
21
- data = stream.read(*args, **kwargs)
22
- callback.relative_update(len(data))
23
- return data
24
-
25
- self.read = read
26
-
27
- def __getattr__(self, attr):
28
- return getattr(self.stream, attr)
29
-
30
-
31
- class ScopedCallback(fsspec.Callback):
32
- def __enter__(self):
33
- return self
34
-
35
- def __exit__(self, *exc_args):
36
- self.close()
37
-
38
- def close(self):
39
- """Handle here on exit."""
40
-
41
- def branch(
42
- self,
43
- path_1: "Union[str, BinaryIO]",
44
- path_2: str,
45
- kwargs: Dict[str, Any],
46
- child: Optional["Callback"] = None,
47
- ) -> "Callback":
48
- child = kwargs["callback"] = child or DEFAULT_CALLBACK
49
- return child
50
-
51
-
52
- class Callback(ScopedCallback):
53
- def absolute_update(self, value: int) -> None:
54
- value = value if value is not None else self.value
55
- return super().absolute_update(value)
56
-
57
- @classmethod
58
- def as_callback(
59
- cls, maybe_callback: Optional[fsspec.Callback] = None
60
- ) -> "Callback":
61
- if maybe_callback is None:
62
- return DEFAULT_CALLBACK
63
- if isinstance(maybe_callback, Callback):
64
- return maybe_callback
65
- return FsspecCallbackWrapper(maybe_callback)
66
-
67
-
68
- class NoOpCallback(Callback, fsspec.callbacks.NoOpCallback):
69
- pass
70
-
71
-
72
- class TqdmCallback(Callback):
73
- def __init__(
74
- self,
75
- size: Optional[int] = None,
76
- value: int = 0,
77
- progress_bar: Optional["Tqdm"] = None,
78
- **tqdm_kwargs,
79
- ):
80
- from dvc_objects._tqdm import Tqdm
81
-
82
- tqdm_kwargs.pop("total", None)
83
- self._tqdm_kwargs = tqdm_kwargs
84
- self._tqdm_cls = Tqdm
85
- self.tqdm = progress_bar
86
- super().__init__(size=size, value=value)
87
-
88
- def close(self):
89
- if self.tqdm is not None:
90
- self.tqdm.close()
91
- self.tqdm = None
92
-
93
- def call(self, hook_name=None, **kwargs):
94
- if self.tqdm is None:
95
- self.tqdm = self._tqdm_cls(**self._tqdm_kwargs, total=self.size or -1)
96
- self.tqdm.update_to(self.value, total=self.size)
97
-
98
- def branch(
99
- self,
100
- path_1: "Union[str, BinaryIO]",
101
- path_2: str,
102
- kwargs: Dict[str, Any],
103
- child: Optional[Callback] = None,
104
- ):
105
- desc = path_1 if isinstance(path_1, str) else path_2
106
- child = child or TqdmCallback(bytes=True, desc=desc)
107
- return super().branch(path_1, path_2, kwargs, child=child)
108
-
109
-
110
- class FsspecCallbackWrapper(Callback):
111
- def __init__(self, callback: fsspec.Callback):
112
- object.__setattr__(self, "_callback", callback)
113
-
114
- def __getattr__(self, name: str):
115
- return getattr(self._callback, name)
116
-
117
- def __setattr__(self, name: str, value: Any):
118
- setattr(self._callback, name, value)
119
-
120
- def absolute_update(self, value: int) -> None:
121
- value = value if value is not None else self.value
122
- return self._callback.absolute_update(value)
123
-
124
- def branch(
125
- self,
126
- path_1: "Union[str, BinaryIO]",
127
- path_2: str,
128
- kwargs: Dict[str, Any],
129
- child: Optional["Callback"] = None,
130
- ) -> "Callback":
131
- if not child:
132
- self._callback.branch(path_1, path_2, kwargs)
133
- child = self.as_callback(kwargs.get("callback"))
134
- return super().branch(path_1, path_2, kwargs, child=child)
135
-
136
-
137
- def wrap_fn(callback: fsspec.Callback, fn: F) -> F:
138
- @wraps(fn)
139
- async def async_wrapper(*args, **kwargs):
140
- res = await fn(*args, **kwargs)
141
- callback.relative_update()
142
- return res
143
-
144
- @wraps(fn)
145
- def sync_wrapper(*args, **kwargs):
146
- res = fn(*args, **kwargs)
147
- callback.relative_update()
148
- return res
149
-
150
- return async_wrapper if asyncio.iscoroutinefunction(fn) else sync_wrapper # type: ignore[return-value]
151
-
152
-
153
- def branch_callback(callback: fsspec.Callback, fn: F) -> F:
154
- callback = Callback.as_callback(callback)
155
-
156
- @wraps(fn)
157
- async def async_wrapper(path1: "Union[str, BinaryIO]", path2: str, **kwargs):
158
- with callback.branch(path1, path2, kwargs):
159
- return await fn(path1, path2, **kwargs)
160
-
161
- @wraps(fn)
162
- def sync_wrapper(path1: "Union[str, BinaryIO]", path2: str, **kwargs):
163
- with callback.branch(path1, path2, kwargs):
164
- return fn(path1, path2, **kwargs)
165
-
166
- return async_wrapper if asyncio.iscoroutinefunction(fn) else sync_wrapper # type: ignore[return-value]
167
-
168
-
169
- def wrap_and_branch_callback(callback: fsspec.Callback, fn: F) -> F:
170
- branch_wrapper = branch_callback(callback, fn)
171
- return wrap_fn(callback, branch_wrapper)
172
-
173
-
174
- def wrap_file(file, callback: fsspec.Callback) -> BinaryIO:
175
- return cast(BinaryIO, CallbackStream(file, callback))
176
-
177
-
178
- DEFAULT_CALLBACK = NoOpCallback()
@@ -1,163 +0,0 @@
1
- from typing import Optional
2
-
3
- import fsspec
4
- import pytest
5
-
6
- from dvc_objects.fs.callbacks import (
7
- DEFAULT_CALLBACK,
8
- Callback,
9
- TqdmCallback,
10
- branch_callback,
11
- wrap_and_branch_callback,
12
- wrap_file,
13
- wrap_fn,
14
- )
15
-
16
-
17
- @pytest.mark.parametrize("api", ["set_size", "absolute_update"])
18
- def test_callback_with_none(request, api, mocker):
19
- """
20
- Test that callback don't fail if they receive None.
21
- The callbacks should not receive None, but there may be some
22
- filesystems that are not compliant, we may want to maintain
23
- maximum compatibility, and not break UI in these edge-cases.
24
- See https://github.com/iterative/dvc/issues/7704.
25
- """
26
- callback = Callback.as_callback()
27
- request.addfinalizer(callback.close)
28
-
29
- call_mock = mocker.spy(callback, "call")
30
- method = getattr(callback, api)
31
- method(None)
32
- call_mock.assert_called_once_with()
33
- if callback is not DEFAULT_CALLBACK:
34
- assert callback.size is None
35
- assert callback.value == 0
36
-
37
-
38
- def test_wrap_fsspec():
39
- def _branch_fn(*args, callback: Optional["Callback"] = None, **kwargs):
40
- pass
41
-
42
- callback = fsspec.callbacks.Callback()
43
- assert callback.value == 0
44
- with Callback.as_callback(callback) as cb:
45
- assert not isinstance(cb, TqdmCallback)
46
- assert cb.value == 0
47
- cb.relative_update()
48
- assert cb.value == 1
49
- assert callback.value == 1
50
-
51
- with cb.branch("foo", "bar", {}) as child:
52
- _branch_fn("foo", "bar", callback=child)
53
- cb.relative_update()
54
-
55
- assert cb.value == 2
56
- assert callback.value == 2
57
-
58
-
59
- def ids_func(cb_type):
60
- return f"{cb_type.__module__}.{cb_type.__qualname__}"
61
-
62
-
63
- class IsDVCCallback:
64
- def __eq__(self, value: object) -> bool:
65
- return isinstance(value, Callback)
66
-
67
-
68
- @pytest.fixture(params=[Callback, fsspec.Callback], ids=ids_func)
69
- def cb_class(request):
70
- return request.param
71
-
72
-
73
- def test_wrap_fn_sync(mocker, cb_class):
74
- m = mocker.MagicMock(return_value=1)
75
- callback = cb_class()
76
-
77
- wrapped = wrap_fn(callback, m)
78
-
79
- assert wrapped("arg") == 1
80
- assert callback.value == 1
81
- m.assert_called_once_with("arg")
82
-
83
-
84
- @pytest.mark.asyncio
85
- async def test_wrap_fn_async(mocker, cb_class):
86
- m = mocker.AsyncMock(return_value=1)
87
- callback = cb_class()
88
-
89
- wrapped = wrap_fn(callback, m)
90
-
91
- assert await wrapped("arg") == 1
92
- assert callback.value == 1
93
- m.assert_called_once_with("arg")
94
-
95
-
96
- def test_branch_fn_sync(mocker, cb_class):
97
- m = mocker.MagicMock(return_value=1)
98
- callback = cb_class()
99
- spy = mocker.spy(callback, "branch")
100
- wrapped = branch_callback(callback, m)
101
-
102
- assert wrapped("arg1", "arg2") == 1
103
- assert callback.value == 0
104
- assert spy.call_count == 1
105
- m.assert_called_once_with("arg1", "arg2", callback=IsDVCCallback())
106
-
107
-
108
- @pytest.mark.asyncio
109
- async def test_branch_fn_async(mocker, cb_class):
110
- m = mocker.AsyncMock(return_value=1)
111
- callback = cb_class()
112
- spy = mocker.spy(callback, "branch")
113
- wrapped = branch_callback(callback, m)
114
-
115
- assert await wrapped("arg1", "arg2") == 1
116
- assert callback.value == 0
117
- assert spy.call_count == 1
118
- m.assert_called_once_with("arg1", "arg2", callback=IsDVCCallback())
119
-
120
-
121
- def test_wrap_and_branch_callback_sync(mocker, cb_class):
122
- m = mocker.MagicMock(return_value=1)
123
- callback = cb_class()
124
- spy = mocker.spy(callback, "branch")
125
- wrapped = wrap_and_branch_callback(callback, m)
126
-
127
- assert wrapped("arg1", "arg2", arg3="arg3") == 1
128
- assert wrapped("argA", "argB", arg3="argC") == 1
129
-
130
- m.assert_any_call("arg1", "arg2", arg3="arg3", callback=IsDVCCallback())
131
- m.assert_any_call("argA", "argB", arg3="argC", callback=IsDVCCallback())
132
- assert callback.value == 2
133
- assert spy.call_count == 2
134
-
135
-
136
- @pytest.mark.asyncio
137
- async def test_wrap_and_branch_callback_async(mocker, cb_class):
138
- m = mocker.AsyncMock(return_value=1)
139
- callback = cb_class()
140
- spy = mocker.spy(callback, "branch")
141
- wrapped = wrap_and_branch_callback(callback, m)
142
-
143
- assert await wrapped("arg1", "arg2", arg3="arg3") == 1
144
- assert await wrapped("argA", "argB", arg3="argC") == 1
145
-
146
- m.assert_any_call("arg1", "arg2", arg3="arg3", callback=IsDVCCallback())
147
- m.assert_any_call("argA", "argB", arg3="argC", callback=IsDVCCallback())
148
- assert callback.value == 2
149
- assert spy.call_count == 2
150
-
151
-
152
- def test_wrap_file(memfs):
153
- memfs.pipe_file("/file", b"foo\n")
154
-
155
- callback = Callback()
156
-
157
- callback.set_size(4)
158
- with memfs.open("/file", mode="rb") as f:
159
- wrapped = wrap_file(f, callback)
160
- assert wrapped.read() == b"foo\n"
161
-
162
- assert callback.value == 4
163
- assert callback.size == 4
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes