Skip to content

Commit d4c3a47

Browse files
authored
Rework locks model on exec_result. (#94)
Deprecate `ExecResult().lock` property. Bump to 2.2.0 Expect last release before work start on asyncio support and split py34
1 parent c31300c commit d4c3a47

File tree

6 files changed

+103
-30
lines changed

6 files changed

+103
-30
lines changed

doc/source/ExecResult.rst

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.. ExecResult
22
33
API: ExecResult
4-
===========================
4+
===============
55

66
.. py:module:: exec_helpers
77
.. py:currentmodule:: exec_helpers
@@ -23,10 +23,18 @@ API: ExecResult
2323
:param exit_code: Exit code. If integer - try to convert to BASH enum.
2424
:type exit_code: typing.Union[int, ExitCodes]
2525

26-
.. py:attribute:: lock
26+
.. py:attribute:: stdout_lock
2727
2828
``threading.RLock``
2929
Lock object for thread-safe operation.
30+
.. versionadded:: 2.2.0
31+
32+
.. py:attribute:: stderr_lock
33+
34+
``threading.RLock``
35+
Lock object for thread-safe operation.
36+
37+
.. versionadded:: 2.2.0
3038

3139
.. py:attribute:: timestamp
3240

exec_helpers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"ExecResult",
5050
)
5151

52-
__version__ = "2.1.3"
52+
__version__ = "2.2.0"
5353
__author__ = "Alexey Stepanov"
5454
__author_email__ = "penguinolog@gmail.com"
5555
__maintainers__ = {

exec_helpers/exec_result.py

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import logging
2222
import threading
2323
import typing
24+
import warnings
2425

2526
import yaml
2627

@@ -32,6 +33,38 @@
3233
logger = logging.getLogger(__name__)
3334

3435

36+
class MultipleRLock: # pragma: no cover
37+
"""Handle multiple locks together."""
38+
39+
__slots__ = ("__locks",)
40+
41+
def __init__(self, *locks: threading.RLock) -> None:
42+
"""Multiple locks handler.
43+
44+
.. note:: temporary class during transition to separate locks.
45+
"""
46+
self.__locks = locks
47+
48+
def acquire(self, blocking: bool = True) -> None:
49+
"""Acquire locks.
50+
51+
.. note:: Timeout is not supported.
52+
"""
53+
for lock in self.__locks:
54+
lock.acquire(blocking=blocking)
55+
56+
__enter__ = acquire
57+
58+
def release(self) -> None:
59+
"""Release locks."""
60+
for lock in self.__locks:
61+
lock.release()
62+
63+
def __exit__(self, exc_type: typing.Any, exc_val: typing.Any, exc_tb: typing.Any) -> None:
64+
"""Context manager usage."""
65+
self.release()
66+
67+
3568
class ExecResult:
3669
"""Execution result."""
3770

@@ -47,6 +80,8 @@ class ExecResult:
4780
"__stdout_brief",
4881
"__stderr_brief",
4982
"__lock",
83+
"__stdout_lock",
84+
"__stderr_lock",
5085
]
5186

5287
def __init__(
@@ -70,7 +105,9 @@ def __init__(
70105
:param exit_code: Exit code. If integer - try to convert to BASH enum.
71106
:type exit_code: typing.Union[int, proc_enums.ExitCodes]
72107
"""
73-
self.__lock = threading.RLock()
108+
self.__stdout_lock = threading.RLock()
109+
self.__stderr_lock = threading.RLock()
110+
self.__lock = MultipleRLock(self.stdout_lock, self.stderr_lock)
74111

75112
self.__cmd = cmd
76113
if isinstance(stdin, bytes):
@@ -100,13 +137,41 @@ def __init__(
100137
self.__stderr_brief = None
101138

102139
@property
103-
def lock(self) -> threading.RLock:
140+
def lock(self) -> MultipleRLock: # pragma: no cover
141+
"""Composite lock for stdout and stderr.
142+
143+
:return: Composite from stdout_lock and stderr_lock
144+
:rtype: MultipleRLock
145+
146+
.. versionchanged:: 2.2.0 Deprecated
147+
"""
148+
warnings.warn(
149+
"ExecResult.lock is deprecated and will be removed at version 3. Use stdout_lock and stderr_lock instead.",
150+
DeprecationWarning,
151+
)
152+
return self.__lock
153+
154+
@property
155+
def stdout_lock(self) -> threading.RLock:
104156
"""Lock object for thread-safe operation.
105157
106-
:return: internal lock
158+
:return: internal lock for stdout
107159
:rtype: threading.RLock
160+
161+
.. versionadded:: 2.2.0
108162
"""
109-
return self.__lock
163+
return self.__stdout_lock
164+
165+
@property
166+
def stderr_lock(self) -> threading.RLock:
167+
"""Lock object for thread-safe operation.
168+
169+
:return: internal lock for stderr
170+
:rtype: threading.RLock
171+
172+
.. versionadded:: 2.2.0
173+
"""
174+
return self.__stderr_lock
110175

111176
@property
112177
def timestamp(self) -> typing.Optional[datetime.datetime]:
@@ -226,7 +291,7 @@ def read_stdout(
226291
if self.timestamp:
227292
raise RuntimeError("Final exit code received.")
228293

229-
with self.lock:
294+
with self.stdout_lock:
230295
self.__stdout_str = self.__stdout_brief = None
231296
self.__stdout += tuple(self.__poll_stream(src, log, verbose))
232297

@@ -253,7 +318,7 @@ def read_stderr(
253318
if self.timestamp:
254319
raise RuntimeError("Final exit code received.")
255320

256-
with self.lock:
321+
with self.stderr_lock:
257322
self.__stderr_str = self.__stderr_brief = None
258323
self.__stderr += tuple(self.__poll_stream(src, log, verbose))
259324

@@ -265,7 +330,7 @@ def stdout_bin(self) -> bytearray:
265330
and for debug purposes we can use this as data source.
266331
:rtype: bytearray
267332
"""
268-
with self.lock:
333+
with self.stdout_lock:
269334
return self._get_bytearray_from_array(self.stdout)
270335

271336
@property
@@ -274,7 +339,7 @@ def stderr_bin(self) -> bytearray:
274339
275340
:rtype: bytearray
276341
"""
277-
with self.lock:
342+
with self.stderr_lock:
278343
return self._get_bytearray_from_array(self.stderr)
279344

280345
@property
@@ -283,7 +348,7 @@ def stdout_str(self) -> str:
283348
284349
:rtype: str
285350
"""
286-
with self.lock:
351+
with self.stdout_lock:
287352
if self.__stdout_str is None:
288353
self.__stdout_str = self._get_str_from_bin(self.stdout_bin) # type: ignore
289354
return self.__stdout_str # type: ignore
@@ -294,7 +359,7 @@ def stderr_str(self) -> str:
294359
295360
:rtype: str
296361
"""
297-
with self.lock:
362+
with self.stderr_lock:
298363
if self.__stderr_str is None:
299364
self.__stderr_str = self._get_str_from_bin(self.stderr_bin) # type: ignore
300365
return self.__stderr_str # type: ignore
@@ -305,7 +370,7 @@ def stdout_brief(self) -> str:
305370
306371
:rtype: str
307372
"""
308-
with self.lock:
373+
with self.stdout_lock:
309374
if self.__stdout_brief is None:
310375
self.__stdout_brief = self._get_brief(self.stdout) # type: ignore
311376
return self.__stdout_brief # type: ignore
@@ -316,7 +381,7 @@ def stderr_brief(self) -> str:
316381
317382
:rtype: str
318383
"""
319-
with self.lock:
384+
with self.stderr_lock:
320385
if self.__stderr_brief is None:
321386
self.__stderr_brief = self._get_brief(self.stderr) # type: ignore
322387
return self.__stderr_brief # type: ignore
@@ -345,7 +410,7 @@ def exit_code(self, new_val: typing.Union[int, proc_enums.ExitCodes]) -> None:
345410
raise RuntimeError("Exit code is already received.")
346411
if not isinstance(new_val, int):
347412
raise TypeError("Exit code is strictly int, received: {code!r}".format(code=new_val))
348-
with self.lock:
413+
with self.stdout_lock, self.stderr_lock:
349414
self.__exit_code = proc_enums.exit_code_to_enum(new_val)
350415
if self.__exit_code != proc_enums.ExitCodes.EX_INVALID:
351416
self.__timestamp = datetime.datetime.utcnow() # type: ignore
@@ -383,7 +448,7 @@ def stdout_json(self) -> typing.Any:
383448
384449
:rtype: typing.Any
385450
"""
386-
with self.lock:
451+
with self.stdout_lock:
387452
return self.__deserialize(fmt="json")
388453

389454
@property
@@ -392,7 +457,7 @@ def stdout_yaml(self) -> typing.Any:
392457
393458
:rtype: typing.Any
394459
"""
395-
with self.lock:
460+
with self.stdout_lock:
396461
return self.__deserialize(fmt="yaml")
397462

398463
def __dir__(self) -> typing.List[str]:

exec_helpers/ssh_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class SSHClient(SSHClientBase):
3636
@staticmethod
3737
def _path_esc(path: str) -> str:
3838
"""Escape space character in the path."""
39-
return path.replace(" ", "\ ")
39+
return path.replace(" ", r"\ ")
4040

4141
def mkdir(self, path: str) -> None:
4242
"""Run 'mkdir -p path' on remote.

test/test_exec_result.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def test_json(self):
153153
@mock.patch("exec_helpers.exec_result.logger", autospec=True)
154154
def test_wrong_result(self, logger):
155155
"""Test logging exception if stdout if not a correct json"""
156-
cmd = "ls -la | awk '{print $1\}'"
156+
cmd = r"ls -la | awk '{print $1\}'"
157157
result = exec_helpers.ExecResult(cmd=cmd)
158158
with self.assertRaises(exec_helpers.ExecHelperError):
159159
# noinspection PyStatementEffect

test/test_ssh_client.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,7 +1249,7 @@ def prepare_sftp_file_tests(client):
12491249
)
12501250
return ssh, _sftp
12511251

1252-
def test_exists(self, client, policy, logger):
1252+
def test_exists(self, client, *args):
12531253
ssh, _sftp = self.prepare_sftp_file_tests(client)
12541254
lstat = mock.Mock()
12551255
_sftp.attach_mock(lstat, "lstat")
@@ -1269,7 +1269,7 @@ def test_exists(self, client, policy, logger):
12691269
self.assertFalse(result)
12701270
lstat.assert_called_once_with(dst)
12711271

1272-
def test_stat(self, client, policy, logger):
1272+
def test_stat(self, client, *args):
12731273
ssh, _sftp = self.prepare_sftp_file_tests(client)
12741274
stat = mock.Mock()
12751275
_sftp.attach_mock(stat, "stat")
@@ -1285,7 +1285,7 @@ def test_stat(self, client, policy, logger):
12851285
self.assertEqual(result.st_uid, 0)
12861286
self.assertEqual(result.st_gid, 0)
12871287

1288-
def test_isfile(self, client, policy, logger):
1288+
def test_isfile(self, client, *args):
12891289
class Attrs(object):
12901290
def __init__(self, mode):
12911291
self.st_mode = mode
@@ -1318,7 +1318,7 @@ def __init__(self, mode):
13181318
self.assertFalse(result)
13191319
lstat.assert_called_once_with(dst)
13201320

1321-
def test_isdir(self, client, policy, logger):
1321+
def test_isdir(self, client, *args):
13221322
class Attrs(object):
13231323
def __init__(self, mode):
13241324
self.st_mode = mode
@@ -1352,11 +1352,11 @@ def __init__(self, mode):
13521352

13531353
@mock.patch("exec_helpers.ssh_client.SSHClient.exists")
13541354
@mock.patch("exec_helpers.ssh_client.SSHClient.execute")
1355-
def test_mkdir(self, execute, exists, client, policy, logger):
1355+
def test_mkdir(self, execute, exists, *args):
13561356
exists.side_effect = [False, True]
13571357

13581358
dst = "~/tst dir"
1359-
escaped_dst = "~/tst\ dir"
1359+
escaped_dst = r"~/tst\ dir"
13601360

13611361
# noinspection PyTypeChecker
13621362
ssh = exec_helpers.SSHClient(
@@ -1379,7 +1379,7 @@ def test_mkdir(self, execute, exists, client, policy, logger):
13791379
execute.assert_not_called()
13801380

13811381
@mock.patch("exec_helpers.ssh_client.SSHClient.execute")
1382-
def test_rm_rf(self, execute, client, policy, logger):
1382+
def test_rm_rf(self, execute, *args):
13831383
dst = "~/tst"
13841384

13851385
# noinspection PyTypeChecker
@@ -1392,7 +1392,7 @@ def test_rm_rf(self, execute, client, policy, logger):
13921392
ssh.rm_rf(dst)
13931393
execute.assert_called_once_with("rm -rf {}".format(dst))
13941394

1395-
def test_open(self, client, policy, logger):
1395+
def test_open(self, client, *args):
13961396
ssh, _sftp = self.prepare_sftp_file_tests(client)
13971397
fopen = mock.Mock(return_value=True)
13981398
_sftp.attach_mock(fopen, "open")
@@ -1437,7 +1437,7 @@ def test_download(self, isdir, remote_isdir, exists, remote_exists, logger, clie
14371437

14381438
@mock.patch("exec_helpers.ssh_client.SSHClient.isdir")
14391439
@mock.patch("os.path.isdir", autospec=True)
1440-
def test_upload_file(self, isdir, remote_isdir, client, policy, logger):
1440+
def test_upload_file(self, isdir, remote_isdir, client, *args):
14411441
ssh, _sftp = self.prepare_sftp_file_tests(client)
14421442
isdir.return_value = False
14431443
remote_isdir.return_value = False
@@ -1455,7 +1455,7 @@ def test_upload_file(self, isdir, remote_isdir, client, policy, logger):
14551455
@mock.patch("os.walk")
14561456
@mock.patch("exec_helpers.ssh_client.SSHClient.isdir")
14571457
@mock.patch("os.path.isdir", autospec=True)
1458-
def test_upload_dir(self, isdir, remote_isdir, walk, mkdir, exists, client, policy, logger):
1458+
def test_upload_dir(self, isdir, remote_isdir, walk, mkdir, exists, client, *args):
14591459
ssh, _sftp = self.prepare_sftp_file_tests(client)
14601460
isdir.return_value = True
14611461
remote_isdir.return_value = True

0 commit comments

Comments
 (0)