Skip to content

Commit 94d8cfe

Browse files
committed
Python 3.4-only update. Bump to 2.9.0
[x] partial migration to pytest for parametrized tests [x] Fix no stdin in exec result (cherry picked from commit 038ba13) Subprocess is not singleton anymore: only lock property is shared. (#96) `SingleLock` metaclass added. `Singleton` saved for historical reasons. All Subprocess tests and SSHAuth tests ported to pytest. (cherry picked from commit f120cae) Signed-off-by: Alexey Stepanov <penguinolog@gmail.com>
1 parent d4c3a47 commit 94d8cfe

23 files changed

+899
-1172
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
.pytest_cache/*
77

88
### Generated code
9-
/exec_helpers/*.c
9+
/exec_helpers/**/*.c
1010

1111
### TortoiseGit template
1212
# Project-level settings

.travis.yml

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,19 @@ language: python
33
os: linux
44
install:
55
- &upgrade_python_toolset pip install --upgrade pip setuptools wheel
6-
- &install_test_deps pip install --upgrade pytest pytest-sugar
6+
- &install_test_deps pip install --upgrade mock pytest pytest-mock pytest-sugar
77
- &install_deps pip install -r CI_REQUIREMENTS.txt
88
- pip install --upgrade pytest-cov coveralls
99

1010
_python:
1111
- &python34
1212
name: "Python 3.4"
1313
python: 3.4
14-
- &python35
15-
name: "Python 3.5"
16-
python: 3.5
17-
- &python36
18-
name: "Python 3.6"
19-
python: 3.6
2014
- &python37
2115
name: "Python 3.7"
2216
python: 3.7
2317
dist: xenial
2418
sudo: true
25-
- &pypy3
26-
name: "PyPy3"
27-
python: pypy3.5
2819

2920
_helpers:
3021
- &install_cython pip install --upgrade Cython
@@ -46,12 +37,12 @@ _helpers:
4637

4738
- &static_analysis
4839
stage: Static analysis
49-
<<: *python37
40+
<<: *python34
5041
after_success: skip
5142

5243
- &code_style_check
5344
stage: Code style check
54-
<<: *python37
45+
<<: *python34
5546
after_success: skip
5647

5748
script:
@@ -62,7 +53,6 @@ after_success:
6253
- coveralls
6354

6455
jobs:
65-
fast_finish: true
6656
include:
6757
- <<: *static_analysis
6858
name: "PyLint"
@@ -80,6 +70,7 @@ jobs:
8070
script:
8171
- bandit -r exec_helpers
8272
- <<: *static_analysis
73+
<<: *python37
8374
name: "MyPy"
8475
install:
8576
- *upgrade_python_toolset
@@ -103,6 +94,7 @@ jobs:
10394
script:
10495
- pydocstyle exec_helpers
10596
- <<: *code_style_check
97+
<<: *python37
10698
name: "Black formatting"
10799
install:
108100
- *upgrade_python_toolset
@@ -112,29 +104,15 @@ jobs:
112104

113105
- stage: test
114106
<<: *python34
115-
- stage: test
116-
<<: *python35
117-
- stage: test
118-
<<: *python36
119-
- stage: test
120-
<<: *python37
121-
- stage: test
122-
<<: *pypy3
123107

124108
- <<: *test_cythonized
125109
<<: *python34
126-
- <<: *test_cythonized
127-
<<: *python35
128-
- <<: *test_cythonized
129-
<<: *python36
130-
- <<: *test_cythonized
131-
<<: *python37
132110

133111
- stage: deploy
134112
# This prevents job from appearing in test plan unless commit is tagged:
135113
if: tag IS present
136114
# Run on pypy to build not cythonized wheel
137-
<<: *pypy3
115+
<<: *python34
138116
name: Build universal and cythonized bdist_wheel. Deploy bdist and sdist.
139117
services:
140118
- docker
@@ -143,7 +121,7 @@ jobs:
143121
script:
144122
- ./tools/run_docker.sh "exec_helpers"
145123
before_deploy:
146-
- pip install -r build_requirements.txt
124+
- pip install -r requirements.txt
147125
- *build_package
148126
deploy:
149127
- provider: pypi

README.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,12 @@ Pros:
4444

4545
::
4646

47-
Python 3.4
4847
Python 3.5
4948
Python 3.6
5049
Python 3.7
5150
PyPy3 3.5+
5251

53-
.. note:: For Python 2.7 and PyPy please use versions 1.x.x
52+
.. note:: For Python 2.7 and PyPy please use versions 1.x.x. For python 3.5+ use versions 3.0+
5453

5554
This package includes:
5655

build_requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
Cython; platform_python_implementation == "CPython"
2-
wheel
2+
wheel==0.31.1
33
-r CI_REQUIREMENTS.txt
44
-r requirements.txt

doc/source/ExecResult.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ API: ExecResult
66
.. py:module:: exec_helpers
77
.. py:currentmodule:: exec_helpers
88
9-
.. py:class:: ExecResult(object)
9+
.. py:class:: ExecResult()
1010
1111
Command execution result.
1212

@@ -27,6 +27,7 @@ API: ExecResult
2727
2828
``threading.RLock``
2929
Lock object for thread-safe operation.
30+
3031
.. versionadded:: 2.2.0
3132

3233
.. py:attribute:: stderr_lock

doc/source/SSHClient.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ API: SSHClient and SSHAuth.
325325
:rtype: ``bool``
326326

327327

328-
.. py:class:: SSHAuth(object)
328+
.. py:class:: SSHAuth()
329329
330330
SSH credentials object.
331331

doc/source/Subprocess.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ API: Subprocess
1616
:type log_mask_re: typing.Optional[str]
1717

1818
.. versionchanged:: 1.2.0 log_mask_re regex rule for masking cmd
19+
.. versionchanged:: 2.9.0 Not singleton anymore. Only lock is shared between all instances.
1920

2021
.. py:attribute:: log_mask_re
2122

exec_helpers/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@
2525
ExecHelperTimeoutError,
2626
)
2727

28-
from .exec_result import ExecResult
2928
from .api import ExecHelper
29+
from .exec_result import ExecResult
3030
from .ssh_auth import SSHAuth
31-
from ._ssh_client_base import SshExecuteAsyncResult
3231
from .ssh_client import SSHClient
32+
from ._ssh_client_base import SshExecuteAsyncResult
3333
from .subprocess_runner import Subprocess, SubprocessExecuteAsyncResult # nosec # Expected
3434

3535
__all__ = (
@@ -49,7 +49,7 @@
4949
"ExecResult",
5050
)
5151

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

exec_helpers/_ssh_client_base.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -614,12 +614,10 @@ def execute_async(
614614

615615
return SshExecuteAsyncResult(chan, _stdin, stderr, stdout)
616616

617-
def _exec_command(
617+
def _exec_command( # type: ignore
618618
self,
619619
command: str,
620-
interface: paramiko.channel.Channel,
621-
stdout: typing.Optional[paramiko.ChannelFile],
622-
stderr: typing.Optional[paramiko.ChannelFile],
620+
async_result: SshExecuteAsyncResult,
623621
timeout: typing.Union[int, float, None],
624622
verbose: bool = False,
625623
log_mask_re: typing.Optional[str] = None,
@@ -629,12 +627,8 @@ def _exec_command(
629627
630628
:param command: executed command (for logs)
631629
:type command: str
632-
:param interface: interface to control execution
633-
:type interface: paramiko.channel.Channel
634-
:param stdout: source for STDOUT read
635-
:type stdout: typing.Optional[paramiko.ChannelFile]
636-
:param stderr: source for STDERR read
637-
:type stderr: typing.Optional[paramiko.ChannelFile]
630+
:param async_result: execute_async result
631+
:type async_result: SshExecuteAsyncResult
638632
:param timeout: timeout before stop execution with TimeoutError
639633
:type timeout: typing.Union[int, float, None]
640634
:param verbose: produce log.info records for STDOUT/STDERR
@@ -653,10 +647,10 @@ def _exec_command(
653647

654648
def poll_streams() -> None:
655649
"""Poll FIFO buffers if data available."""
656-
if stdout and interface.recv_ready():
657-
result.read_stdout(src=stdout, log=self.logger, verbose=verbose)
658-
if stderr and interface.recv_stderr_ready():
659-
result.read_stderr(src=stderr, log=self.logger, verbose=verbose)
650+
if async_result.stdout and async_result.interface.recv_ready():
651+
result.read_stdout(src=async_result.stdout, log=self.logger, verbose=verbose)
652+
if async_result.stderr and async_result.interface.recv_stderr_ready():
653+
result.read_stderr(src=async_result.stderr, log=self.logger, verbose=verbose)
660654

661655
@threaded.threadpooled
662656
def poll_pipes(stop: threading.Event) -> None:
@@ -666,21 +660,21 @@ def poll_pipes(stop: threading.Event) -> None:
666660
"""
667661
while not stop.is_set():
668662
time.sleep(0.1)
669-
if stdout or stderr:
663+
if async_result.stdout or async_result.stderr:
670664
poll_streams()
671665

672-
if interface.status_event.is_set():
673-
result.read_stdout(src=stdout, log=self.logger, verbose=verbose)
674-
result.read_stderr(src=stderr, log=self.logger, verbose=verbose)
675-
result.exit_code = interface.exit_status
666+
if async_result.interface.status_event.is_set():
667+
result.read_stdout(src=async_result.stdout, log=self.logger, verbose=verbose)
668+
result.read_stderr(src=async_result.stderr, log=self.logger, verbose=verbose)
669+
result.exit_code = async_result.interface.exit_status
676670

677671
stop.set()
678672

679673
# channel.status_event.wait(timeout)
680674
cmd_for_log = self._mask_command(cmd=command, log_mask_re=log_mask_re)
681675

682676
# Store command with hidden data
683-
result = exec_result.ExecResult(cmd=cmd_for_log)
677+
result = exec_result.ExecResult(cmd=cmd_for_log, stdin=kwargs.get("stdin"))
684678

685679
stop_event = threading.Event()
686680

@@ -693,11 +687,11 @@ def poll_pipes(stop: threading.Event) -> None:
693687

694688
# Process closed?
695689
if stop_event.is_set():
696-
interface.close()
690+
async_result.interface.close()
697691
return result
698692

699693
stop_event.set()
700-
interface.close()
694+
async_result.interface.close()
701695
future.cancel()
702696

703697
wait_err_msg = _log_templates.CMD_WAIT_ERROR.format(result=result, timeout=timeout)
@@ -774,9 +768,15 @@ def execute_through_host(
774768

775769
channel.exec_command(command) # nosec # Sanitize on caller side
776770

771+
async_result = SshExecuteAsyncResult(interface=channel, stdin=None, stdout=stdout, stderr=stderr)
772+
777773
# noinspection PyDictCreation
778774
result = self._exec_command(
779-
command, channel, stdout, stderr, timeout, verbose=verbose, log_mask_re=kwargs.get("log_mask_re", None)
775+
command,
776+
async_result=async_result,
777+
timeout=timeout,
778+
verbose=verbose,
779+
log_mask_re=kwargs.get("log_mask_re", None),
780780
)
781781

782782
intermediate_channel.close()
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright 2018 Alexey Stepanov aka penguinolog.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
"""Python subprocess shared code."""
16+
17+
__all__ = ("kill_proc_tree", "subprocess_kw")
18+
19+
import platform
20+
21+
# pylint: disable=unused-import
22+
import typing # noqa: F401
23+
24+
# pylint: enable=unused-import
25+
26+
import psutil # type: ignore
27+
28+
29+
# Adopt from:
30+
# https://stackoverflow.com/questions/1230669/subprocess-deleting-child-processes-in-windows
31+
def kill_proc_tree(pid: int, including_parent: bool = True) -> None: # pragma: no cover
32+
"""Kill process tree.
33+
34+
:param pid: PID of parent process to kill
35+
:type pid: int
36+
:param including_parent: kill also parent process
37+
:type including_parent: bool
38+
"""
39+
parent = psutil.Process(pid)
40+
children = parent.children(recursive=True)
41+
for child in children: # type: psutil.Process
42+
child.kill()
43+
_, alive = psutil.wait_procs(children, timeout=5)
44+
for proc in alive: # type: psutil.Process
45+
proc.kill() # 2nd shot
46+
if including_parent:
47+
parent.kill()
48+
parent.wait(5)
49+
50+
51+
# Subprocess extra arguments.
52+
# Flags from:
53+
# https://stackoverflow.com/questions/13243807/popen-waiting-for-child-process-even-when-the-immediate-child-has-terminated
54+
subprocess_kw = {} # type: typing.Dict[str, typing.Any]
55+
if "Windows" == platform.system(): # pragma: no cover
56+
subprocess_kw["creationflags"] = 0x00000200
57+
else: # pragma: no cover
58+
subprocess_kw["start_new_session"] = True

0 commit comments

Comments
 (0)