dvc-objects 3.0.5__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.5 → dvc-objects-4.0.0}/.github/workflows/tests.yml +1 -0
  2. {dvc-objects-3.0.5/src/dvc_objects.egg-info → dvc-objects-4.0.0}/PKG-INFO +2 -2
  3. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/pyproject.toml +2 -2
  4. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/db.py +4 -2
  5. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/executors.py +1 -1
  6. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/fs/base.py +17 -14
  7. dvc-objects-4.0.0/src/dvc_objects/fs/callbacks.py +57 -0
  8. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/fs/generic.py +44 -25
  9. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/fs/utils.py +7 -4
  10. {dvc-objects-3.0.5 → dvc-objects-4.0.0/src/dvc_objects.egg-info}/PKG-INFO +2 -2
  11. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects.egg-info/SOURCES.txt +1 -0
  12. {dvc-objects-3.0.5 → 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.5/src/dvc_objects/fs/callbacks.py +0 -184
  16. dvc-objects-3.0.5/tests/fs/test_callbacks.py +0 -148
  17. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/.cruft.json +0 -0
  18. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/.gitattributes +0 -0
  19. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/.github/dependabot.yml +0 -0
  20. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/.github/workflows/release.yml +0 -0
  21. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/.github/workflows/update-template.yaml +0 -0
  22. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/.gitignore +0 -0
  23. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/.pre-commit-config.yaml +0 -0
  24. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/CODE_OF_CONDUCT.rst +0 -0
  25. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/CONTRIBUTING.rst +0 -0
  26. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/LICENSE +0 -0
  27. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/README.rst +0 -0
  28. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/noxfile.py +0 -0
  29. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/setup.cfg +0 -0
  30. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/__init__.py +0 -0
  31. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/_tqdm.py +0 -0
  32. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/compat.py +0 -0
  33. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/errors.py +0 -0
  34. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/fs/__init__.py +0 -0
  35. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/fs/errors.py +0 -0
  36. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/fs/local.py +0 -0
  37. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/fs/memory.py +0 -0
  38. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/fs/scheme.py +0 -0
  39. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/fs/system.py +0 -0
  40. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/obj.py +0 -0
  41. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/py.typed +0 -0
  42. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects/transfer.py +0 -0
  43. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects.egg-info/dependency_links.txt +0 -0
  44. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/src/dvc_objects.egg-info/top_level.txt +0 -0
  45. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/tests/__init__.py +0 -0
  46. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/tests/benchmarks/__init__.py +0 -0
  47. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/tests/benchmarks/test_fs.py +0 -0
  48. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/tests/conftest.py +0 -0
  49. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/tests/fs/__init__.py +0 -0
  50. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/tests/fs/test_localfs.py +0 -0
  51. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/tests/fs/test_system.py +0 -0
  52. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/tests/fs/test_utils.py +0 -0
  53. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/tests/test_odb.py +0 -0
  54. {dvc-objects-3.0.5 → dvc-objects-4.0.0}/tests/test_reflink.py +0 -0
  55. {dvc-objects-3.0.5 → 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.5
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
 
@@ -21,22 +21,18 @@ from typing import (
21
21
  Sequence,
22
22
  Tuple,
23
23
  Union,
24
- cast,
25
24
  overload,
26
25
  )
27
26
  from urllib.parse import urlsplit, urlunsplit
28
27
 
29
28
  import fsspec
30
29
  from fsspec.asyn import get_loop
30
+ from fsspec.callbacks import DEFAULT_CALLBACK
31
31
 
32
32
  from dvc_objects.compat import cached_property
33
33
  from dvc_objects.executors import ThreadPoolExecutor, batch_coros
34
34
 
35
- from .callbacks import (
36
- DEFAULT_CALLBACK,
37
- CallbackStream,
38
- wrap_and_branch_callback,
39
- )
35
+ from .callbacks import wrap_file
40
36
  from .errors import RemoteMissingDepsError
41
37
 
42
38
  if TYPE_CHECKING:
@@ -637,7 +633,7 @@ class FileSystem:
637
633
  if size:
638
634
  callback.set_size(size)
639
635
  if hasattr(from_file, "read"):
640
- stream = cast("BinaryIO", CallbackStream(from_file, callback))
636
+ stream = wrap_file(from_file, callback)
641
637
  self.upload_fobj(stream, to_info, size=size)
642
638
  else:
643
639
  assert isinstance(from_file, str)
@@ -707,9 +703,14 @@ class FileSystem:
707
703
 
708
704
  callback.set_size(len(from_infos))
709
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
+
710
711
  with executor:
711
- put_file = wrap_and_branch_callback(callback, self.put_file)
712
- 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))
713
714
 
714
715
  def get(
715
716
  self,
@@ -725,9 +726,8 @@ class FileSystem:
725
726
 
726
727
  def get_file(rpath, lpath, **kwargs):
727
728
  localfs.makedirs(localfs.parent(lpath), exist_ok=True)
728
- self.fs.get_file(rpath, lpath, **kwargs)
729
-
730
- 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)
731
731
 
732
732
  if isinstance(from_info, list) and isinstance(to_info, list):
733
733
  from_infos: List[AnyFSPath] = from_info
@@ -738,7 +738,9 @@ class FileSystem:
738
738
 
739
739
  if not self.isdir(from_info):
740
740
  callback.set_size(1)
741
- return get_file(from_info, to_info)
741
+ get_file(from_info, to_info)
742
+ callback.relative_update()
743
+ return
742
744
 
743
745
  from_infos = list(self.find(from_info))
744
746
  if not from_infos:
@@ -761,7 +763,8 @@ class FileSystem:
761
763
  callback.set_size(len(from_infos))
762
764
  executor = ThreadPoolExecutor(max_workers=jobs, cancel_on_error=True)
763
765
  with executor:
764
- 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))
765
768
 
766
769
  def ukey(self, path: AnyFSPath) -> str:
767
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, CallbackStream
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__)
@@ -168,8 +171,8 @@ def copyfile(
168
171
 
169
172
  callback.set_size(total)
170
173
  with open(src, "rb") as fsrc, open(dest, "wb+") as fdest:
171
- wrapped = CallbackStream(fdest, callback, "write")
172
- shutil.copyfileobj(fsrc, wrapped, length=LOCAL_CHUNK_SIZE)
174
+ wrapped = wrap_file(fsrc, callback)
175
+ shutil.copyfileobj(wrapped, fdest, length=LOCAL_CHUNK_SIZE)
173
176
 
174
177
 
175
178
  def tmp_fname(prefix: str = "") -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dvc-objects
3
- Version: 3.0.5
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,184 +0,0 @@
1
- import asyncio
2
- from functools import wraps
3
- from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, TypeVar
4
-
5
- import fsspec
6
-
7
- if TYPE_CHECKING:
8
- from typing import BinaryIO, 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, method="read"):
17
- self.stream = stream
18
- if method == "write":
19
-
20
- @wraps(stream.write)
21
- def write(data, *args, **kwargs):
22
- res = stream.write(data, *args, **kwargs)
23
- callback.relative_update(len(data))
24
- return res
25
-
26
- self.write = write
27
- else:
28
-
29
- @wraps(stream.read)
30
- def read(*args, **kwargs):
31
- data = stream.read(*args, **kwargs)
32
- callback.relative_update(len(data))
33
- return data
34
-
35
- self.read = read
36
-
37
- def __getattr__(self, attr):
38
- return getattr(self.stream, attr)
39
-
40
-
41
- class ScopedCallback(fsspec.Callback):
42
- def __enter__(self):
43
- return self
44
-
45
- def __exit__(self, *exc_args):
46
- self.close()
47
-
48
- def close(self):
49
- """Handle here on exit."""
50
-
51
- def branch(
52
- self,
53
- path_1: "Union[str, BinaryIO]",
54
- path_2: str,
55
- kwargs: Dict[str, Any],
56
- child: Optional["Callback"] = None,
57
- ) -> "Callback":
58
- child = kwargs["callback"] = child or DEFAULT_CALLBACK
59
- return child
60
-
61
-
62
- class Callback(ScopedCallback):
63
- def absolute_update(self, value: int) -> None:
64
- value = value if value is not None else self.value
65
- return super().absolute_update(value)
66
-
67
- @classmethod
68
- def as_callback(
69
- cls, maybe_callback: Optional[fsspec.Callback] = None
70
- ) -> "Callback":
71
- if maybe_callback is None:
72
- return DEFAULT_CALLBACK
73
- if isinstance(maybe_callback, Callback):
74
- return maybe_callback
75
- return FsspecCallbackWrapper(maybe_callback)
76
-
77
-
78
- class NoOpCallback(Callback, fsspec.callbacks.NoOpCallback):
79
- pass
80
-
81
-
82
- class TqdmCallback(Callback):
83
- def __init__(
84
- self,
85
- size: Optional[int] = None,
86
- value: int = 0,
87
- progress_bar: Optional["Tqdm"] = None,
88
- **tqdm_kwargs,
89
- ):
90
- from dvc_objects._tqdm import Tqdm
91
-
92
- tqdm_kwargs.pop("total", None)
93
- self._tqdm_kwargs = tqdm_kwargs
94
- self._tqdm_cls = Tqdm
95
- self.tqdm = progress_bar
96
- super().__init__(size=size, value=value)
97
-
98
- def close(self):
99
- if self.tqdm is not None:
100
- self.tqdm.close()
101
- self.tqdm = None
102
-
103
- def call(self, hook_name=None, **kwargs):
104
- if self.tqdm is None:
105
- self.tqdm = self._tqdm_cls(**self._tqdm_kwargs, total=self.size or -1)
106
- self.tqdm.update_to(self.value, total=self.size)
107
-
108
- def branch(
109
- self,
110
- path_1: "Union[str, BinaryIO]",
111
- path_2: str,
112
- kwargs: Dict[str, Any],
113
- child: Optional[Callback] = None,
114
- ):
115
- desc = path_1 if isinstance(path_1, str) else path_2
116
- child = child or TqdmCallback(bytes=True, desc=desc)
117
- return super().branch(path_1, path_2, kwargs, child=child)
118
-
119
-
120
- class FsspecCallbackWrapper(Callback):
121
- def __init__(self, callback: fsspec.Callback):
122
- object.__setattr__(self, "_callback", callback)
123
-
124
- def __getattr__(self, name: str):
125
- return getattr(self._callback, name)
126
-
127
- def __setattr__(self, name: str, value: Any):
128
- setattr(self._callback, name, value)
129
-
130
- def absolute_update(self, value: int) -> None:
131
- value = value if value is not None else self.value
132
- return self._callback.absolute_update(value)
133
-
134
- def branch(
135
- self,
136
- path_1: "Union[str, BinaryIO]",
137
- path_2: str,
138
- kwargs: Dict[str, Any],
139
- child: Optional["Callback"] = None,
140
- ) -> "Callback":
141
- if not child:
142
- self._callback.branch(path_1, path_2, kwargs)
143
- child = self.as_callback(kwargs.get("callback"))
144
- return super().branch(path_1, path_2, kwargs, child=child)
145
-
146
-
147
- def wrap_fn(callback: fsspec.Callback, fn: F) -> F:
148
- @wraps(fn)
149
- async def async_wrapper(*args, **kwargs):
150
- res = await fn(*args, **kwargs)
151
- callback.relative_update()
152
- return res
153
-
154
- @wraps(fn)
155
- def sync_wrapper(*args, **kwargs):
156
- res = fn(*args, **kwargs)
157
- callback.relative_update()
158
- return res
159
-
160
- return async_wrapper if asyncio.iscoroutinefunction(fn) else sync_wrapper # type: ignore[return-value]
161
-
162
-
163
- def branch_callback(callback: fsspec.Callback, fn: F) -> F:
164
- callback = Callback.as_callback(callback)
165
-
166
- @wraps(fn)
167
- async def async_wrapper(path1: "Union[str, BinaryIO]", path2: str, **kwargs):
168
- with callback.branch(path1, path2, kwargs):
169
- return await fn(path1, path2, **kwargs)
170
-
171
- @wraps(fn)
172
- def sync_wrapper(path1: "Union[str, BinaryIO]", path2: str, **kwargs):
173
- with callback.branch(path1, path2, kwargs):
174
- return fn(path1, path2, **kwargs)
175
-
176
- return async_wrapper if asyncio.iscoroutinefunction(fn) else sync_wrapper # type: ignore[return-value]
177
-
178
-
179
- def wrap_and_branch_callback(callback: fsspec.Callback, fn: F) -> F:
180
- branch_wrapper = branch_callback(callback, fn)
181
- return wrap_fn(callback, branch_wrapper)
182
-
183
-
184
- DEFAULT_CALLBACK = NoOpCallback()
@@ -1,148 +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_fn,
13
- )
14
-
15
-
16
- @pytest.mark.parametrize("api", ["set_size", "absolute_update"])
17
- def test_callback_with_none(request, api, mocker):
18
- """
19
- Test that callback don't fail if they receive None.
20
- The callbacks should not receive None, but there may be some
21
- filesystems that are not compliant, we may want to maintain
22
- maximum compatibility, and not break UI in these edge-cases.
23
- See https://github.com/iterative/dvc/issues/7704.
24
- """
25
- callback = Callback.as_callback()
26
- request.addfinalizer(callback.close)
27
-
28
- call_mock = mocker.spy(callback, "call")
29
- method = getattr(callback, api)
30
- method(None)
31
- call_mock.assert_called_once_with()
32
- if callback is not DEFAULT_CALLBACK:
33
- assert callback.size is None
34
- assert callback.value == 0
35
-
36
-
37
- def test_wrap_fsspec():
38
- def _branch_fn(*args, callback: Optional["Callback"] = None, **kwargs):
39
- pass
40
-
41
- callback = fsspec.callbacks.Callback()
42
- assert callback.value == 0
43
- with Callback.as_callback(callback) as cb:
44
- assert not isinstance(cb, TqdmCallback)
45
- assert cb.value == 0
46
- cb.relative_update()
47
- assert cb.value == 1
48
- assert callback.value == 1
49
-
50
- with cb.branch("foo", "bar", {}) as child:
51
- _branch_fn("foo", "bar", callback=child)
52
- cb.relative_update()
53
-
54
- assert cb.value == 2
55
- assert callback.value == 2
56
-
57
-
58
- def ids_func(cb_type):
59
- return f"{cb_type.__module__}.{cb_type.__qualname__}"
60
-
61
-
62
- class IsDVCCallback:
63
- def __eq__(self, value: object) -> bool:
64
- return isinstance(value, Callback)
65
-
66
-
67
- @pytest.fixture(params=[Callback, fsspec.Callback], ids=ids_func)
68
- def cb_class(request):
69
- return request.param
70
-
71
-
72
- def test_wrap_fn_sync(mocker, cb_class):
73
- m = mocker.MagicMock(return_value=1)
74
- callback = cb_class()
75
-
76
- wrapped = wrap_fn(callback, m)
77
-
78
- assert wrapped("arg") == 1
79
- assert callback.value == 1
80
- m.assert_called_once_with("arg")
81
-
82
-
83
- @pytest.mark.asyncio
84
- async def test_wrap_fn_async(mocker, cb_class):
85
- m = mocker.AsyncMock(return_value=1)
86
- callback = cb_class()
87
-
88
- wrapped = wrap_fn(callback, m)
89
-
90
- assert await wrapped("arg") == 1
91
- assert callback.value == 1
92
- m.assert_called_once_with("arg")
93
-
94
-
95
- def test_branch_fn_sync(mocker, cb_class):
96
- m = mocker.MagicMock(return_value=1)
97
- callback = cb_class()
98
- spy = mocker.spy(callback, "branch")
99
- wrapped = branch_callback(callback, m)
100
-
101
- assert wrapped("arg1", "arg2") == 1
102
- assert callback.value == 0
103
- assert spy.call_count == 1
104
- m.assert_called_once_with("arg1", "arg2", callback=IsDVCCallback())
105
-
106
-
107
- @pytest.mark.asyncio
108
- async def test_branch_fn_async(mocker, cb_class):
109
- m = mocker.AsyncMock(return_value=1)
110
- callback = cb_class()
111
- spy = mocker.spy(callback, "branch")
112
- wrapped = branch_callback(callback, m)
113
-
114
- assert await wrapped("arg1", "arg2") == 1
115
- assert callback.value == 0
116
- assert spy.call_count == 1
117
- m.assert_called_once_with("arg1", "arg2", callback=IsDVCCallback())
118
-
119
-
120
- def test_wrap_and_branch_callback_sync(mocker, cb_class):
121
- m = mocker.MagicMock(return_value=1)
122
- callback = cb_class()
123
- spy = mocker.spy(callback, "branch")
124
- wrapped = wrap_and_branch_callback(callback, m)
125
-
126
- assert wrapped("arg1", "arg2", arg3="arg3") == 1
127
- assert wrapped("argA", "argB", arg3="argC") == 1
128
-
129
- m.assert_any_call("arg1", "arg2", arg3="arg3", callback=IsDVCCallback())
130
- m.assert_any_call("argA", "argB", arg3="argC", callback=IsDVCCallback())
131
- assert callback.value == 2
132
- assert spy.call_count == 2
133
-
134
-
135
- @pytest.mark.asyncio
136
- async def test_wrap_and_branch_callback_async(mocker, cb_class):
137
- m = mocker.AsyncMock(return_value=1)
138
- callback = cb_class()
139
- spy = mocker.spy(callback, "branch")
140
- wrapped = wrap_and_branch_callback(callback, m)
141
-
142
- assert await wrapped("arg1", "arg2", arg3="arg3") == 1
143
- assert await wrapped("argA", "argB", arg3="argC") == 1
144
-
145
- m.assert_any_call("arg1", "arg2", arg3="arg3", callback=IsDVCCallback())
146
- m.assert_any_call("argA", "argB", arg3="argC", callback=IsDVCCallback())
147
- assert callback.value == 2
148
- assert spy.call_count == 2
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes