Skip to content

Commit 96442cf

Browse files
committed
Allow pathlib.Path as path for chroot
Signed-off-by: Aleksei Stepanov <penguinolog@gmail.com>
1 parent 5ba5553 commit 96442cf

File tree

5 files changed

+41
-11
lines changed

5 files changed

+41
-11
lines changed

doc/source/SSHClient.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ API: SSHClient and SSHAuth.
103103
Context manager for changing chroot rules.
104104

105105
:param path: chroot path or none for working without chroot.
106-
:type path: typing.Optional[str]
106+
:type path: typing.Optional[typing.Union[str, pathlib.Path]]
107107
:return: context manager with selected chroot state inside
108108
:rtype: typing.ContextManager
109109

doc/source/Subprocess.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ API: Subprocess
5050
Context manager for changing chroot rules.
5151

5252
:param path: chroot path or none for working without chroot.
53-
:type path: typing.Optional[str]
53+
:type path: typing.Optional[typing.Union[str, pathlib.Path]]
5454
:return: context manager with selected chroot state inside
5555
:rtype: typing.ContextManager
5656

exec_helpers/api.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import abc
2626
import datetime
2727
import logging
28+
import pathlib
2829
import re
2930
import shlex
3031
import threading
@@ -58,17 +59,23 @@ class _ChRootContext:
5859

5960
__slots__ = ("_conn", "_chroot_status", "_path")
6061

61-
def __init__(self, conn: "ExecHelper", path: typing.Optional[str] = None) -> None:
62+
def __init__(self, conn: "ExecHelper", path: typing.Optional[typing.Union[str, pathlib.Path]] = None) -> None:
6263
"""Context manager for call commands with sudo.
6364
6465
:param conn: connection instance
6566
:type conn: ExecHelper
6667
:param path: chroot path or None for no chroot
67-
:type path: typing.Optional[str]
68+
:type path: typing.Optional[typing.Union[str, pathlib.Path]]
69+
:raises TypeError: incorrect type of path variable
6870
"""
6971
self._conn: "ExecHelper" = conn
7072
self._chroot_status: typing.Optional[str] = conn._chroot_path
71-
self._path: typing.Optional[str] = path
73+
if path is None or isinstance(path, str):
74+
self._path: typing.Optional[str] = path
75+
elif isinstance(path, pathlib.Path):
76+
self._path = path.as_posix() # get absolute path
77+
else:
78+
raise TypeError(f"path={path!r} is not instance of Optional[Union[str, pathlib.Path]]")
7279

7380
def __enter__(self) -> None:
7481
self._conn.__enter__()
@@ -146,11 +153,11 @@ def _chroot_path(self) -> None:
146153
"""
147154
self.__chroot_path = None
148155

149-
def chroot(self, path: typing.Union[str, None]) -> "typing.ContextManager[None]":
156+
def chroot(self, path: typing.Union[str, pathlib.Path, None]) -> "typing.ContextManager[None]":
150157
"""Context manager for changing chroot rules.
151158
152159
:param path: chroot path or none for working without chroot.
153-
:type path: typing.Optional[str]
160+
:type path: typing.Optional[typing.Union[str, pathlib.Path]]
154161
:return: context manager with selected chroot state inside
155162
:rtype: typing.ContextManager
156163
@@ -233,6 +240,7 @@ def _prepare_command(self, cmd: str, chroot_path: typing.Optional[str] = None) -
233240
return f'chroot {target_path} sh -c {shlex.quote(f"eval {quoted_command}")}'
234241
return cmd
235242

243+
# noinspection PyIncorrectDocstring
236244
def execute_async( # pylint: disable=missing-param-doc,differing-param-doc,differing-type-doc
237245
self, *args: typing.Any, **kwargs: typing.Any
238246
) -> ExecuteAsyncResult:

exec_helpers/async_api/api.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import abc
2424
import asyncio
2525
import logging
26+
import pathlib
2627
import typing
2728

2829
# Exec-Helpers Implementation
@@ -37,13 +38,13 @@
3738
class _ChRootContext(api._ChRootContext): # pylint: disable=protected-access
3839
"""Async extension for chroot."""
3940

40-
def __init__(self, conn: "ExecHelper", path: typing.Optional[str] = None) -> None:
41+
def __init__(self, conn: "ExecHelper", path: typing.Optional[typing.Union[str, pathlib.Path]] = None) -> None:
4142
"""Context manager for call commands with sudo.
4243
4344
:param conn: connection instance
4445
:type conn: ExecHelper
4546
:param path: chroot path or None for no chroot
46-
:type path: typing.Optional[str]
47+
:type path: typing.Optional[typing.Union[str, pathlib.Path]]
4748
"""
4849
super(_ChRootContext, self).__init__(conn=conn, path=path)
4950

@@ -85,11 +86,11 @@ async def __aexit__(self, exc_type: typing.Any, exc_val: typing.Any, exc_tb: typ
8586
"""Async context manager."""
8687
self.__alock.release() # type: ignore
8788

88-
def chroot(self, path: typing.Union[str, None]) -> "typing.ContextManager[None]":
89+
def chroot(self, path: typing.Union[str, pathlib.Path, None]) -> "typing.ContextManager[None]":
8990
"""Context manager for changing chroot rules.
9091
9192
:param path: chroot path or none for working without chroot.
92-
:type path: typing.Optional[str]
93+
:type path: typing.Optional[typing.Union[str, pathlib.Path]]
9394
:return: context manager with selected chroot state inside
9495
:rtype: typing.ContextManager
9596

test/test_ssh_client_execute_async_special.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# under the License.
1414

1515
# Standard Library
16+
import pathlib
1617
import shlex
1718
import typing
1819
from unittest import mock
@@ -263,3 +264,23 @@ def test_013_execute_async_no_chroot_context(ssh, ssh_transport_channel):
263264
mock.call.exec_command(f'{command}\n'),
264265
)
265266
)
267+
268+
269+
def test_012_execute_async_chroot_path(ssh, ssh_transport_channel):
270+
"""Command-only chroot path."""
271+
with ssh.chroot(pathlib.Path('/')):
272+
ssh._execute_async(command)
273+
ssh_transport_channel.assert_has_calls(
274+
(
275+
mock.call.makefile_stderr("rb"),
276+
mock.call.exec_command(f'chroot / sh -c {shlex.quote(f"eval {quoted_command}")}\n'),
277+
)
278+
)
279+
280+
281+
def test_013_execute_async_chroot_path_invalid_type(ssh, ssh_transport_channel):
282+
"""Command-only chroot path."""
283+
with pytest.raises(TypeError) as exc:
284+
with ssh.chroot(...):
285+
ssh._execute_async(command)
286+
assert str(exc.value) == f"path={...!r} is not instance of Optional[Union[str, pathlib.Path]]"

0 commit comments

Comments
 (0)