Skip to content

Commit e9e08f0

Browse files
committed
lfs: drop dvc-objects dependency
1 parent 641ee43 commit e9e08f0

File tree

3 files changed

+139
-13
lines changed

3 files changed

+139
-13
lines changed

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,12 @@ dependencies = [
2525
"dulwich>=0.21.6",
2626
"pygit2>=1.14.0",
2727
"pygtrie>=2.3.2",
28-
"fsspec>=2024.2.0",
28+
"fsspec[tqdm]>=2024.2.0",
2929
"pathspec>=0.9.0",
3030
"asyncssh>=2.13.1,<3",
3131
"funcy>=1.14",
32-
"shortuuid>=0.5.0",
33-
"dvc-objects>=4,<5",
3432
"aiohttp-retry>=2.5.0",
33+
"tqdm",
3534
]
3635

3736
[project.urls]
@@ -54,6 +53,7 @@ tests = [
5453
"types-certifi==2021.10.8.3",
5554
"types-mock==5.1.0.2",
5655
"types-paramiko==3.4.0.20240120",
56+
"types-tqdm",
5757
]
5858
dev = [
5959
"scmrepo[tests]",

src/scmrepo/git/lfs/client.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import logging
2-
from collections.abc import Iterable
3-
from contextlib import AbstractContextManager
2+
import os
3+
import shutil
4+
from collections.abc import Iterable, Iterator
5+
from contextlib import AbstractContextManager, contextmanager, suppress
46
from multiprocessing import cpu_count
7+
from tempfile import NamedTemporaryFile
58
from typing import TYPE_CHECKING, Any, Optional
69

710
import aiohttp
811
from aiohttp_retry import ExponentialRetry, RetryClient
9-
from dvc_objects.fs import localfs
10-
from dvc_objects.fs.utils import as_atomic
1112
from fsspec.asyn import _run_coros_in_chunks, sync_wrapper
1213
from fsspec.callbacks import DEFAULT_CALLBACK
1314
from fsspec.implementations.http import HTTPFileSystem
@@ -156,11 +157,11 @@ async def _download(
156157
**kwargs,
157158
):
158159
async def _get_one(from_path: str, to_path: str, **kwargs):
159-
with as_atomic(localfs, to_path, create_parents=True) as tmp_file:
160+
with _as_atomic(to_path, create_parents=True) as tmp_file:
160161
with callback.branched(from_path, tmp_file) as child:
161162
await self._fs._get_file(
162163
from_path, tmp_file, callback=child, **kwargs
163-
) # pylint: disable=protected-access
164+
)
164165
callback.relative_update()
165166

166167
resp_data = await self._batch_request(objects, **kwargs)
@@ -184,3 +185,21 @@ async def _get_one(from_path: str, to_path: str, **kwargs):
184185
raise result
185186

186187
download = sync_wrapper(_download)
188+
189+
190+
@contextmanager
191+
def _as_atomic(to_info: str, create_parents: bool = False) -> Iterator[str]:
192+
parent = os.path.dirname(to_info)
193+
if create_parents:
194+
os.makedirs(parent, exist_ok=True)
195+
196+
tmp_file = NamedTemporaryFile(dir=parent, delete=False)
197+
tmp_file.close()
198+
try:
199+
yield tmp_file.name
200+
except BaseException:
201+
with suppress(FileNotFoundError):
202+
os.unlink(tmp_file.name)
203+
raise
204+
else:
205+
shutil.move(tmp_file.name, to_info)

src/scmrepo/git/lfs/progress.py

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,114 @@
1-
from typing import BinaryIO, Callable, Optional, Union
1+
import logging
2+
import sys
3+
from typing import Any, BinaryIO, Callable, ClassVar, Optional, Union
24

3-
from dvc_objects.fs.callbacks import TqdmCallback
4-
from fsspec.callbacks import DEFAULT_CALLBACK, Callback
5+
from fsspec.callbacks import DEFAULT_CALLBACK, Callback, TqdmCallback
6+
from tqdm import tqdm
57

68
from scmrepo.progress import GitProgressEvent
79

810

11+
class _Tqdm(tqdm):
12+
"""
13+
maximum-compatibility tqdm-based progressbars
14+
"""
15+
16+
BAR_FMT_DEFAULT = (
17+
"{percentage:3.0f}% {desc}|{bar}|"
18+
"{postfix[info]}{n_fmt}/{total_fmt}"
19+
" [{elapsed}<{remaining}, {rate_fmt:>11}]"
20+
)
21+
# nested bars should have fixed bar widths to align nicely
22+
BAR_FMT_DEFAULT_NESTED = (
23+
"{percentage:3.0f}%|{bar:10}|{desc:{ncols_desc}.{ncols_desc}}"
24+
"{postfix[info]}{n_fmt}/{total_fmt}"
25+
" [{elapsed}<{remaining}, {rate_fmt:>11}]"
26+
)
27+
BAR_FMT_NOTOTAL = "{desc}{bar:b}|{postfix[info]}{n_fmt} [{elapsed}, {rate_fmt:>11}]"
28+
BYTES_DEFAULTS: ClassVar[dict[str, Any]] = {
29+
"unit": "B",
30+
"unit_scale": True,
31+
"unit_divisor": 1024,
32+
"miniters": 1,
33+
}
34+
35+
def __init__( # noqa: PLR0913
36+
self,
37+
iterable=None,
38+
disable=None,
39+
level=logging.ERROR,
40+
desc=None,
41+
leave=False,
42+
bar_format=None,
43+
bytes=False, # noqa: A002
44+
file=None,
45+
total=None,
46+
postfix=None,
47+
**kwargs,
48+
):
49+
kwargs = kwargs.copy()
50+
if bytes:
51+
kwargs = {**self.BYTES_DEFAULTS, **kwargs}
52+
else:
53+
kwargs.setdefault("unit_scale", total > 999 if total else True)
54+
if file is None:
55+
file = sys.stderr
56+
super().__init__(
57+
iterable=iterable,
58+
disable=disable,
59+
leave=leave,
60+
desc=desc,
61+
bar_format="!",
62+
lock_args=(False,),
63+
total=total,
64+
**kwargs,
65+
)
66+
self.postfix = postfix or {"info": ""}
67+
if bar_format is None:
68+
if self.__len__():
69+
self.bar_format = (
70+
self.BAR_FMT_DEFAULT_NESTED if self.pos else self.BAR_FMT_DEFAULT
71+
)
72+
else:
73+
self.bar_format = self.BAR_FMT_NOTOTAL
74+
else:
75+
self.bar_format = bar_format
76+
self.refresh()
77+
78+
def update_to(self, current, total=None):
79+
if total:
80+
self.total = total
81+
self.update(current - self.n)
82+
83+
def close(self):
84+
self.postfix["info"] = ""
85+
# remove ETA (either unknown or zero); remove completed bar
86+
self.bar_format = self.bar_format.replace("<{remaining}", "").replace(
87+
"|{bar:10}|", " "
88+
)
89+
super().close()
90+
91+
@property
92+
def format_dict(self):
93+
"""inject `ncols_desc` to fill the display width (`ncols`)"""
94+
d = super().format_dict
95+
ncols = d["ncols"] or 80
96+
# assumes `bar_format` has max one of ("ncols_desc" & "ncols_info")
97+
98+
meter = self.format_meter( # type: ignore[call-arg]
99+
ncols_desc=1, ncols_info=1, **d
100+
)
101+
ncols_left = ncols - len(meter) + 1
102+
ncols_left = max(ncols_left, 0)
103+
if ncols_left:
104+
d["ncols_desc"] = d["ncols_info"] = ncols_left
105+
else:
106+
# work-around for zero-width description
107+
d["ncols_desc"] = d["ncols_info"] = 1
108+
d["prefix"] = ""
109+
return d
110+
111+
9112
class LFSCallback(Callback):
10113
"""Callback subclass to generate Git/LFS style progress."""
11114

@@ -37,7 +140,11 @@ def _update_git(self):
37140
def branched(self, path_1: Union[str, BinaryIO], path_2: str, **kwargs):
38141
if self.git_progress:
39142
return TqdmCallback(
40-
bytes=True, desc=path_1 if isinstance(path_1, str) else path_2
143+
tqdm_kwargs={
144+
"desc": path_1 if isinstance(path_1, str) else path_2,
145+
"bytes": True,
146+
},
147+
tqdm_cls=_Tqdm,
41148
)
42149
return DEFAULT_CALLBACK
43150

0 commit comments

Comments
 (0)