parallel-ssh 2.12.0__py3-none-any.whl → 2.13.0rc1__py3-none-any.whl

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.
@@ -1,23 +1,22 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: parallel-ssh
3
- Version: 2.12.0
3
+ Version: 2.13.0rc1
4
4
  Summary: Asynchronous parallel SSH library
5
5
  Home-page: https://github.com/ParallelSSH/parallel-ssh
6
6
  Author: Panos Kittenis
7
7
  Author-email: zuboci@yandex.com
8
8
  License: LGPLv2.1
9
- Platform: UNKNOWN
10
9
  Classifier: Development Status :: 5 - Production/Stable
11
10
  Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)
12
11
  Classifier: Intended Audience :: Developers
13
12
  Classifier: Operating System :: OS Independent
14
13
  Classifier: Programming Language :: Python
15
14
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.6
17
- Classifier: Programming Language :: Python :: 3.7
18
15
  Classifier: Programming Language :: Python :: 3.8
19
16
  Classifier: Programming Language :: Python :: 3.9
20
17
  Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
21
20
  Classifier: Topic :: System :: Networking
22
21
  Classifier: Topic :: Software Development :: Libraries
23
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -28,7 +27,7 @@ Classifier: Operating System :: MacOS :: MacOS X
28
27
  License-File: LICENSE
29
28
  License-File: COPYING
30
29
  License-File: COPYING.LESSER
31
- Requires-Dist: gevent (>=1.3.0)
30
+ Requires-Dist: gevent
32
31
  Requires-Dist: ssh2-python
33
32
  Requires-Dist: ssh-python
34
33
 
@@ -59,6 +58,7 @@ Native code based clients with extremely high performance, making use of C libra
59
58
  :alt: Latest documentation
60
59
 
61
60
  .. _`read the docs`: https://parallel-ssh.readthedocs.org/en/latest/
61
+ .. _`SFTP and SCP documentation`: https://parallel-ssh.readthedocs.io/en/latest/advanced.html#sftp-scp
62
62
 
63
63
  ************
64
64
  Installation
@@ -273,7 +273,7 @@ To copy a local file to remote hosts in parallel with SCP:
273
273
  cmds = client.scp_send('../test', 'test_dir/test')
274
274
  joinall(cmds, raise_error=True)
275
275
 
276
- See `SFTP and SCP documentation <https://parallel-ssh.readthedocs.io/en/latest/advanced.html#sftp-scp>`_ for more examples.
276
+ See `SFTP and SCP documentation`_ for more examples.
277
277
 
278
278
 
279
279
  *****
@@ -309,10 +309,4 @@ In addition, per-host configurable file name functionality is provided for both
309
309
 
310
310
  Directory recursion is supported in both cases via the ``recurse`` parameter - defaults to off.
311
311
 
312
- See `SFTP and SCP documentation <https://parallel-ssh.readthedocs.io/en/latest/advanced.html#sftp-scp>`_ for more examples.
313
-
314
-
315
- .. image:: https://ga-beacon.appspot.com/UA-9132694-7/parallel-ssh/README.rst?pixel
316
- :target: https://github.com/igrigorik/ga-beacon
317
-
318
-
312
+ See `SFTP and SCP documentation`_ for more examples.
@@ -0,0 +1,27 @@
1
+ pssh/__init__.py,sha256=W7jdMnEJJfpd3LbTxdzOkjvspMs6VIHyZ0FH-eJGuOU,1444
2
+ pssh/_version.py,sha256=ORfZeFepHteZV6a-ErdSrZvIosQNSSEdGlc5mCrLh0Q,24494
3
+ pssh/config.py,sha256=xM0zRWY0Noig273Ki32pTw5U9ybV_NEBEUKGTX9Pp1c,10050
4
+ pssh/constants.py,sha256=toVllZuLBKxLYB8fuY_46UoPjmAA3J59oH1wS4OlL7g,1056
5
+ pssh/exceptions.py,sha256=ZjnrT_Ye1v4OH582jNwYUCkI4NVgujCWwu8aB8XymUM,2561
6
+ pssh/output.py,sha256=0pkyaUxTY4Oi36GYbGTNvPbxZRA2qC0aeXYoq9XXuR8,4665
7
+ pssh/utils.py,sha256=qdsVnKGxZovWGZpGS3eUyrfCaeWTKX7yETfA29zbLMk,1802
8
+ pssh/clients/__init__.py,sha256=QzNb1FcbjQyxEPtRbog-ZOidMRu0zA718PkRz21SMmQ,836
9
+ pssh/clients/common.py,sha256=fd_jJj8ezDjwr3peN3LonLff5EYSNkMEre0yjlnRjBU,1375
10
+ pssh/clients/reader.py,sha256=9boQMv9xUBGLY53OybJmWvfa6qsA6mudfdfXk4sF2Oc,3092
11
+ pssh/clients/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ pssh/clients/base/parallel.py,sha256=cNirNYL0tGw3usWD7XMm0BWVbT_9PNrUjjEOxHLREhw,25010
13
+ pssh/clients/base/single.py,sha256=9MxJwz6eq8UY2mBksDIyg7YZyRkmO2yAQle4Y2jSJjA,26132
14
+ pssh/clients/native/__init__.py,sha256=dydX5Fae9U9gRBT8koPTM8H9u4t0rIgD1q9XammFYSs,865
15
+ pssh/clients/native/parallel.py,sha256=AjOJoLdhyyUKWp4Leda08cXC-SsyT16XK4PExiOl3-c,24119
16
+ pssh/clients/native/single.py,sha256=zXbMzYxY7_hkVOS2b2tHujqHq4iFWYgXV4KIL2MVFz8,32118
17
+ pssh/clients/native/tunnel.py,sha256=294UpCYOYlhI9sgoZNvjxr5I199GNOmHevz1lbNVjQI,9309
18
+ pssh/clients/ssh/__init__.py,sha256=HgYZkVJ5QOgaVFp5snwyLZvQbp0Tbdxj8Gbyic2_Ibo,857
19
+ pssh/clients/ssh/parallel.py,sha256=2X47tgPklubHbb7RjUk75IZjidbbOdJq3VstH3KU6KM,11397
20
+ pssh/clients/ssh/single.py,sha256=oSolU4_D-38rkT1eRqOl33js0CmR727tn9wj9T5qXmI,13617
21
+ parallel_ssh-2.13.0rc1.dist-info/COPYING,sha256=ZA2Q9u5AEkH_YoNNDRsz-DBJ6ZuL_foE7RsKFjXd4-c,18093
22
+ parallel_ssh-2.13.0rc1.dist-info/COPYING.LESSER,sha256=AKibDRiqzUEU3s95Ei24e_Nb3a8rxQ44PJyfTCYzkLI,24486
23
+ parallel_ssh-2.13.0rc1.dist-info/LICENSE,sha256=m4cqigcLitMpxL04D7G_AAD1ZMdQI-yOHmgD8VNkuek,26461
24
+ parallel_ssh-2.13.0rc1.dist-info/METADATA,sha256=EsRUli7LAx_verG8ezBIb2hGb1uV_e0ltHIgUMJ9cqE,10733
25
+ parallel_ssh-2.13.0rc1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
26
+ parallel_ssh-2.13.0rc1.dist-info/top_level.txt,sha256=s8P6ZHOwt2BYgDc62Cpd2z7i-rebGzIhhnO09pger0U,5
27
+ parallel_ssh-2.13.0rc1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.37.0)
2
+ Generator: setuptools (75.6.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
pssh/__init__.py CHANGED
@@ -29,9 +29,9 @@ for class documentation.
29
29
 
30
30
 
31
31
  from logging import getLogger, NullHandler
32
- from ._version import get_versions
33
- __version__ = get_versions()['version']
34
- del get_versions
32
+ from . import _version
33
+ __version__ = _version.get_versions()['version']
34
+ del _version
35
35
 
36
36
  host_logger = getLogger('pssh.host_logger')
37
37
  logger = getLogger('pssh')
pssh/_version.py CHANGED
@@ -5,8 +5,9 @@
5
5
  # directories (produced by setup.py build) will contain a much shorter file
6
6
  # that just contains the computed version number.
7
7
 
8
- # This file is released into the public domain. Generated by
9
- # versioneer-0.18 (https://github.com/warner/python-versioneer)
8
+ # This file is released into the public domain.
9
+ # Generated by versioneer-0.29
10
+ # https://github.com/python-versioneer/python-versioneer
10
11
 
11
12
  """Git implementation of _version.py."""
12
13
 
@@ -15,9 +16,11 @@ import os
15
16
  import re
16
17
  import subprocess
17
18
  import sys
19
+ from typing import Any, Callable, Dict, List, Optional, Tuple
20
+ import functools
18
21
 
19
22
 
20
- def get_keywords():
23
+ def get_keywords() -> Dict[str, str]:
21
24
  """Get the keywords needed to look up the version information."""
22
25
  # these strings will be replaced by git during git-archive.
23
26
  # setup.py/versioneer.py will grep for the variable names, so they must
@@ -33,8 +36,15 @@ def get_keywords():
33
36
  class VersioneerConfig:
34
37
  """Container for Versioneer configuration parameters."""
35
38
 
39
+ VCS: str
40
+ style: str
41
+ tag_prefix: str
42
+ parentdir_prefix: str
43
+ versionfile_source: str
44
+ verbose: bool
36
45
 
37
- def get_config():
46
+
47
+ def get_config() -> VersioneerConfig:
38
48
  """Create, populate and return the VersioneerConfig() object."""
39
49
  # these strings are filled in when 'setup.py versioneer' creates
40
50
  # _version.py
@@ -52,13 +62,13 @@ class NotThisMethod(Exception):
52
62
  """Exception raised if a method is not valid for the current scenario."""
53
63
 
54
64
 
55
- LONG_VERSION_PY = {}
56
- HANDLERS = {}
65
+ LONG_VERSION_PY: Dict[str, str] = {}
66
+ HANDLERS: Dict[str, Dict[str, Callable]] = {}
57
67
 
58
68
 
59
- def register_vcs_handler(vcs, method): # decorator
60
- """Decorator to mark a method as the handler for a particular VCS."""
61
- def decorate(f):
69
+ def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator
70
+ """Create decorator to mark a method as the handler of a VCS."""
71
+ def decorate(f: Callable) -> Callable:
62
72
  """Store f in HANDLERS[vcs][method]."""
63
73
  if vcs not in HANDLERS:
64
74
  HANDLERS[vcs] = {}
@@ -67,22 +77,35 @@ def register_vcs_handler(vcs, method): # decorator
67
77
  return decorate
68
78
 
69
79
 
70
- def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
71
- env=None):
80
+ def run_command(
81
+ commands: List[str],
82
+ args: List[str],
83
+ cwd: Optional[str] = None,
84
+ verbose: bool = False,
85
+ hide_stderr: bool = False,
86
+ env: Optional[Dict[str, str]] = None,
87
+ ) -> Tuple[Optional[str], Optional[int]]:
72
88
  """Call the given command(s)."""
73
89
  assert isinstance(commands, list)
74
- p = None
75
- for c in commands:
90
+ process = None
91
+
92
+ popen_kwargs: Dict[str, Any] = {}
93
+ if sys.platform == "win32":
94
+ # This hides the console window if pythonw.exe is used
95
+ startupinfo = subprocess.STARTUPINFO()
96
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
97
+ popen_kwargs["startupinfo"] = startupinfo
98
+
99
+ for command in commands:
76
100
  try:
77
- dispcmd = str([c] + args)
101
+ dispcmd = str([command] + args)
78
102
  # remember shell=False, so use git.cmd on windows, not just git
79
- p = subprocess.Popen([c] + args, cwd=cwd, env=env,
80
- stdout=subprocess.PIPE,
81
- stderr=(subprocess.PIPE if hide_stderr
82
- else None))
103
+ process = subprocess.Popen([command] + args, cwd=cwd, env=env,
104
+ stdout=subprocess.PIPE,
105
+ stderr=(subprocess.PIPE if hide_stderr
106
+ else None), **popen_kwargs)
83
107
  break
84
- except EnvironmentError:
85
- e = sys.exc_info()[1]
108
+ except OSError as e:
86
109
  if e.errno == errno.ENOENT:
87
110
  continue
88
111
  if verbose:
@@ -93,18 +116,20 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
93
116
  if verbose:
94
117
  print("unable to find command, tried %s" % (commands,))
95
118
  return None, None
96
- stdout = p.communicate()[0].strip()
97
- if sys.version_info[0] >= 3:
98
- stdout = stdout.decode()
99
- if p.returncode != 0:
119
+ stdout = process.communicate()[0].strip().decode()
120
+ if process.returncode != 0:
100
121
  if verbose:
101
122
  print("unable to run %s (error)" % dispcmd)
102
123
  print("stdout was %s" % stdout)
103
- return None, p.returncode
104
- return stdout, p.returncode
124
+ return None, process.returncode
125
+ return stdout, process.returncode
105
126
 
106
127
 
107
- def versions_from_parentdir(parentdir_prefix, root, verbose):
128
+ def versions_from_parentdir(
129
+ parentdir_prefix: str,
130
+ root: str,
131
+ verbose: bool,
132
+ ) -> Dict[str, Any]:
108
133
  """Try to determine the version from the parent directory name.
109
134
 
110
135
  Source tarballs conventionally unpack into a directory that includes both
@@ -113,15 +138,14 @@ def versions_from_parentdir(parentdir_prefix, root, verbose):
113
138
  """
114
139
  rootdirs = []
115
140
 
116
- for i in range(3):
141
+ for _ in range(3):
117
142
  dirname = os.path.basename(root)
118
143
  if dirname.startswith(parentdir_prefix):
119
144
  return {"version": dirname[len(parentdir_prefix):],
120
145
  "full-revisionid": None,
121
146
  "dirty": False, "error": None, "date": None}
122
- else:
123
- rootdirs.append(root)
124
- root = os.path.dirname(root) # up a level
147
+ rootdirs.append(root)
148
+ root = os.path.dirname(root) # up a level
125
149
 
126
150
  if verbose:
127
151
  print("Tried directories %s but none started with prefix %s" %
@@ -130,41 +154,48 @@ def versions_from_parentdir(parentdir_prefix, root, verbose):
130
154
 
131
155
 
132
156
  @register_vcs_handler("git", "get_keywords")
133
- def git_get_keywords(versionfile_abs):
157
+ def git_get_keywords(versionfile_abs: str) -> Dict[str, str]:
134
158
  """Extract version information from the given file."""
135
159
  # the code embedded in _version.py can just fetch the value of these
136
160
  # keywords. When used from setup.py, we don't want to import _version.py,
137
161
  # so we do it with a regexp instead. This function is not used from
138
162
  # _version.py.
139
- keywords = {}
163
+ keywords: Dict[str, str] = {}
140
164
  try:
141
- f = open(versionfile_abs, "r")
142
- for line in f.readlines():
143
- if line.strip().startswith("git_refnames ="):
144
- mo = re.search(r'=\s*"(.*)"', line)
145
- if mo:
146
- keywords["refnames"] = mo.group(1)
147
- if line.strip().startswith("git_full ="):
148
- mo = re.search(r'=\s*"(.*)"', line)
149
- if mo:
150
- keywords["full"] = mo.group(1)
151
- if line.strip().startswith("git_date ="):
152
- mo = re.search(r'=\s*"(.*)"', line)
153
- if mo:
154
- keywords["date"] = mo.group(1)
155
- f.close()
156
- except EnvironmentError:
165
+ with open(versionfile_abs, "r") as fobj:
166
+ for line in fobj:
167
+ if line.strip().startswith("git_refnames ="):
168
+ mo = re.search(r'=\s*"(.*)"', line)
169
+ if mo:
170
+ keywords["refnames"] = mo.group(1)
171
+ if line.strip().startswith("git_full ="):
172
+ mo = re.search(r'=\s*"(.*)"', line)
173
+ if mo:
174
+ keywords["full"] = mo.group(1)
175
+ if line.strip().startswith("git_date ="):
176
+ mo = re.search(r'=\s*"(.*)"', line)
177
+ if mo:
178
+ keywords["date"] = mo.group(1)
179
+ except OSError:
157
180
  pass
158
181
  return keywords
159
182
 
160
183
 
161
184
  @register_vcs_handler("git", "keywords")
162
- def git_versions_from_keywords(keywords, tag_prefix, verbose):
185
+ def git_versions_from_keywords(
186
+ keywords: Dict[str, str],
187
+ tag_prefix: str,
188
+ verbose: bool,
189
+ ) -> Dict[str, Any]:
163
190
  """Get version information from git keywords."""
164
- if not keywords:
165
- raise NotThisMethod("no keywords at all, weird")
191
+ if "refnames" not in keywords:
192
+ raise NotThisMethod("Short version file found")
166
193
  date = keywords.get("date")
167
194
  if date is not None:
195
+ # Use only the last line. Previous lines may contain GPG signature
196
+ # information.
197
+ date = date.splitlines()[-1]
198
+
168
199
  # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
169
200
  # datestamp. However we prefer "%ci" (which expands to an "ISO-8601
170
201
  # -like" string, which we must then edit to make compliant), because
@@ -177,11 +208,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
177
208
  if verbose:
178
209
  print("keywords are unexpanded, not using")
179
210
  raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
180
- refs = set([r.strip() for r in refnames.strip("()").split(",")])
211
+ refs = {r.strip() for r in refnames.strip("()").split(",")}
181
212
  # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
182
213
  # just "foo-1.0". If we see a "tag: " prefix, prefer those.
183
214
  TAG = "tag: "
184
- tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
215
+ tags = {r[len(TAG):] for r in refs if r.startswith(TAG)}
185
216
  if not tags:
186
217
  # Either we're using git < 1.8.3, or there really are no tags. We use
187
218
  # a heuristic: assume all version tags have a digit. The old git %d
@@ -190,7 +221,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
190
221
  # between branches and tags. By ignoring refnames without digits, we
191
222
  # filter out many common branch names like "release" and
192
223
  # "stabilization", as well as "HEAD" and "master".
193
- tags = set([r for r in refs if re.search(r'\d', r)])
224
+ tags = {r for r in refs if re.search(r'\d', r)}
194
225
  if verbose:
195
226
  print("discarding '%s', no digits" % ",".join(refs - tags))
196
227
  if verbose:
@@ -199,6 +230,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
199
230
  # sorting will prefer e.g. "2.0" over "2.0rc1"
200
231
  if ref.startswith(tag_prefix):
201
232
  r = ref[len(tag_prefix):]
233
+ # Filter out refs that exactly match prefix or that don't start
234
+ # with a number once the prefix is stripped (mostly a concern
235
+ # when prefix is '')
236
+ if not re.match(r'\d', r):
237
+ continue
202
238
  if verbose:
203
239
  print("picking %s" % r)
204
240
  return {"version": r,
@@ -214,7 +250,12 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
214
250
 
215
251
 
216
252
  @register_vcs_handler("git", "pieces_from_vcs")
217
- def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
253
+ def git_pieces_from_vcs(
254
+ tag_prefix: str,
255
+ root: str,
256
+ verbose: bool,
257
+ runner: Callable = run_command
258
+ ) -> Dict[str, Any]:
218
259
  """Get version from 'git describe' in the root of the source tree.
219
260
 
220
261
  This only gets called if the git-archive 'subst' keywords were *not*
@@ -225,8 +266,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
225
266
  if sys.platform == "win32":
226
267
  GITS = ["git.cmd", "git.exe"]
227
268
 
228
- out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
229
- hide_stderr=True)
269
+ # GIT_DIR can interfere with correct operation of Versioneer.
270
+ # It may be intended to be passed to the Versioneer-versioned project,
271
+ # but that should not change where we get our version from.
272
+ env = os.environ.copy()
273
+ env.pop("GIT_DIR", None)
274
+ runner = functools.partial(runner, env=env)
275
+
276
+ _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root,
277
+ hide_stderr=not verbose)
230
278
  if rc != 0:
231
279
  if verbose:
232
280
  print("Directory %s not under git control" % root)
@@ -234,24 +282,57 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
234
282
 
235
283
  # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
236
284
  # if there isn't one, this yields HEX[-dirty] (no NUM)
237
- describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
238
- "--always", "--long",
239
- "--match", "%s*" % tag_prefix],
240
- cwd=root)
285
+ describe_out, rc = runner(GITS, [
286
+ "describe", "--tags", "--dirty", "--always", "--long",
287
+ "--match", f"{tag_prefix}[[:digit:]]*"
288
+ ], cwd=root)
241
289
  # --long was added in git-1.5.5
242
290
  if describe_out is None:
243
291
  raise NotThisMethod("'git describe' failed")
244
292
  describe_out = describe_out.strip()
245
- full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
293
+ full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root)
246
294
  if full_out is None:
247
295
  raise NotThisMethod("'git rev-parse' failed")
248
296
  full_out = full_out.strip()
249
297
 
250
- pieces = {}
298
+ pieces: Dict[str, Any] = {}
251
299
  pieces["long"] = full_out
252
300
  pieces["short"] = full_out[:7] # maybe improved later
253
301
  pieces["error"] = None
254
302
 
303
+ branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"],
304
+ cwd=root)
305
+ # --abbrev-ref was added in git-1.6.3
306
+ if rc != 0 or branch_name is None:
307
+ raise NotThisMethod("'git rev-parse --abbrev-ref' returned error")
308
+ branch_name = branch_name.strip()
309
+
310
+ if branch_name == "HEAD":
311
+ # If we aren't exactly on a branch, pick a branch which represents
312
+ # the current commit. If all else fails, we are on a branchless
313
+ # commit.
314
+ branches, rc = runner(GITS, ["branch", "--contains"], cwd=root)
315
+ # --contains was added in git-1.5.4
316
+ if rc != 0 or branches is None:
317
+ raise NotThisMethod("'git branch --contains' returned error")
318
+ branches = branches.split("\n")
319
+
320
+ # Remove the first line if we're running detached
321
+ if "(" in branches[0]:
322
+ branches.pop(0)
323
+
324
+ # Strip off the leading "* " from the list of branches.
325
+ branches = [branch[2:] for branch in branches]
326
+ if "master" in branches:
327
+ branch_name = "master"
328
+ elif not branches:
329
+ branch_name = None
330
+ else:
331
+ # Pick the first branch that is returned. Good or bad.
332
+ branch_name = branches[0]
333
+
334
+ pieces["branch"] = branch_name
335
+
255
336
  # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
256
337
  # TAG might have hyphens.
257
338
  git_describe = describe_out
@@ -268,7 +349,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
268
349
  # TAG-NUM-gHEX
269
350
  mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
270
351
  if not mo:
271
- # unparseable. Maybe git-describe is misbehaving?
352
+ # unparsable. Maybe git-describe is misbehaving?
272
353
  pieces["error"] = ("unable to parse git-describe output: '%s'"
273
354
  % describe_out)
274
355
  return pieces
@@ -293,26 +374,27 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
293
374
  else:
294
375
  # HEX: no tags
295
376
  pieces["closest-tag"] = None
296
- count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
297
- cwd=root)
298
- pieces["distance"] = int(count_out) # total number of commits
377
+ out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root)
378
+ pieces["distance"] = len(out.split()) # total number of commits
299
379
 
300
380
  # commit date: see ISO-8601 comment in git_versions_from_keywords()
301
- date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
302
- cwd=root)[0].strip()
381
+ date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip()
382
+ # Use only the last line. Previous lines may contain GPG signature
383
+ # information.
384
+ date = date.splitlines()[-1]
303
385
  pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
304
386
 
305
387
  return pieces
306
388
 
307
389
 
308
- def plus_or_dot(pieces):
390
+ def plus_or_dot(pieces: Dict[str, Any]) -> str:
309
391
  """Return a + if we don't already have one, else return a ."""
310
392
  if "+" in pieces.get("closest-tag", ""):
311
393
  return "."
312
394
  return "+"
313
395
 
314
396
 
315
- def render_pep440(pieces):
397
+ def render_pep440(pieces: Dict[str, Any]) -> str:
316
398
  """Build up version string, with post-release "local version identifier".
317
399
 
318
400
  Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
@@ -337,23 +419,71 @@ def render_pep440(pieces):
337
419
  return rendered
338
420
 
339
421
 
340
- def render_pep440_pre(pieces):
341
- """TAG[.post.devDISTANCE] -- No -dirty.
422
+ def render_pep440_branch(pieces: Dict[str, Any]) -> str:
423
+ """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .
424
+
425
+ The ".dev0" means not master branch. Note that .dev0 sorts backwards
426
+ (a feature branch will appear "older" than the master branch).
342
427
 
343
428
  Exceptions:
344
- 1: no tags. 0.post.devDISTANCE
429
+ 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty]
345
430
  """
346
431
  if pieces["closest-tag"]:
347
432
  rendered = pieces["closest-tag"]
433
+ if pieces["distance"] or pieces["dirty"]:
434
+ if pieces["branch"] != "master":
435
+ rendered += ".dev0"
436
+ rendered += plus_or_dot(pieces)
437
+ rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
438
+ if pieces["dirty"]:
439
+ rendered += ".dirty"
440
+ else:
441
+ # exception #1
442
+ rendered = "0"
443
+ if pieces["branch"] != "master":
444
+ rendered += ".dev0"
445
+ rendered += "+untagged.%d.g%s" % (pieces["distance"],
446
+ pieces["short"])
447
+ if pieces["dirty"]:
448
+ rendered += ".dirty"
449
+ return rendered
450
+
451
+
452
+ def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]:
453
+ """Split pep440 version string at the post-release segment.
454
+
455
+ Returns the release segments before the post-release and the
456
+ post-release version number (or -1 if no post-release segment is present).
457
+ """
458
+ vc = str.split(ver, ".post")
459
+ return vc[0], int(vc[1] or 0) if len(vc) == 2 else None
460
+
461
+
462
+ def render_pep440_pre(pieces: Dict[str, Any]) -> str:
463
+ """TAG[.postN.devDISTANCE] -- No -dirty.
464
+
465
+ Exceptions:
466
+ 1: no tags. 0.post0.devDISTANCE
467
+ """
468
+ if pieces["closest-tag"]:
348
469
  if pieces["distance"]:
349
- rendered += ".post.dev%d" % pieces["distance"]
470
+ # update the post release segment
471
+ tag_version, post_version = pep440_split_post(pieces["closest-tag"])
472
+ rendered = tag_version
473
+ if post_version is not None:
474
+ rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"])
475
+ else:
476
+ rendered += ".post0.dev%d" % (pieces["distance"])
477
+ else:
478
+ # no commits, use the tag as the version
479
+ rendered = pieces["closest-tag"]
350
480
  else:
351
481
  # exception #1
352
- rendered = "0.post.dev%d" % pieces["distance"]
482
+ rendered = "0.post0.dev%d" % pieces["distance"]
353
483
  return rendered
354
484
 
355
485
 
356
- def render_pep440_post(pieces):
486
+ def render_pep440_post(pieces: Dict[str, Any]) -> str:
357
487
  """TAG[.postDISTANCE[.dev0]+gHEX] .
358
488
 
359
489
  The ".dev0" means dirty. Note that .dev0 sorts backwards
@@ -380,12 +510,41 @@ def render_pep440_post(pieces):
380
510
  return rendered
381
511
 
382
512
 
383
- def render_pep440_old(pieces):
513
+ def render_pep440_post_branch(pieces: Dict[str, Any]) -> str:
514
+ """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .
515
+
516
+ The ".dev0" means not master branch.
517
+
518
+ Exceptions:
519
+ 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty]
520
+ """
521
+ if pieces["closest-tag"]:
522
+ rendered = pieces["closest-tag"]
523
+ if pieces["distance"] or pieces["dirty"]:
524
+ rendered += ".post%d" % pieces["distance"]
525
+ if pieces["branch"] != "master":
526
+ rendered += ".dev0"
527
+ rendered += plus_or_dot(pieces)
528
+ rendered += "g%s" % pieces["short"]
529
+ if pieces["dirty"]:
530
+ rendered += ".dirty"
531
+ else:
532
+ # exception #1
533
+ rendered = "0.post%d" % pieces["distance"]
534
+ if pieces["branch"] != "master":
535
+ rendered += ".dev0"
536
+ rendered += "+g%s" % pieces["short"]
537
+ if pieces["dirty"]:
538
+ rendered += ".dirty"
539
+ return rendered
540
+
541
+
542
+ def render_pep440_old(pieces: Dict[str, Any]) -> str:
384
543
  """TAG[.postDISTANCE[.dev0]] .
385
544
 
386
545
  The ".dev0" means dirty.
387
546
 
388
- Eexceptions:
547
+ Exceptions:
389
548
  1: no tags. 0.postDISTANCE[.dev0]
390
549
  """
391
550
  if pieces["closest-tag"]:
@@ -402,7 +561,7 @@ def render_pep440_old(pieces):
402
561
  return rendered
403
562
 
404
563
 
405
- def render_git_describe(pieces):
564
+ def render_git_describe(pieces: Dict[str, Any]) -> str:
406
565
  """TAG[-DISTANCE-gHEX][-dirty].
407
566
 
408
567
  Like 'git describe --tags --dirty --always'.
@@ -422,7 +581,7 @@ def render_git_describe(pieces):
422
581
  return rendered
423
582
 
424
583
 
425
- def render_git_describe_long(pieces):
584
+ def render_git_describe_long(pieces: Dict[str, Any]) -> str:
426
585
  """TAG-DISTANCE-gHEX[-dirty].
427
586
 
428
587
  Like 'git describe --tags --dirty --always -long'.
@@ -442,7 +601,7 @@ def render_git_describe_long(pieces):
442
601
  return rendered
443
602
 
444
603
 
445
- def render(pieces, style):
604
+ def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]:
446
605
  """Render the given version pieces into the requested style."""
447
606
  if pieces["error"]:
448
607
  return {"version": "unknown",
@@ -456,10 +615,14 @@ def render(pieces, style):
456
615
 
457
616
  if style == "pep440":
458
617
  rendered = render_pep440(pieces)
618
+ elif style == "pep440-branch":
619
+ rendered = render_pep440_branch(pieces)
459
620
  elif style == "pep440-pre":
460
621
  rendered = render_pep440_pre(pieces)
461
622
  elif style == "pep440-post":
462
623
  rendered = render_pep440_post(pieces)
624
+ elif style == "pep440-post-branch":
625
+ rendered = render_pep440_post_branch(pieces)
463
626
  elif style == "pep440-old":
464
627
  rendered = render_pep440_old(pieces)
465
628
  elif style == "git-describe":
@@ -474,7 +637,7 @@ def render(pieces, style):
474
637
  "date": pieces.get("date")}
475
638
 
476
639
 
477
- def get_versions():
640
+ def get_versions() -> Dict[str, Any]:
478
641
  """Get version information or return default if unable to do so."""
479
642
  # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
480
643
  # __file__, we can work backwards from there to the root. Some
@@ -495,7 +658,7 @@ def get_versions():
495
658
  # versionfile_source is the relative path from the top of the source
496
659
  # tree (where the .git directory might live) to this file. Invert
497
660
  # this to find the root from __file__.
498
- for i in cfg.versionfile_source.split('/'):
661
+ for _ in cfg.versionfile_source.split('/'):
499
662
  root = os.path.dirname(root)
500
663
  except NameError:
501
664
  return {"version": "0+unknown", "full-revisionid": None,
pssh/clients/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # This file is part of parallel-ssh.
2
2
  #
3
- # Copyright (C) 2014-2022 Panos Kittenis and contributors.
3
+ # Copyright (C) 2014-2025 Panos Kittenis and contributors.
4
4
  #
5
5
  # This library is free software; you can redistribute it and/or
6
6
  # modify it under the terms of the GNU Lesser General Public
@@ -16,5 +16,4 @@
16
16
  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
18
  # flake8: noqa: F401
19
- from .native.parallel import ParallelSSHClient
20
- from .native.single import SSHClient
19
+ from .native import ParallelSSHClient, SSHClient
@@ -118,6 +118,7 @@ class BaseParallelSSHClient(object):
118
118
  self.disconnect()
119
119
 
120
120
  def disconnect(self):
121
+ """Disconnect all clients."""
121
122
  if not hasattr(self, '_host_clients'):
122
123
  return
123
124
  for s_client in self._host_clients.values():
@@ -126,7 +127,6 @@ class BaseParallelSSHClient(object):
126
127
  except Exception as ex:
127
128
  logger.debug("Client disconnect failed with %s", ex)
128
129
  pass
129
- del s_client
130
130
 
131
131
  def _check_host_config(self):
132
132
  if self.host_config is None:
@@ -266,7 +266,7 @@ class BaseParallelSSHClient(object):
266
266
  :param cmds: Commands to get output for. Defaults to ``client.cmds``
267
267
  :type cmds: list(:py:class:`gevent.Greenlet`)
268
268
 
269
- :rtype: dict or list
269
+ :rtype: list(:py:class:`pssh.output.HostOutput`)
270
270
  """
271
271
  cmds = self.cmds if cmds is None else cmds
272
272
  if cmds is None:
@@ -290,7 +290,6 @@ class BaseParallelSSHClient(object):
290
290
  gssapi_server_identity=self.gssapi_server_identity,
291
291
  gssapi_client_identity=self.gssapi_client_identity,
292
292
  gssapi_delegate_credentials=self.gssapi_delegate_credentials,
293
- alias=None,
294
293
  )
295
294
  return config
296
295
  config = self.host_config[host_i]
@@ -68,6 +68,8 @@ class Stdin(object):
68
68
 
69
69
  def flush(self):
70
70
  """Flush pending data written to stdin."""
71
+ if not hasattr(self._channel, "flush"):
72
+ return
71
73
  return self._client._eagain(self._channel.flush)
72
74
 
73
75
 
@@ -213,10 +213,12 @@ class SSHClient(BaseSSHClient):
213
213
  sleep(self._eagain(self.session.keepalive_send))
214
214
 
215
215
  def configure_keepalive(self):
216
+ """Configures keepalive on the server for `self.keepalive_seconds`."""
216
217
  self.session.keepalive_config(False, self.keepalive_seconds)
217
218
 
218
219
  def _init_session(self, retries=1):
219
220
  self.session = Session()
221
+
220
222
  if self.timeout:
221
223
  # libssh2 timeout is in ms
222
224
  self.session.set_timeout(self.timeout * 1000)
@@ -266,7 +268,10 @@ class SSHClient(BaseSSHClient):
266
268
  return chan
267
269
 
268
270
  def open_session(self):
269
- """Open new channel from session"""
271
+ """Open new channel from session.
272
+
273
+ :rtype: :py:class:`ssh2.channel.Channel`
274
+ """
270
275
  try:
271
276
  chan = self._open_session()
272
277
  except Exception as ex:
@@ -662,9 +667,9 @@ class SSHClient(BaseSSHClient):
662
667
  elif remote_file.endswith('/'):
663
668
  local_filename = local_file.rsplit('/')[-1]
664
669
  remote_file += local_filename
665
- self._scp_send(local_file, remote_file)
666
670
  logger.info("SCP local file %s to remote destination %s:%s",
667
671
  local_file, self.host, remote_file)
672
+ self._scp_send(local_file, remote_file)
668
673
 
669
674
  def _scp_send(self, local_file, remote_file):
670
675
  fileinfo = os.stat(local_file)
@@ -16,9 +16,8 @@
16
16
  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
18
  import logging
19
-
20
- from threading import Thread, Event
21
19
  from queue import Queue
20
+ from threading import Thread, Event
22
21
 
23
22
  from gevent import spawn, joinall, get_hub, sleep
24
23
  from gevent.server import StreamServer
@@ -26,7 +25,6 @@ from ssh2.error_codes import LIBSSH2_ERROR_EAGAIN
26
25
 
27
26
  from ...constants import DEFAULT_RETRIES
28
27
 
29
-
30
28
  logger = logging.getLogger(__name__)
31
29
 
32
30
 
pssh/clients/reader.py CHANGED
@@ -34,11 +34,13 @@ class _Eof(Event):
34
34
  class ConcurrentRWBuffer(object):
35
35
  """Concurrent reader/writer of bytes for use from multiple greenlets.
36
36
 
37
- Supports both concurrent reading and writing.
37
+ Supports both concurrent reading and writing and combinations there of.
38
38
 
39
39
  Iterate on buffer object to read data, yielding event loop if no data exists
40
40
  until self.eof has been set.
41
41
 
42
+ Check if end-of-file without blocking with ``ConcurrentRWBuffer.eof.is_set()``.
43
+
42
44
  Writers should call ``ConcurrentRWBuffer.eof.set()`` when finished writing data via ``write``.
43
45
 
44
46
  Readers can use ``read()`` to get any available data or ``None``.
pssh/config.py CHANGED
@@ -59,7 +59,7 @@ class HostConfig(object):
59
59
  :param allow_agent: Enable/disable SSH agent authentication.
60
60
  :type allow_agent: bool
61
61
  :param alias: Use an alias for this host.
62
- :type alias: str or int
62
+ :type alias: str
63
63
  :param num_retries: Number of retry attempts before giving up on connection
64
64
  and SSH operations.
65
65
  :type num_retries: int
pssh/output.py CHANGED
@@ -70,7 +70,7 @@ class HostOutput(object):
70
70
  :param stdin: Standard input buffer
71
71
  :type stdin: :py:func:`file`-like object
72
72
  :param client: `SSHClient` output is coming from.
73
- :type client: :py:class:`pssh.clients.base.single.BaseSSHClient`
73
+ :type client: :py:class:`pssh.clients.base.single.BaseSSHClient` or `None`.
74
74
  :param alias: Host alias.
75
75
  :type alias: str
76
76
  :param exception: Exception from host if any
@@ -1,27 +0,0 @@
1
- pssh/__init__.py,sha256=Kd_8EGJTiujApfM-2gB4JlCr1m37rGQvtrlIaWu3CzM,1451
2
- pssh/_version.py,sha256=Oc3htkNS0sL05sUFjLa2gXhHYAZ65Defi2SyddJyChk,18445
3
- pssh/config.py,sha256=2HyyPK5zLbH5TObOYshI5qE7t4Bk2tNhT8F0U9KSFII,10057
4
- pssh/constants.py,sha256=toVllZuLBKxLYB8fuY_46UoPjmAA3J59oH1wS4OlL7g,1056
5
- pssh/exceptions.py,sha256=ZjnrT_Ye1v4OH582jNwYUCkI4NVgujCWwu8aB8XymUM,2561
6
- pssh/output.py,sha256=P8e9rXNfXRPWIFQwLsLkw68X8ApIQ-EIXOfQr8wlH1g,4654
7
- pssh/utils.py,sha256=qdsVnKGxZovWGZpGS3eUyrfCaeWTKX7yETfA29zbLMk,1802
8
- pssh/clients/__init__.py,sha256=ByjAWGrbhFnNz5dX3ED7sT3m8A-qKgWgBH4xsmZROrg,871
9
- pssh/clients/common.py,sha256=fd_jJj8ezDjwr3peN3LonLff5EYSNkMEre0yjlnRjBU,1375
10
- pssh/clients/reader.py,sha256=ljobItde3KnPQkx3K6HPt3E7qFnI1PKYpymzwQeTh2U,2981
11
- pssh/clients/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- pssh/clients/base/parallel.py,sha256=VHimYUiVOWkI5Wt9S5n-EF9PVz32BXxWb3hA_llHcHg,24997
13
- pssh/clients/base/single.py,sha256=y6kwnFUWet1BGo2YLSbtfxYqQWpCvJlzHJtLqs6Vh0c,26065
14
- pssh/clients/native/__init__.py,sha256=dydX5Fae9U9gRBT8koPTM8H9u4t0rIgD1q9XammFYSs,865
15
- pssh/clients/native/parallel.py,sha256=AjOJoLdhyyUKWp4Leda08cXC-SsyT16XK4PExiOl3-c,24119
16
- pssh/clients/native/single.py,sha256=RgmBFyrF5NgCoWb-fKHR7zTKbIFH56onlBsPg5hw7BY,31978
17
- pssh/clients/native/tunnel.py,sha256=-EM2LMhIEWR486owKOnt0pIV4n1by2_ATIhN_DSf0O4,9311
18
- pssh/clients/ssh/__init__.py,sha256=HgYZkVJ5QOgaVFp5snwyLZvQbp0Tbdxj8Gbyic2_Ibo,857
19
- pssh/clients/ssh/parallel.py,sha256=2X47tgPklubHbb7RjUk75IZjidbbOdJq3VstH3KU6KM,11397
20
- pssh/clients/ssh/single.py,sha256=oSolU4_D-38rkT1eRqOl33js0CmR727tn9wj9T5qXmI,13617
21
- parallel_ssh-2.12.0.dist-info/COPYING,sha256=ZA2Q9u5AEkH_YoNNDRsz-DBJ6ZuL_foE7RsKFjXd4-c,18093
22
- parallel_ssh-2.12.0.dist-info/COPYING.LESSER,sha256=AKibDRiqzUEU3s95Ei24e_Nb3a8rxQ44PJyfTCYzkLI,24486
23
- parallel_ssh-2.12.0.dist-info/LICENSE,sha256=m4cqigcLitMpxL04D7G_AAD1ZMdQI-yOHmgD8VNkuek,26461
24
- parallel_ssh-2.12.0.dist-info/METADATA,sha256=KlaVHJISbBkVWpWQ_zdGnOqaBZ1HRdaiFBVASMoZWco,10933
25
- parallel_ssh-2.12.0.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92
26
- parallel_ssh-2.12.0.dist-info/top_level.txt,sha256=s8P6ZHOwt2BYgDc62Cpd2z7i-rebGzIhhnO09pger0U,5
27
- parallel_ssh-2.12.0.dist-info/RECORD,,