Skip to content

Commit 43da048

Browse files
committed
Properly collect coverage on azure
(cherry picked from commit d1c92f8) better coverage: remove useless lines from abstract methods, branches (cherry picked from commit a47b657) use libyaml pyyaml, when it possible (cherry picked from commit 66a8e5b) fix bandit checker (cherry picked from commit 32022ad) rename ssh connection logger to separate from logger path (cherry picked from commit c6f9705) Properly work with isfile/isdir/islink * add islink * add symlink (`os.symlink` mimic) * add chmod (`os.chmod` mimic) (cherry picked from commit 5e9bea3) Fix pylint warnings: install isort with toml support
1 parent b26b1c6 commit 43da048

File tree

8 files changed

+126
-17
lines changed

8 files changed

+126
-17
lines changed

.coveragerc

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
[run]
22
source =
3-
exec_helpers
3+
exec_helpers
44
omit =
5-
test/*
6-
5+
test/*
6+
branch = True
77
[report]
88
exclude_lines =
9-
# Have to re-enable the standard pragma
10-
pragma: no cover
9+
# Have to re-enable the standard pragma
10+
pragma: no cover
11+
12+
# Don't complain about missing debug-only code:
13+
def __repr__
1114

12-
# Don't complain about missing debug-only code:
13-
def __repr__
15+
# Don't complain if tests don't hit defensive assertion code:
16+
raise NotImplementedError
1417

15-
# Don't complain if non-runnable code isn't run:
16-
if __name__ == .__main__.:
18+
# Don't complain if non-runnable code isn't run:
19+
if __name__ == .__main__.:

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
install:
4848
- *upgrade_python_toolset
4949
- *install_deps
50-
- pip install --upgrade "pylint < 2.0"
50+
- pip install --upgrade "pylint < 2.0" isort[pyproject,requirements]
5151
script:
5252
- pylint exec_helpers
5353
- <<: *static_analysis

doc/source/SSHClient.rst

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,31 +271,39 @@ API: SSHClient and SSHAuth.
271271
272272
Open file on remote using SFTP session.
273273

274+
:param path: filesystem object path
274275
:type path: ``str``
276+
:param mode: open file mode ('t' is not supported)
275277
:type mode: ``str``
278+
:return: file.open() stream
279+
:rtype: ``paramiko.SFTPFile``
276280

277281
.. py:method:: exists(path)
278282
279283
Check for file existence using SFTP session.
280284

285+
:param path: filesystem object path
281286
:type path: ``str``
287+
:return: path is valid (object exists)
282288
:rtype: ``bool``
283289

284290
.. py:method:: stat(path)
285291
286292
Get stat info for path with following symlinks.
287293

294+
:param path: filesystem object path
288295
:type path: ``str``
296+
:return: stat like information for remote path
289297
:rtype: ``paramiko.sftp_attr.SFTPAttributes``
290298

291299
.. py:method:: utime(path, times=None):
292300
293301
Set atime, mtime.
294302

295303
:param path: filesystem object path
296-
:type path: str
304+
:type path: ``str``
297305
:param times: (atime, mtime)
298-
:type times: typing.Optional[typing.Tuple[int, int]]
306+
:type times: ``typing.Optional[typing.Tuple[int, int]]``
299307
:rtype: None
300308

301309
.. versionadded:: 1.0.0
@@ -304,16 +312,47 @@ API: SSHClient and SSHAuth.
304312
305313
Check, that path is file using SFTP session.
306314

315+
:param path: remote path to validate
307316
:type path: ``str``
317+
:return: path is file
308318
:rtype: ``bool``
309319

310320
.. py:method:: isdir(path)
311321
312322
Check, that path is directory using SFTP session.
313323

324+
:param path: remote path to validate
325+
:type path: ``str``
326+
:return: path is directory
327+
:rtype: ``bool``
328+
329+
.. py:method:: islink(path)
330+
331+
Check, that path is symlink using SFTP session.
332+
333+
:param path: remote path to validate
314334
:type path: ``str``
335+
:return: path is symlink
315336
:rtype: ``bool``
316337

338+
.. py:method:: symlink(source, dest)
339+
340+
Produce symbolic link like `os.symlink`.
341+
342+
:param source: source path
343+
:type source: ``str``
344+
:param dest: source path
345+
:type dest: ``str``
346+
347+
.. py:method:: chmod(path, mode)
348+
349+
Change the mode (permissions) of a file like `os.chmod`.
350+
351+
:param path: filesystem object path
352+
:type path: ``str``
353+
:param mode: new permissions
354+
:type mode: ``int``
355+
317356
**Non standard methods:**
318357

319358
.. py:method:: mkdir(path)

exec_helpers/_ssh_client_base.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ def __init__(
290290
.. note:: auth has priority over username/password/private_keys
291291
"""
292292
super(SSHClientBase, self).__init__(
293-
logger=logging.getLogger(self.__class__.__name__).getChild("{host}:{port}".format(host=host, port=port))
293+
logger=logging.getLogger(self.__class__.__name__).getChild("({host}:{port})".format(host=host, port=port))
294294
)
295295

296296
self.__hostname = host
@@ -952,7 +952,7 @@ def isfile(self, path): # type: (typing.Union[str, typing.Text]) -> bool
952952
"""
953953
try:
954954
attrs = self._sftp.lstat(path)
955-
return attrs.st_mode & stat.S_IFREG != 0 # type: ignore
955+
return stat.S_ISREG(attrs.st_mode)
956956
except IOError:
957957
return False
958958

@@ -966,6 +966,40 @@ def isdir(self, path): # type: (typing.Union[str, typing.Text]) -> bool
966966
"""
967967
try:
968968
attrs = self._sftp.lstat(path)
969-
return attrs.st_mode & stat.S_IFDIR != 0 # type: ignore
969+
return stat.S_ISDIR(attrs.st_mode)
970970
except IOError:
971971
return False
972+
973+
def islink(self, path): # type: (typing.Union[str, typing.Text]) -> bool
974+
"""Check, that path is symlink using SFTP session.
975+
976+
:param path: remote path to validate
977+
:type path: str
978+
:return: path is symlink
979+
:rtype: bool
980+
"""
981+
try:
982+
attrs = self._sftp.lstat(path)
983+
return stat.S_ISLNK(attrs.st_mode)
984+
except IOError:
985+
return False
986+
987+
def symlink(self, source, dest): # type: (typing.Union[str, typing.Text], typing.Union[str, typing.Text]) -> None
988+
"""Produce symbolic link like `os.symlink`.
989+
990+
:param source: source path
991+
:type source: str
992+
:param dest: source path
993+
:type dest: str
994+
"""
995+
self._sftp.symlink(source, dest) # pragma: no cover
996+
997+
def chmod(self, path, mode): # type: (typing.Union[str, typing.Text],int) -> None
998+
"""Change the mode (permissions) of a file like `os.chmod`.
999+
1000+
:param path: filesystem object path
1001+
:type path: str
1002+
:param mode: new permissions
1003+
:type mode: int
1004+
"""
1005+
self._sftp.chmod(path, mode) # pragma: no cover

exec_helpers/api.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,6 @@ def execute_async(
282282
.. versionchanged:: 1.4.0 Use typed NamedTuple as result
283283
.. versionchanged:: 1.12.0 support chroot
284284
"""
285-
raise NotImplementedError # pragma: no cover
286285

287286
@abc.abstractmethod
288287
def _exec_command(
@@ -315,7 +314,6 @@ def _exec_command(
315314
316315
.. versionchanged:: 1.2.0 log_mask_re regex rule for masking cmd
317316
"""
318-
raise NotImplementedError # pragma: no cover
319317

320318
def execute(
321319
self,

exec_helpers/exec_result.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,8 @@ def __deserialize(self, fmt): # type: (typing.Text) -> typing.Any
486486
if fmt == "json":
487487
return json.loads(self.stdout_str, encoding="utf-8")
488488
if fmt == "yaml":
489+
if yaml.__with_libyaml__:
490+
return yaml.load(self.stdout_str, Loader=yaml.CSafeLoader) # nosec # Safe
489491
return yaml.safe_load(self.stdout_str)
490492
except Exception:
491493
tmpl = " stdout is not valid {fmt}:\n" "{{stdout!r}}\n".format(fmt=fmt)

test/test_sftp.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,38 @@ def __init__(self, mode):
159159
self.assertFalse(result)
160160
lstat.assert_called_once_with(dst)
161161

162+
def test_islink(self, client, *args):
163+
class Attrs:
164+
def __init__(self, mode):
165+
self.st_mode = mode
166+
167+
ssh, _sftp = self.prepare_sftp_file_tests(client)
168+
lstat = mock.Mock()
169+
_sftp.attach_mock(lstat, "lstat")
170+
lstat.return_value = Attrs(stat.S_IFLNK)
171+
dst = "/etc/passwd"
172+
173+
# noinspection PyTypeChecker
174+
result = ssh.islink(dst)
175+
self.assertTrue(result)
176+
lstat.assert_called_once_with(dst)
177+
178+
# Negative scenario
179+
lstat.reset_mock()
180+
lstat.return_value = Attrs(stat.S_IFREG)
181+
182+
# noinspection PyTypeChecker
183+
result = ssh.islink(dst)
184+
self.assertFalse(result)
185+
lstat.assert_called_once_with(dst)
186+
187+
lstat.reset_mock()
188+
lstat.side_effect = IOError
189+
# noinspection PyTypeChecker
190+
result = ssh.islink(dst)
191+
self.assertFalse(result)
192+
lstat.assert_called_once_with(dst)
193+
162194
@mock.patch("exec_helpers.ssh_client.SSHClient.exists")
163195
@mock.patch("exec_helpers.ssh_client.SSHClient.execute")
164196
def test_mkdir(self, execute, exists, *args):

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ commands = pip install ./ -vvv -U
5656
usedevelop = False
5757
deps =
5858
pylint<2
59+
isort[pyproject,requirements]
5960
-r{toxinidir}/CI_REQUIREMENTS.txt
6061
commands = pylint exec_helpers
6162

0 commit comments

Comments
 (0)