scmrepo 3.3.10__tar.gz → 3.3.12__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.

Potentially problematic release.


This version of scmrepo might be problematic. Click here for more details.

Files changed (73) hide show
  1. {scmrepo-3.3.10 → scmrepo-3.3.12}/.github/workflows/tests.yaml +2 -2
  2. {scmrepo-3.3.10 → scmrepo-3.3.12}/.pre-commit-config.yaml +2 -2
  3. {scmrepo-3.3.10/src/scmrepo.egg-info → scmrepo-3.3.12}/PKG-INFO +6 -5
  4. {scmrepo-3.3.10 → scmrepo-3.3.12}/pyproject.toml +4 -3
  5. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/asyn.py +1 -3
  6. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/__init__.py +2 -1
  7. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/dulwich/__init__.py +4 -4
  8. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/dulwich/asyncssh_vendor.py +23 -67
  9. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/dulwich/client.py +10 -3
  10. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/pygit2/__init__.py +3 -1
  11. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/pygit2/callbacks.py +2 -0
  12. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/pygit2/filter.py +1 -1
  13. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/credentials.py +4 -4
  14. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/smudge.py +1 -1
  15. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/objects.py +1 -1
  16. {scmrepo-3.3.10 → scmrepo-3.3.12/src/scmrepo.egg-info}/PKG-INFO +6 -5
  17. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo.egg-info/requires.txt +3 -3
  18. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_dulwich.py +39 -26
  19. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_git.py +1 -1
  20. {scmrepo-3.3.10 → scmrepo-3.3.12}/.coveragerc +0 -0
  21. {scmrepo-3.3.10 → scmrepo-3.3.12}/.cruft.json +0 -0
  22. {scmrepo-3.3.10 → scmrepo-3.3.12}/.gitattributes +0 -0
  23. {scmrepo-3.3.10 → scmrepo-3.3.12}/.github/dependabot.yml +0 -0
  24. {scmrepo-3.3.10 → scmrepo-3.3.12}/.github/workflows/release.yaml +0 -0
  25. {scmrepo-3.3.10 → scmrepo-3.3.12}/.github/workflows/update-template.yaml +0 -0
  26. {scmrepo-3.3.10 → scmrepo-3.3.12}/.gitignore +0 -0
  27. {scmrepo-3.3.10 → scmrepo-3.3.12}/CODE_OF_CONDUCT.rst +0 -0
  28. {scmrepo-3.3.10 → scmrepo-3.3.12}/CONTRIBUTING.rst +0 -0
  29. {scmrepo-3.3.10 → scmrepo-3.3.12}/LICENSE +0 -0
  30. {scmrepo-3.3.10 → scmrepo-3.3.12}/README.rst +0 -0
  31. {scmrepo-3.3.10 → scmrepo-3.3.12}/noxfile.py +0 -0
  32. {scmrepo-3.3.10 → scmrepo-3.3.12}/setup.cfg +0 -0
  33. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/__init__.py +0 -0
  34. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/base.py +0 -0
  35. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/exceptions.py +0 -0
  36. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/fs.py +0 -0
  37. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/__init__.py +0 -0
  38. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/base.py +0 -0
  39. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/backend/gitpython.py +0 -0
  40. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/config.py +0 -0
  41. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/__init__.py +0 -0
  42. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/client.py +0 -0
  43. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/exceptions.py +0 -0
  44. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/fetch.py +0 -0
  45. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/object.py +0 -0
  46. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/pointer.py +0 -0
  47. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/progress.py +0 -0
  48. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/lfs/storage.py +0 -0
  49. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/git/stash.py +0 -0
  50. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/noscm.py +0 -0
  51. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/progress.py +0 -0
  52. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/py.typed +0 -0
  53. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/urls.py +0 -0
  54. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo/utils.py +0 -0
  55. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo.egg-info/SOURCES.txt +0 -0
  56. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo.egg-info/dependency_links.txt +0 -0
  57. {scmrepo-3.3.10 → scmrepo-3.3.12}/src/scmrepo.egg-info/top_level.txt +0 -0
  58. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/__init__.py +0 -0
  59. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/conftest.py +0 -0
  60. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/docker-compose.yml +0 -0
  61. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/git-init/git.sh +0 -0
  62. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_credentials.py +0 -0
  63. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_fs.py +0 -0
  64. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_lfs.py +0 -0
  65. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_noscm.py +0 -0
  66. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_pygit2.py +0 -0
  67. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_scmrepo.py +0 -0
  68. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_stash.py +0 -0
  69. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/test_urls.py +0 -0
  70. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/user.key +0 -0
  71. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/user.key.pub +0 -0
  72. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/vendor/__init__.py +0 -0
  73. {scmrepo-3.3.10 → scmrepo-3.3.12}/tests/vendor/test_paramiko_vendor.py +0 -0
@@ -20,7 +20,7 @@ jobs:
20
20
  strategy:
21
21
  fail-fast: false
22
22
  matrix:
23
- os: [ubuntu-20.04, windows-latest, macos-latest]
23
+ os: [ubuntu-latest, windows-latest, macos-latest]
24
24
  pyv: ['3.9', '3.10', '3.11', '3.12', '3.13']
25
25
 
26
26
  steps:
@@ -47,7 +47,7 @@ jobs:
47
47
  run: nox -s tests-${{ matrix.pyv }} -- --slow --cov-report=xml
48
48
 
49
49
  - name: Upload coverage report
50
- uses: codecov/codecov-action@v5.3.1
50
+ uses: codecov/codecov-action@v5
51
51
 
52
52
  - name: Build package
53
53
  run: nox -s build
@@ -20,13 +20,13 @@ repos:
20
20
  - id: sort-simple-yaml
21
21
  - id: trailing-whitespace
22
22
  - repo: https://github.com/astral-sh/ruff-pre-commit
23
- rev: 'v0.9.3'
23
+ rev: 'v0.12.3'
24
24
  hooks:
25
25
  - id: ruff
26
26
  args: [--fix, --exit-non-zero-on-fix]
27
27
  - id: ruff-format
28
28
  - repo: https://github.com/codespell-project/codespell
29
- rev: v2.4.0
29
+ rev: v2.4.1
30
30
  hooks:
31
31
  - id: codespell
32
32
  additional_dependencies: ["tomli"]
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: scmrepo
3
- Version: 3.3.10
3
+ Version: 3.3.12
4
4
  Summary: scmrepo
5
5
  Author-email: Iterative <support@dvc.org>
6
6
  License: Apache-2.0
@@ -17,7 +17,7 @@ Requires-Python: >=3.9
17
17
  Description-Content-Type: text/x-rst
18
18
  License-File: LICENSE
19
19
  Requires-Dist: gitpython>3
20
- Requires-Dist: dulwich>=0.22.1
20
+ Requires-Dist: dulwich>=0.23.1
21
21
  Requires-Dist: pygit2>=1.14.0
22
22
  Requires-Dist: pygtrie>=2.3.2
23
23
  Requires-Dist: fsspec[tqdm]>=2024.2.0
@@ -30,7 +30,7 @@ Provides-Extra: tests
30
30
  Requires-Dist: aioresponses<0.8,>=0.7; extra == "tests"
31
31
  Requires-Dist: paramiko<4,>=3.4.0; extra == "tests"
32
32
  Requires-Dist: pytest<9,>=7; extra == "tests"
33
- Requires-Dist: pytest-asyncio<1,>=0.23.2; extra == "tests"
33
+ Requires-Dist: pytest-asyncio<2,>=0.23.2; extra == "tests"
34
34
  Requires-Dist: pytest-cov>=4.1.0; extra == "tests"
35
35
  Requires-Dist: pytest-docker<4,>=1; extra == "tests"
36
36
  Requires-Dist: pytest-mock; extra == "tests"
@@ -38,12 +38,13 @@ Requires-Dist: pytest-sugar; extra == "tests"
38
38
  Requires-Dist: pytest-test-utils<0.2,>=0.1.0; extra == "tests"
39
39
  Requires-Dist: proxy.py; extra == "tests"
40
40
  Provides-Extra: dev
41
- Requires-Dist: mypy==1.14.1; extra == "dev"
41
+ Requires-Dist: mypy==1.17.0; extra == "dev"
42
42
  Requires-Dist: scmrepo[tests]; extra == "dev"
43
43
  Requires-Dist: types-certifi; extra == "dev"
44
44
  Requires-Dist: types-mock; extra == "dev"
45
45
  Requires-Dist: types-paramiko; extra == "dev"
46
46
  Requires-Dist: types-tqdm; extra == "dev"
47
+ Dynamic: license-file
47
48
 
48
49
  scmrepo
49
50
  =======
@@ -23,7 +23,7 @@ requires-python = ">=3.9"
23
23
  dynamic = ["version"]
24
24
  dependencies = [
25
25
  "gitpython>3",
26
- "dulwich>=0.22.1",
26
+ "dulwich>=0.23.1",
27
27
  "pygit2>=1.14.0",
28
28
  "pygtrie>=2.3.2",
29
29
  "fsspec[tqdm]>=2024.2.0",
@@ -43,7 +43,7 @@ tests = [
43
43
  "aioresponses>=0.7,<0.8",
44
44
  "paramiko>=3.4.0,<4",
45
45
  "pytest>=7,<9",
46
- "pytest-asyncio>=0.23.2,<1",
46
+ "pytest-asyncio>=0.23.2,<2",
47
47
  "pytest-cov>=4.1.0",
48
48
  "pytest-docker>=1,<4",
49
49
  "pytest-mock",
@@ -52,7 +52,7 @@ tests = [
52
52
  "proxy.py",
53
53
  ]
54
54
  dev = [
55
- "mypy==1.14.1",
55
+ "mypy==1.17.0",
56
56
  "scmrepo[tests]",
57
57
  "types-certifi",
58
58
  "types-mock",
@@ -147,6 +147,7 @@ ignore = [
147
147
  "RET503", # implicit-return
148
148
  "SIM117", # multiple-with-statements
149
149
  "N818", # error-suffix-on-exception-name
150
+ "PLC0415", # import-outside-top-level
150
151
  ]
151
152
  select = [
152
153
  "A", # flake8-buitlins
@@ -6,7 +6,6 @@ import threading
6
6
  from typing import Any, Optional
7
7
 
8
8
  from fsspec.asyn import ( # noqa: F401, pylint:disable=unused-import
9
- _selector_policy,
10
9
  sync,
11
10
  sync_wrapper,
12
11
  )
@@ -23,8 +22,7 @@ def get_loop() -> asyncio.AbstractEventLoop:
23
22
  if default_loop[0] is None:
24
23
  with lock:
25
24
  if default_loop[0] is None:
26
- with _selector_policy():
27
- default_loop[0] = asyncio.new_event_loop()
25
+ default_loop[0] = asyncio.new_event_loop()
28
26
  loop = default_loop[0]
29
27
  th = threading.Thread(
30
28
  target=loop.run_forever, # type: ignore[attr-defined]
@@ -10,6 +10,7 @@ from contextlib import contextmanager
10
10
  from functools import partialmethod
11
11
  from typing import (
12
12
  TYPE_CHECKING,
13
+ Any,
13
14
  Callable,
14
15
  ClassVar,
15
16
  Optional,
@@ -295,7 +296,7 @@ class Git(Base):
295
296
  # See:
296
297
  # https://github.com/iterative/dvc/issues/5641
297
298
  # https://github.com/iterative/dvc/issues/7458
298
- def _backend_func(self, name, *args, **kwargs):
299
+ def _backend_func(self, name, *args, **kwargs) -> Any:
299
300
  backends: Iterable[str] = kwargs.pop("backends", self.backends)
300
301
  for key in backends:
301
302
  if self._last_backend is not None and key != self._last_backend:
@@ -39,7 +39,7 @@ logger = logging.getLogger(__name__)
39
39
 
40
40
 
41
41
  class DulwichObject(GitObject):
42
- def __init__(self, repo, name, mode, sha):
42
+ def __init__(self, repo, name, mode, sha) -> None:
43
43
  self.repo = repo
44
44
  self._name = name
45
45
  self._mode = mode
@@ -136,7 +136,7 @@ def _get_ssh_vendor() -> "SSHVendor":
136
136
 
137
137
 
138
138
  class DulwichConfig(Config):
139
- def __init__(self, config: Union["ConfigFile", "StackedConfig"]):
139
+ def __init__(self, config: Union["ConfigFile", "StackedConfig"]) -> None:
140
140
  self._config = config
141
141
 
142
142
  @property
@@ -183,7 +183,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
183
183
 
184
184
  def __init__( # pylint:disable=W0231
185
185
  self, root_dir=os.curdir, search_parent_directories=True
186
- ):
186
+ ) -> None:
187
187
  from dulwich.errors import NotGitRepository
188
188
  from dulwich.repo import Repo
189
189
 
@@ -990,7 +990,7 @@ def ls_remote(url: str) -> dict[str, str]:
990
990
  from dulwich.client import HTTPUnauthorized
991
991
 
992
992
  try:
993
- refs = porcelain.ls_remote(url)
993
+ refs = porcelain.ls_remote(url).refs
994
994
  return {os.fsdecode(ref): sha.decode("ascii") for ref, sha in refs.items()}
995
995
  except HTTPUnauthorized as exc:
996
996
  raise AuthError(url) from exc
@@ -2,7 +2,7 @@
2
2
 
3
3
  import asyncio
4
4
  import os
5
- from collections.abc import Coroutine, Iterator, Sequence
5
+ from collections.abc import Coroutine, Iterator
6
6
  from typing import (
7
7
  TYPE_CHECKING,
8
8
  Any,
@@ -18,6 +18,7 @@ from scmrepo.asyn import BaseAsyncObject, sync_wrapper
18
18
  from scmrepo.exceptions import AuthError
19
19
 
20
20
  if TYPE_CHECKING:
21
+ from collections.abc import Sequence
21
22
  from pathlib import Path
22
23
 
23
24
  from asyncssh.auth import KbdIntPrompts, KbdIntResponse
@@ -40,6 +41,12 @@ async def _read_all(read: Callable[[int], Coroutine], n: Optional[int] = None) -
40
41
  return b"".join(result)
41
42
 
42
43
 
44
+ async def _getpass(*args, **kwargs) -> str:
45
+ from getpass import getpass
46
+
47
+ return await asyncio.to_thread(getpass, *args, **kwargs)
48
+
49
+
43
50
  class _StderrWrapper:
44
51
  def __init__(self, stderr: "SSHReader", loop: asyncio.AbstractEventLoop) -> None:
45
52
  self.stderr = stderr
@@ -64,7 +71,9 @@ class _StderrWrapper:
64
71
 
65
72
 
66
73
  class AsyncSSHWrapper(BaseAsyncObject):
67
- def __init__(self, conn: "SSHClientConnection", proc: "SSHClientProcess", **kwargs):
74
+ def __init__(
75
+ self, conn: "SSHClientConnection", proc: "SSHClientProcess", **kwargs
76
+ ) -> None:
68
77
  super().__init__(**kwargs)
69
78
  self.conn: SSHClientConnection = conn
70
79
  self.proc: SSHClientProcess = proc
@@ -97,51 +106,12 @@ class AsyncSSHWrapper(BaseAsyncObject):
97
106
  close = sync_wrapper(_close)
98
107
 
99
108
 
100
- # NOTE: Github's SSH server does not strictly comply with the SSH protocol.
101
- # When validating a public key using the rsa-sha2-256 or rsa-sha2-512
102
- # signature algorithms, RFC4252 + RFC8332 state that the server should respond
103
- # with the same algorithm in SSH_MSG_USERAUTH_PK_OK. Github's server always
104
- # returns "ssh-rsa" rather than the correct sha2 algorithm name (likely for
105
- # backwards compatibility with old SSH client reasons). This behavior causes
106
- # asyncssh to fail with a key-mismatch error (since asyncssh expects the server
107
- # to behave properly).
108
- #
109
- # See also:
110
- # https://www.ietf.org/rfc/rfc4252.txt
111
- # https://www.ietf.org/rfc/rfc8332.txt
112
- def _process_public_key_ok_gh(self, _pkttype, _pktid, packet):
113
- from asyncssh.misc import ProtocolError
114
-
115
- algorithm = packet.get_string()
116
- key_data = packet.get_string()
117
- packet.check_end()
118
-
119
- # pylint: disable=protected-access
120
- if (
121
- (
122
- algorithm == b"ssh-rsa"
123
- and self._keypair.algorithm
124
- not in (
125
- b"ssh-rsa",
126
- b"rsa-sha2-256",
127
- b"rsa-sha2-512",
128
- )
129
- )
130
- or (algorithm not in (b"ssh-rsa", self._keypair.algorithm))
131
- or key_data != self._keypair.public_data
132
- ):
133
- raise ProtocolError("Key mismatch")
134
-
135
- self.create_task(self._send_signed_request())
136
- return True
137
-
138
-
139
109
  class InteractiveSSHClient(SSHClient):
140
110
  _conn: Optional["SSHClientConnection"] = None
141
111
  _keys_to_try: Optional[list["FilePath"]] = None
142
112
  _passphrases: dict[str, str]
143
113
 
144
- def __init__(self, *args, **kwargs):
114
+ def __init__(self, *args, **kwargs) -> None:
145
115
  super(*args, **kwargs)
146
116
  self._passphrases: dict[str, str] = {}
147
117
 
@@ -171,7 +141,7 @@ class InteractiveSSHClient(SSHClient):
171
141
  self._keys_to_try = []
172
142
  options = self._conn._options # pylint: disable=protected-access
173
143
  config = options.config
174
- client_keys = cast(Sequence["FilePath"], config.get("IdentityFile", ()))
144
+ client_keys = cast("Sequence[FilePath]", config.get("IdentityFile", ()))
175
145
  if not client_keys:
176
146
  client_keys = [
177
147
  os.path.expanduser(os.path.join("~", ".ssh", path))
@@ -202,8 +172,6 @@ class InteractiveSSHClient(SSHClient):
202
172
  return None
203
173
 
204
174
  async def _read_private_key_interactive(self, path: "FilePath") -> "SSHKey":
205
- from getpass import getpass
206
-
207
175
  from asyncssh.public_key import (
208
176
  KeyEncryptionError,
209
177
  KeyImportError,
@@ -215,11 +183,8 @@ class InteractiveSSHClient(SSHClient):
215
183
  if passphrase:
216
184
  return read_private_key(path, passphrase=passphrase)
217
185
 
218
- loop = asyncio.get_running_loop()
219
186
  for _ in range(3):
220
- passphrase = await loop.run_in_executor(
221
- None, getpass, f"Enter passphrase for key '{path}': "
222
- )
187
+ passphrase = await _getpass(f"Enter passphrase for key {path!r}: ")
223
188
  if passphrase:
224
189
  try:
225
190
  key = read_private_key(path, passphrase=passphrase)
@@ -239,23 +204,20 @@ class InteractiveSSHClient(SSHClient):
239
204
  lang: str,
240
205
  prompts: "KbdIntPrompts",
241
206
  ) -> Optional["KbdIntResponse"]:
242
- from getpass import getpass
243
-
244
207
  if os.environ.get("GIT_TERMINAL_PROMPT") == "0":
245
208
  return None
246
209
 
247
- def _getpass(prompt: str) -> str:
248
- return getpass(prompt=prompt).rstrip()
249
-
250
210
  if instructions:
251
211
  pass
252
- loop = asyncio.get_running_loop()
253
- return [
254
- await loop.run_in_executor(
255
- None, _getpass, f"({name}) {prompt}" if name else prompt
256
- )
257
- for prompt, _ in prompts
258
- ]
212
+
213
+ response: list[str] = []
214
+ for prompt, _echo in prompts:
215
+ p = await _getpass(f"({name}) {prompt}" if name else prompt)
216
+ response.append(p.rstrip())
217
+ return response
218
+
219
+ async def password_auth_requested(self) -> str:
220
+ return await _getpass()
259
221
 
260
222
 
261
223
  class AsyncSSHVendor(BaseAsyncObject, SSHVendor):
@@ -286,12 +248,6 @@ class AsyncSSHVendor(BaseAsyncObject, SSHVendor):
286
248
  key_filename: Optional path to private keyfile
287
249
  """
288
250
  import asyncssh
289
- from asyncssh.auth import MSG_USERAUTH_PK_OK, _ClientPublicKeyAuth
290
-
291
- # pylint: disable=protected-access
292
- _ClientPublicKeyAuth._packet_handlers[MSG_USERAUTH_PK_OK] = (
293
- _process_public_key_ok_gh
294
- )
295
251
 
296
252
  try:
297
253
  conn = await asyncssh.connect(
@@ -14,7 +14,7 @@ class GitCredentialsHTTPClient(Urllib3HttpGitClient): # pylint: disable=abstrac
14
14
  password=None,
15
15
  config=None,
16
16
  **kwargs,
17
- ):
17
+ ) -> None:
18
18
  super().__init__(
19
19
  base_url=base_url,
20
20
  username=username,
@@ -29,6 +29,7 @@ class GitCredentialsHTTPClient(Urllib3HttpGitClient): # pylint: disable=abstrac
29
29
  url: str,
30
30
  headers: Optional[dict[str, str]] = None,
31
31
  data: Optional[Union[bytes, Iterator[bytes]]] = None,
32
+ raise_for_status: bool = True,
32
33
  ):
33
34
  cached_chunks: list[bytes] = []
34
35
 
@@ -48,7 +49,10 @@ class GitCredentialsHTTPClient(Urllib3HttpGitClient): # pylint: disable=abstrac
48
49
 
49
50
  try:
50
51
  result = super()._http_request(
51
- url, headers=headers, data=None if data is None else _cached_data()
52
+ url,
53
+ headers=headers,
54
+ data=None if data is None else _cached_data(),
55
+ raise_for_status=raise_for_status,
52
56
  )
53
57
  except HTTPUnauthorized:
54
58
  auth_header = self._get_auth()
@@ -59,7 +63,10 @@ class GitCredentialsHTTPClient(Urllib3HttpGitClient): # pylint: disable=abstrac
59
63
  else:
60
64
  headers = auth_header
61
65
  result = super()._http_request(
62
- url, headers=headers, data=None if data is None else _cached_data()
66
+ url,
67
+ headers=headers,
68
+ data=None if data is None else _cached_data(),
69
+ raise_for_status=raise_for_status,
63
70
  )
64
71
  if self._store_credentials is not None:
65
72
  self._store_credentials.approve()
@@ -289,7 +289,9 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
289
289
  bare = True
290
290
  try:
291
291
  with RemoteCallbacks(progress=progress) as cb:
292
- repo = clone_repository(url, to_path, callbacks=cb, bare=bare)
292
+ repo = clone_repository(
293
+ url, os.fspath(to_path), callbacks=cb, bare=bare
294
+ )
293
295
  if mirror:
294
296
  cls._set_mirror(repo, progress=progress)
295
297
  except GitError as exc:
@@ -66,6 +66,8 @@ class RemoteCallbacks(_RemoteCallbacks, AbstractContextManager):
66
66
  else:
67
67
  creds = Credential(username=username_from_url, url=url).fill()
68
68
  self._store_credentials = creds
69
+ assert creds.username is not None
70
+ assert creds.password is not None
69
71
  return UserPass(creds.username, creds.password)
70
72
  except CredentialNotFoundError:
71
73
  pass
@@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
13
13
  class LFSFilter(Filter):
14
14
  attributes = "filter=*"
15
15
 
16
- def __init__(self, *args, **kwargs):
16
+ def __init__(self, *args, **kwargs) -> None:
17
17
  self._smudge_buf: Optional[io.BytesIO] = None
18
18
  self._smudge_root: Optional[str] = None
19
19
 
@@ -105,7 +105,7 @@ class GitCredentialHelper(CredentialHelper):
105
105
  >>> password = credentials.password
106
106
  """
107
107
 
108
- def __init__(self, command: str, use_http_path: bool = False):
108
+ def __init__(self, command: str, use_http_path: bool = False) -> None:
109
109
  super().__init__()
110
110
  self._command = command
111
111
  self._run_kwargs: dict[str, Any] = {}
@@ -136,7 +136,7 @@ class GitCredentialHelper(CredentialHelper):
136
136
  if not shutil.which(executable) and shutil.which("git"):
137
137
  # If the helper cannot be found in PATH, it might be
138
138
  # a C git helper in GIT_EXEC_PATH
139
- git_exec_path = subprocess.check_output( # noqa: S603
139
+ git_exec_path = subprocess.check_output(
140
140
  ("git", "--exec-path"),
141
141
  text=True,
142
142
  ).strip()
@@ -353,7 +353,7 @@ def _input_tty(prompt: str = "Username: ") -> str:
353
353
  class MemoryCredentialHelper(CredentialHelper):
354
354
  """Memory credential helper that supports optional interactive input."""
355
355
 
356
- def __init__(self):
356
+ def __init__(self) -> None:
357
357
  super().__init__()
358
358
  self._credentials: dict[_CredentialKey, Credential] = {}
359
359
 
@@ -535,7 +535,7 @@ class Credential(Mapping[str, str]):
535
535
  path: Optional[str] = None,
536
536
  username: Optional[str] = None,
537
537
  password: Optional[str] = None,
538
- password_expiry_utc: Optional[int] = None,
538
+ password_expiry_utc: Optional[str] = None,
539
539
  url: Optional[str] = None,
540
540
  ):
541
541
  self.protocol = protocol
@@ -17,7 +17,7 @@ def smudge(
17
17
  batch_size: Optional[int] = None,
18
18
  ) -> BinaryIO:
19
19
  """Wrap the specified binary IO stream and run LFS smudge if necessary."""
20
- reader = io.BufferedReader(fobj) # type: ignore[arg-type]
20
+ reader = io.BufferedReader(fobj) # type: ignore[type-var]
21
21
  data = reader.peek(100)
22
22
  if any(data.startswith(header) for header in HEADERS):
23
23
  # read the pointer data into memory since the raw stream is unseekable
@@ -160,7 +160,7 @@ class GitTrie:
160
160
  }
161
161
  )
162
162
 
163
- return cast(dict, ret)
163
+ return cast("dict", ret)
164
164
 
165
165
 
166
166
  @dataclass
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: scmrepo
3
- Version: 3.3.10
3
+ Version: 3.3.12
4
4
  Summary: scmrepo
5
5
  Author-email: Iterative <support@dvc.org>
6
6
  License: Apache-2.0
@@ -17,7 +17,7 @@ Requires-Python: >=3.9
17
17
  Description-Content-Type: text/x-rst
18
18
  License-File: LICENSE
19
19
  Requires-Dist: gitpython>3
20
- Requires-Dist: dulwich>=0.22.1
20
+ Requires-Dist: dulwich>=0.23.1
21
21
  Requires-Dist: pygit2>=1.14.0
22
22
  Requires-Dist: pygtrie>=2.3.2
23
23
  Requires-Dist: fsspec[tqdm]>=2024.2.0
@@ -30,7 +30,7 @@ Provides-Extra: tests
30
30
  Requires-Dist: aioresponses<0.8,>=0.7; extra == "tests"
31
31
  Requires-Dist: paramiko<4,>=3.4.0; extra == "tests"
32
32
  Requires-Dist: pytest<9,>=7; extra == "tests"
33
- Requires-Dist: pytest-asyncio<1,>=0.23.2; extra == "tests"
33
+ Requires-Dist: pytest-asyncio<2,>=0.23.2; extra == "tests"
34
34
  Requires-Dist: pytest-cov>=4.1.0; extra == "tests"
35
35
  Requires-Dist: pytest-docker<4,>=1; extra == "tests"
36
36
  Requires-Dist: pytest-mock; extra == "tests"
@@ -38,12 +38,13 @@ Requires-Dist: pytest-sugar; extra == "tests"
38
38
  Requires-Dist: pytest-test-utils<0.2,>=0.1.0; extra == "tests"
39
39
  Requires-Dist: proxy.py; extra == "tests"
40
40
  Provides-Extra: dev
41
- Requires-Dist: mypy==1.14.1; extra == "dev"
41
+ Requires-Dist: mypy==1.17.0; extra == "dev"
42
42
  Requires-Dist: scmrepo[tests]; extra == "dev"
43
43
  Requires-Dist: types-certifi; extra == "dev"
44
44
  Requires-Dist: types-mock; extra == "dev"
45
45
  Requires-Dist: types-paramiko; extra == "dev"
46
46
  Requires-Dist: types-tqdm; extra == "dev"
47
+ Dynamic: license-file
47
48
 
48
49
  scmrepo
49
50
  =======
@@ -1,5 +1,5 @@
1
1
  gitpython>3
2
- dulwich>=0.22.1
2
+ dulwich>=0.23.1
3
3
  pygit2>=1.14.0
4
4
  pygtrie>=2.3.2
5
5
  fsspec[tqdm]>=2024.2.0
@@ -10,7 +10,7 @@ aiohttp-retry>=2.5.0
10
10
  tqdm
11
11
 
12
12
  [dev]
13
- mypy==1.14.1
13
+ mypy==1.17.0
14
14
  scmrepo[tests]
15
15
  types-certifi
16
16
  types-mock
@@ -21,7 +21,7 @@ types-tqdm
21
21
  aioresponses<0.8,>=0.7
22
22
  paramiko<4,>=3.4.0
23
23
  pytest<9,>=7
24
- pytest-asyncio<1,>=0.23.2
24
+ pytest-asyncio<2,>=0.23.2
25
25
  pytest-cov>=4.1.0
26
26
  pytest-docker<4,>=1
27
27
  pytest-mock
@@ -8,6 +8,7 @@ from unittest.mock import AsyncMock
8
8
  import asyncssh
9
9
  import paramiko
10
10
  import pytest
11
+ from paramiko.server import InteractiveQuery
11
12
  from pytest_mock import MockerFixture
12
13
  from pytest_test_utils.waiters import wait_until
13
14
 
@@ -52,13 +53,24 @@ class Server(paramiko.ServerInterface):
52
53
  """http://docs.paramiko.org/en/2.4/api/server.html."""
53
54
 
54
55
  def __init__(self, commands, *args, **kwargs) -> None:
55
- super().__init__(*args, **kwargs)
56
+ super().__init__()
56
57
  self.commands = commands
58
+ self.allowed_auths = kwargs.get("allowed_auths", "publickey,password")
57
59
 
58
60
  def check_channel_exec_request(self, channel, command):
59
61
  self.commands.append(command)
60
62
  return True
61
63
 
64
+ def check_auth_interactive(self, username: str, submethods: str):
65
+ return InteractiveQuery(
66
+ "Password", "Enter the password", f"Password for user {USER}:"
67
+ )
68
+
69
+ def check_auth_interactive_response(self, responses):
70
+ if responses[0] == PASSWORD:
71
+ return paramiko.AUTH_SUCCESSFUL
72
+ return paramiko.AUTH_FAILED
73
+
62
74
  def check_auth_password(self, username, password):
63
75
  if username == USER and password == PASSWORD:
64
76
  return paramiko.AUTH_SUCCESSFUL
@@ -76,12 +88,12 @@ class Server(paramiko.ServerInterface):
76
88
  return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
77
89
 
78
90
  def get_allowed_auths(self, username):
79
- return "password,publickey"
91
+ return self.allowed_auths
80
92
 
81
93
 
82
94
  @pytest.fixture
83
95
  def ssh_conn(request: pytest.FixtureRequest) -> dict[str, Any]:
84
- server = Server([])
96
+ server = Server([], **getattr(request, "param", {}))
85
97
 
86
98
  socket.setdefaulttimeout(10)
87
99
  request.addfinalizer(lambda: socket.setdefaulttimeout(None))
@@ -133,7 +145,8 @@ def test_run_command_password(server: Server, ssh_port: int):
133
145
  assert b"test_run_command_password" in server.commands
134
146
 
135
147
 
136
- def test_run_command_no_password(server: Server, ssh_port: int):
148
+ @pytest.mark.parametrize("ssh_conn", [{"allowed_auths": "publickey"}], indirect=True)
149
+ def test_run_command_no_password(ssh_port: int):
137
150
  vendor = AsyncSSHVendor()
138
151
  with pytest.raises(AuthError):
139
152
  vendor.run_command(
@@ -145,6 +158,28 @@ def test_run_command_no_password(server: Server, ssh_port: int):
145
158
  )
146
159
 
147
160
 
161
+ @pytest.mark.parametrize(
162
+ "ssh_conn",
163
+ [{"allowed_auths": "password"}, {"allowed_auths": "keyboard-interactive"}],
164
+ indirect=True,
165
+ ids=["password", "interactive"],
166
+ )
167
+ def test_should_prompt_for_password_when_no_password_passed(
168
+ mocker: MockerFixture, server: Server, ssh_port: int
169
+ ):
170
+ mocked_getpass = mocker.patch("getpass.getpass", return_value=PASSWORD)
171
+ vendor = AsyncSSHVendor()
172
+ vendor.run_command(
173
+ "127.0.0.1",
174
+ "test_run_command_password",
175
+ username=USER,
176
+ port=ssh_port,
177
+ password=None,
178
+ )
179
+ assert server.commands == [b"test_run_command_password"]
180
+ mocked_getpass.asssert_called_once()
181
+
182
+
148
183
  def test_run_command_with_privkey(server: Server, ssh_port: int):
149
184
  key = asyncssh.import_private_key(CLIENT_KEY)
150
185
 
@@ -212,28 +247,6 @@ def test_run_command_partial_transfer(ssh_port: int, mocker: MockerFixture):
212
247
  assert mock_stderr.call_count == 3
213
248
 
214
249
 
215
- @pytest.mark.parametrize("algorithm", [b"ssh-rsa", b"rsa-sha2-256", b"rsa-sha2-512"])
216
- def test_dulwich_github_compat(mocker: MockerFixture, algorithm: bytes):
217
- from asyncssh.misc import ProtocolError
218
-
219
- from scmrepo.git.backend.dulwich.asyncssh_vendor import _process_public_key_ok_gh
220
-
221
- key_data = b"foo"
222
- auth = mocker.Mock(
223
- _keypair=mocker.Mock(algorithm=algorithm, public_data=key_data),
224
- )
225
- packet = mocker.Mock()
226
-
227
- strings = iter((b"ed21556", key_data))
228
- packet.get_string = lambda: next(strings)
229
- with pytest.raises(ProtocolError):
230
- _process_public_key_ok_gh(auth, None, None, packet)
231
-
232
- strings = iter((b"ssh-rsa", key_data))
233
- packet.get_string = lambda: next(strings)
234
- _process_public_key_ok_gh(auth, None, None, packet)
235
-
236
-
237
250
  @pytest.mark.skipif(os.name != "nt", reason="Windows only")
238
251
  def test_git_bash_ssh_vendor(mocker):
239
252
  from dulwich.client import SubprocessSSHVendor
@@ -771,7 +771,7 @@ def test_ignored(tmp_dir: TmpDir, scm: Git, git: Git, git_backend: str):
771
771
  assert not git.is_ignored(tmp_dir / "dir1" / "file2.txt")
772
772
 
773
773
 
774
- @pytest.mark.skip_git_backend("pygit2", "gitpython")
774
+ @pytest.mark.skip_git_backend("pygit2", "gitpython", "dulwich")
775
775
  def test_ignored_dir_unignored_subdirs(tmp_dir: TmpDir, scm: Git, git: Git):
776
776
  tmp_dir.gen({".gitignore": "data/**\n!data/**/\n!data/**/*.csv"})
777
777
  scm.add([".gitignore"])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes