scmrepo 3.3.9__tar.gz → 3.3.11__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.9 → scmrepo-3.3.11}/.github/workflows/tests.yaml +2 -2
  2. {scmrepo-3.3.9 → scmrepo-3.3.11}/.pre-commit-config.yaml +2 -2
  3. {scmrepo-3.3.9/src/scmrepo.egg-info → scmrepo-3.3.11}/PKG-INFO +4 -3
  4. {scmrepo-3.3.9 → scmrepo-3.3.11}/pyproject.toml +1 -1
  5. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/fs.py +0 -31
  6. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/__init__.py +2 -2
  7. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/dulwich/asyncssh_vendor.py +19 -65
  8. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/gitpython.py +1 -1
  9. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/pygit2/__init__.py +4 -2
  10. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/pygit2/callbacks.py +2 -0
  11. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/credentials.py +1 -1
  12. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/fetch.py +1 -1
  13. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/objects.py +1 -1
  14. {scmrepo-3.3.9 → scmrepo-3.3.11/src/scmrepo.egg-info}/PKG-INFO +4 -3
  15. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo.egg-info/requires.txt +1 -1
  16. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_dulwich.py +39 -26
  17. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_lfs.py +1 -1
  18. {scmrepo-3.3.9 → scmrepo-3.3.11}/.coveragerc +0 -0
  19. {scmrepo-3.3.9 → scmrepo-3.3.11}/.cruft.json +0 -0
  20. {scmrepo-3.3.9 → scmrepo-3.3.11}/.gitattributes +0 -0
  21. {scmrepo-3.3.9 → scmrepo-3.3.11}/.github/dependabot.yml +0 -0
  22. {scmrepo-3.3.9 → scmrepo-3.3.11}/.github/workflows/release.yaml +0 -0
  23. {scmrepo-3.3.9 → scmrepo-3.3.11}/.github/workflows/update-template.yaml +0 -0
  24. {scmrepo-3.3.9 → scmrepo-3.3.11}/.gitignore +0 -0
  25. {scmrepo-3.3.9 → scmrepo-3.3.11}/CODE_OF_CONDUCT.rst +0 -0
  26. {scmrepo-3.3.9 → scmrepo-3.3.11}/CONTRIBUTING.rst +0 -0
  27. {scmrepo-3.3.9 → scmrepo-3.3.11}/LICENSE +0 -0
  28. {scmrepo-3.3.9 → scmrepo-3.3.11}/README.rst +0 -0
  29. {scmrepo-3.3.9 → scmrepo-3.3.11}/noxfile.py +0 -0
  30. {scmrepo-3.3.9 → scmrepo-3.3.11}/setup.cfg +0 -0
  31. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/__init__.py +0 -0
  32. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/asyn.py +0 -0
  33. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/base.py +0 -0
  34. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/exceptions.py +0 -0
  35. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/__init__.py +0 -0
  36. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/base.py +0 -0
  37. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/dulwich/__init__.py +0 -0
  38. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/dulwich/client.py +0 -0
  39. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/backend/pygit2/filter.py +0 -0
  40. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/config.py +0 -0
  41. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/__init__.py +0 -0
  42. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/client.py +0 -0
  43. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/exceptions.py +0 -0
  44. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/object.py +0 -0
  45. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/pointer.py +0 -0
  46. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/progress.py +0 -0
  47. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/smudge.py +0 -0
  48. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/lfs/storage.py +0 -0
  49. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/git/stash.py +0 -0
  50. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/noscm.py +0 -0
  51. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/progress.py +0 -0
  52. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/py.typed +0 -0
  53. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/urls.py +0 -0
  54. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo/utils.py +0 -0
  55. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo.egg-info/SOURCES.txt +0 -0
  56. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo.egg-info/dependency_links.txt +0 -0
  57. {scmrepo-3.3.9 → scmrepo-3.3.11}/src/scmrepo.egg-info/top_level.txt +0 -0
  58. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/__init__.py +0 -0
  59. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/conftest.py +0 -0
  60. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/docker-compose.yml +0 -0
  61. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/git-init/git.sh +0 -0
  62. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_credentials.py +0 -0
  63. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_fs.py +0 -0
  64. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_git.py +0 -0
  65. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_noscm.py +0 -0
  66. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_pygit2.py +0 -0
  67. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_scmrepo.py +0 -0
  68. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_stash.py +0 -0
  69. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/test_urls.py +0 -0
  70. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/user.key +0 -0
  71. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/user.key.pub +0 -0
  72. {scmrepo-3.3.9 → scmrepo-3.3.11}/tests/vendor/__init__.py +0 -0
  73. {scmrepo-3.3.9 → scmrepo-3.3.11}/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@v3.1.0
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.7.4'
23
+ rev: 'v0.11.7'
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.3.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.1
1
+ Metadata-Version: 2.4
2
2
  Name: scmrepo
3
- Version: 3.3.9
3
+ Version: 3.3.11
4
4
  Summary: scmrepo
5
5
  Author-email: Iterative <support@dvc.org>
6
6
  License: Apache-2.0
@@ -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.13.0; extra == "dev"
41
+ Requires-Dist: mypy==1.15.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
  =======
@@ -52,7 +52,7 @@ tests = [
52
52
  "proxy.py",
53
53
  ]
54
54
  dev = [
55
- "mypy==1.13.0",
55
+ "mypy==1.15.0",
56
56
  "scmrepo[tests]",
57
57
  "types-certifi",
58
58
  "types-mock",
@@ -3,9 +3,7 @@ import os
3
3
  import posixpath
4
4
  from typing import TYPE_CHECKING, Any, BinaryIO, Callable, Optional
5
5
 
6
- from fsspec.callbacks import _DEFAULT_CALLBACK
7
6
  from fsspec.spec import AbstractFileSystem
8
- from fsspec.utils import isfilelike
9
7
 
10
8
  if TYPE_CHECKING:
11
9
  from io import BytesIO
@@ -242,32 +240,3 @@ class GitFileSystem(AbstractFileSystem):
242
240
  return paths
243
241
 
244
242
  return [self.info(_path) for _path in paths]
245
-
246
- def get_file(
247
- self, rpath, lpath, callback=_DEFAULT_CALLBACK, outfile=None, **kwargs
248
- ):
249
- # NOTE: temporary workaround while waiting for
250
- # https://github.com/fsspec/filesystem_spec/pull/1191
251
-
252
- if isfilelike(lpath):
253
- outfile = lpath
254
- elif self.isdir(rpath):
255
- os.makedirs(lpath, exist_ok=True)
256
- return None
257
-
258
- with self.open(rpath, "rb", **kwargs) as f1:
259
- if outfile is None:
260
- outfile = open(lpath, "wb") # noqa: SIM115
261
-
262
- try:
263
- callback.set_size(getattr(f1, "size", None))
264
- data = True
265
- while data:
266
- data = f1.read(self.blocksize)
267
- segment_len = outfile.write(data)
268
- if segment_len is None:
269
- segment_len = len(data)
270
- callback.relative_update(segment_len)
271
- finally:
272
- if not isfilelike(lpath):
273
- outfile.close()
@@ -258,7 +258,7 @@ class Git(Base):
258
258
  self.hooks_dir.mkdir(exist_ok=True)
259
259
  hook = self.hooks_dir / name
260
260
 
261
- directive = f"#!{shutil.which(interpreter) or '/bin/sh' }"
261
+ directive = f"#!{shutil.which(interpreter) or '/bin/sh'}"
262
262
  hook.write_text(f"{directive}\n{script}\n", encoding="utf-8")
263
263
  hook.chmod(0o777)
264
264
 
@@ -287,7 +287,7 @@ class Git(Base):
287
287
  def no_commits(self):
288
288
  return not bool(self.get_ref("HEAD"))
289
289
 
290
- # Prefer re-using the most recently used backend when possible. When
290
+ # Prefer reusing the most recently used backend when possible. When
291
291
  # changing backends (due to unimplemented calls), we close the previous
292
292
  # backend to release any open git files/contexts that may cause conflicts
293
293
  # with the new backend.
@@ -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
@@ -97,45 +104,6 @@ class AsyncSSHWrapper(BaseAsyncObject):
97
104
  close = sync_wrapper(_close)
98
105
 
99
106
 
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
107
  class InteractiveSSHClient(SSHClient):
140
108
  _conn: Optional["SSHClientConnection"] = None
141
109
  _keys_to_try: Optional[list["FilePath"]] = None
@@ -171,7 +139,7 @@ class InteractiveSSHClient(SSHClient):
171
139
  self._keys_to_try = []
172
140
  options = self._conn._options # pylint: disable=protected-access
173
141
  config = options.config
174
- client_keys = cast(Sequence["FilePath"], config.get("IdentityFile", ()))
142
+ client_keys = cast("Sequence[FilePath]", config.get("IdentityFile", ()))
175
143
  if not client_keys:
176
144
  client_keys = [
177
145
  os.path.expanduser(os.path.join("~", ".ssh", path))
@@ -202,8 +170,6 @@ class InteractiveSSHClient(SSHClient):
202
170
  return None
203
171
 
204
172
  async def _read_private_key_interactive(self, path: "FilePath") -> "SSHKey":
205
- from getpass import getpass
206
-
207
173
  from asyncssh.public_key import (
208
174
  KeyEncryptionError,
209
175
  KeyImportError,
@@ -215,11 +181,8 @@ class InteractiveSSHClient(SSHClient):
215
181
  if passphrase:
216
182
  return read_private_key(path, passphrase=passphrase)
217
183
 
218
- loop = asyncio.get_running_loop()
219
184
  for _ in range(3):
220
- passphrase = await loop.run_in_executor(
221
- None, getpass, f"Enter passphrase for key '{path}': "
222
- )
185
+ passphrase = await _getpass(f"Enter passphrase for key {path!r}: ")
223
186
  if passphrase:
224
187
  try:
225
188
  key = read_private_key(path, passphrase=passphrase)
@@ -239,23 +202,20 @@ class InteractiveSSHClient(SSHClient):
239
202
  lang: str,
240
203
  prompts: "KbdIntPrompts",
241
204
  ) -> Optional["KbdIntResponse"]:
242
- from getpass import getpass
243
-
244
205
  if os.environ.get("GIT_TERMINAL_PROMPT") == "0":
245
206
  return None
246
207
 
247
- def _getpass(prompt: str) -> str:
248
- return getpass(prompt=prompt).rstrip()
249
-
250
208
  if instructions:
251
209
  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
- ]
210
+
211
+ response: list[str] = []
212
+ for prompt, _echo in prompts:
213
+ p = await _getpass(f"({name}) {prompt}" if name else prompt)
214
+ response.append(p.rstrip())
215
+ return response
216
+
217
+ async def password_auth_requested(self) -> str:
218
+ return await _getpass()
259
219
 
260
220
 
261
221
  class AsyncSSHVendor(BaseAsyncObject, SSHVendor):
@@ -286,12 +246,6 @@ class AsyncSSHVendor(BaseAsyncObject, SSHVendor):
286
246
  key_filename: Optional path to private keyfile
287
247
  """
288
248
  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
249
 
296
250
  try:
297
251
  conn = await asyncssh.connect(
@@ -184,7 +184,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
184
184
  # In fix_env, we delete LD_LIBRARY_PATH key if it was empty before
185
185
  # PyInstaller modified it. GitPython, in git.Repo.clone_from, uses
186
186
  # env to update its own internal state. When there is no key in
187
- # env, this value is not updated and GitPython re-uses
187
+ # env, this value is not updated and GitPython reuses
188
188
  # LD_LIBRARY_PATH that has been set by PyInstaller.
189
189
  # See [1] for more info.
190
190
  # [1] https://github.com/gitpython-developers/GitPython/issues/924
@@ -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:
@@ -720,7 +722,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
720
722
  for refname in remote_refs:
721
723
  if fnmatch.fnmatch(refname, lh):
722
724
  src = refname
723
- dst = f"{rh_prefix}{refname[len(lh_prefix):]}"
725
+ dst = f"{rh_prefix}{refname[len(lh_prefix) :]}"
724
726
  result[dst] = cb.result.get(
725
727
  src, _default_status(src, dst, remote_refs)
726
728
  )
@@ -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
@@ -324,7 +324,7 @@ def _input_tty(prompt: str = "Username: ") -> str:
324
324
  try:
325
325
  fd = os.open(
326
326
  "/dev/tty",
327
- os.O_RDWR | os.O_NOCTTY, # pylint: disable=no-member
327
+ os.O_RDWR | os.O_NOCTTY, # type: ignore[attr-defined]
328
328
  )
329
329
  tty = io.FileIO(fd, "w+")
330
330
  stack.enter_context(tty)
@@ -122,7 +122,7 @@ def _collect_objects(
122
122
  and (result := _ROOT_PATH_PREFIX_REGEX.match(path := include[0]))
123
123
  ):
124
124
  root = result.group("prefix")
125
- if path in {root, f'{root.rstrip("/")}/**'}:
125
+ if path in {root, f"{root.rstrip('/')}/**"}:
126
126
  include = []
127
127
  else:
128
128
  root = "/"
@@ -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.1
1
+ Metadata-Version: 2.4
2
2
  Name: scmrepo
3
- Version: 3.3.9
3
+ Version: 3.3.11
4
4
  Summary: scmrepo
5
5
  Author-email: Iterative <support@dvc.org>
6
6
  License: Apache-2.0
@@ -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.13.0; extra == "dev"
41
+ Requires-Dist: mypy==1.15.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
  =======
@@ -10,7 +10,7 @@ aiohttp-retry>=2.5.0
10
10
  tqdm
11
11
 
12
12
  [dev]
13
- mypy==1.13.0
13
+ mypy==1.15.0
14
14
  scmrepo[tests]
15
15
  types-certifi
16
16
  types-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
@@ -32,7 +32,7 @@ def storage(tmp_dir_factory: TempDirFactory) -> LFSStorage:
32
32
 
33
33
 
34
34
  @pytest.fixture
35
- def lfs(tmp_dir: TmpDir, scm: Git) -> None: # noqa: PT004
35
+ def lfs(tmp_dir: TmpDir, scm: Git) -> None:
36
36
  tmp_dir.gen(".gitattributes", "*.lfs filter=lfs diff=lfs merge=lfs -text")
37
37
  scm.add([".gitattributes"])
38
38
  scm.commit("init lfs attributes")
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
File without changes