Skip to content

Commit eac80d1

Browse files
authored
Purge shell_tools functions (#5566)
Remove obsolete functions `shell_tools.run_cmd` and `shell_tools.run_shell`; as they are now replaced by `shell_tools.run`. Remove related classes and auxiliary functions `CommandOutput`, `TeeCapture`, `_async_forward`, `_async_wait_for_process`. This completes and closes #4394
1 parent 5c8d11e commit eac80d1

File tree

2 files changed

+1
-245
lines changed

2 files changed

+1
-245
lines changed

dev_tools/shell_tools.py

Lines changed: 1 addition & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,9 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import asyncio
1615
import subprocess
1716
import sys
18-
from typing import List, Optional, Tuple, Union, IO, Any, cast, NamedTuple
19-
20-
from collections.abc import AsyncIterable
21-
22-
CommandOutput = NamedTuple(
23-
"CommandOutput", [('out', Optional[str]), ('err', Optional[str]), ('exit_code', int)]
24-
)
17+
from typing import List, Tuple, Union
2518

2619

2720
BOLD = 1
@@ -45,69 +38,6 @@ def highlight(text: str, color_code: int, bold: bool = False) -> str:
4538
return '{}\033[{}m{}\033[0m'.format('\033[1m' if bold else '', color_code, text)
4639

4740

48-
class TeeCapture:
49-
"""Marker class indicating desire to capture output written to a pipe.
50-
51-
If out_pipe is None, the caller just wants to capture output without
52-
writing it to anything in particular.
53-
"""
54-
55-
def __init__(self, out_pipe: Optional[IO[str]] = None) -> None:
56-
self.out_pipe = out_pipe
57-
58-
59-
async def _async_forward(
60-
async_chunks: AsyncIterable, out: Optional[Union[TeeCapture, IO[str]]]
61-
) -> Optional[str]:
62-
"""Prints/captures output from the given asynchronous iterable.
63-
64-
Args:
65-
async_chunks: An asynchronous source of bytes or str.
66-
out: Where to put the chunks.
67-
68-
Returns:
69-
The complete captured output, or else None if the out argument wasn't a
70-
TeeCapture instance.
71-
"""
72-
capture = isinstance(out, TeeCapture)
73-
out_pipe = out.out_pipe if isinstance(out, TeeCapture) else out
74-
75-
chunks: Optional[List[str]] = [] if capture else None
76-
async for chunk in async_chunks:
77-
if not isinstance(chunk, str):
78-
chunk = chunk.decode()
79-
if out_pipe:
80-
print(chunk, file=out_pipe, end='')
81-
if chunks is not None:
82-
chunks.append(chunk)
83-
84-
return ''.join(chunks) if chunks is not None else None
85-
86-
87-
async def _async_wait_for_process(
88-
future_process: Any,
89-
out: Optional[Union[TeeCapture, IO[str]]] = sys.stdout,
90-
err: Optional[Union[TeeCapture, IO[str]]] = sys.stderr,
91-
) -> CommandOutput:
92-
"""Awaits the creation and completion of an asynchronous process.
93-
94-
Args:
95-
future_process: The eventually created process.
96-
out: Where to write stuff emitted by the process' stdout.
97-
err: Where to write stuff emitted by the process' stderr.
98-
99-
Returns:
100-
A (captured output, captured error output, return code) triplet.
101-
"""
102-
process = await future_process
103-
future_output = _async_forward(process.stdout, out)
104-
future_err_output = _async_forward(process.stderr, err)
105-
output, err_output = await asyncio.gather(future_output, future_err_output)
106-
await process.wait()
107-
108-
return CommandOutput(output, err_output, process.returncode)
109-
110-
11141
def abbreviate_command_arguments_after_switches(cmd: Tuple[str, ...]) -> Tuple[str, ...]:
11242
result = [cmd[0]]
11343
for i in range(1, len(cmd)):
@@ -165,126 +95,6 @@ def run(
16595
return subprocess.run(args, **subprocess_run_kwargs)
16696

16797

168-
def run_cmd(
169-
*cmd: Optional[str],
170-
out: Optional[Union[TeeCapture, IO[str]]] = sys.stdout,
171-
err: Optional[Union[TeeCapture, IO[str]]] = sys.stderr,
172-
raise_on_fail: bool = True,
173-
log_run_to_stderr: bool = True,
174-
abbreviate_non_option_arguments: bool = False,
175-
**kwargs,
176-
) -> CommandOutput:
177-
"""Invokes a subprocess and waits for it to finish.
178-
179-
Args:
180-
*cmd: Components of the command to execute, e.g. ["echo", "dog"].
181-
out: Where to write the process' stdout. Defaults to sys.stdout. Can be
182-
anything accepted by print's 'file' parameter, or None if the
183-
output should be dropped, or a TeeCapture instance. If a TeeCapture
184-
instance is given, the first element of the returned tuple will be
185-
the captured output.
186-
err: Where to write the process' stderr. Defaults to sys.stderr. Can be
187-
anything accepted by print's 'file' parameter, or None if the
188-
output should be dropped, or a TeeCapture instance. If a TeeCapture
189-
instance is given, the second element of the returned tuple will be
190-
the captured error output.
191-
raise_on_fail: If the process returns a non-zero error code
192-
and this flag is set, a CalledProcessError will be raised.
193-
Otherwise the return code is the third element of the returned
194-
tuple.
195-
log_run_to_stderr: Determines whether the fact that this shell command
196-
was executed is logged to sys.stderr or not.
197-
abbreviate_non_option_arguments: When logging to stderr, this cuts off
198-
the potentially-huge tail of the command listing off e.g. hundreds
199-
of file paths. No effect if log_run_to_stderr is not set.
200-
**kwargs: Extra arguments for asyncio.create_subprocess_shell, such as
201-
a cwd (current working directory) argument.
202-
203-
Returns:
204-
A (captured output, captured error output, return code) triplet. The
205-
captured outputs will be None if the out or err parameters were not set
206-
to an instance of TeeCapture.
207-
208-
Raises:
209-
subprocess.CalledProcessError: The process returned a non-zero error
210-
code and raise_on_fail was set.
211-
"""
212-
kept_cmd = tuple(cast(str, e) for e in cmd if e is not None)
213-
if log_run_to_stderr:
214-
cmd_desc = kept_cmd
215-
if abbreviate_non_option_arguments:
216-
cmd_desc = abbreviate_command_arguments_after_switches(cmd_desc)
217-
print('run:', cmd_desc, file=sys.stderr)
218-
result = asyncio.get_event_loop().run_until_complete(
219-
_async_wait_for_process(
220-
asyncio.create_subprocess_exec(
221-
*kept_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, **kwargs
222-
),
223-
out,
224-
err,
225-
)
226-
)
227-
if raise_on_fail and result[2]:
228-
raise subprocess.CalledProcessError(result[2], kept_cmd)
229-
return result
230-
231-
232-
def run_shell(
233-
cmd: str,
234-
out: Optional[Union[TeeCapture, IO[str]]] = sys.stdout,
235-
err: Optional[Union[TeeCapture, IO[str]]] = sys.stderr,
236-
raise_on_fail: bool = True,
237-
log_run_to_stderr: bool = True,
238-
**kwargs,
239-
) -> CommandOutput:
240-
"""Invokes a shell command and waits for it to finish.
241-
242-
Args:
243-
cmd: The command line string to execute, e.g. "echo dog | cat > file".
244-
out: Where to write the process' stdout. Defaults to sys.stdout. Can be
245-
anything accepted by print's 'file' parameter, or None if the
246-
output should be dropped, or a TeeCapture instance. If a TeeCapture
247-
instance is given, the first element of the returned tuple will be
248-
the captured output.
249-
err: Where to write the process' stderr. Defaults to sys.stderr. Can be
250-
anything accepted by print's 'file' parameter, or None if the
251-
output should be dropped, or a TeeCapture instance. If a TeeCapture
252-
instance is given, the second element of the returned tuple will be
253-
the captured error output.
254-
raise_on_fail: If the process returns a non-zero error code
255-
and this flag is set, a CalledProcessError will be raised.
256-
Otherwise the return code is the third element of the returned
257-
tuple.
258-
log_run_to_stderr: Determines whether the fact that this shell command
259-
was executed is logged to sys.stderr or not.
260-
**kwargs: Extra arguments for asyncio.create_subprocess_shell, such as
261-
a cwd (current working directory) argument.
262-
263-
Returns:
264-
A (captured output, captured error output, return code) triplet. The
265-
captured outputs will be None if the out or err parameters were not set
266-
to an instance of TeeCapture.
267-
268-
Raises:
269-
subprocess.CalledProcessError: The process returned a non-zero error
270-
code and raise_on_fail was set.
271-
"""
272-
if log_run_to_stderr:
273-
print('shell:', cmd, file=sys.stderr)
274-
result = asyncio.get_event_loop().run_until_complete(
275-
_async_wait_for_process(
276-
asyncio.create_subprocess_shell(
277-
cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, **kwargs
278-
),
279-
out,
280-
err,
281-
)
282-
)
283-
if raise_on_fail and result[2]:
284-
raise subprocess.CalledProcessError(result[2], cmd)
285-
return result
286-
287-
28898
def output_of(args: Union[str, List[str]], **kwargs) -> str:
28999
"""Invokes a subprocess and returns its output as a string.
290100

dev_tools/shell_tools_test.py

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,6 @@ def run(*args, **kwargs):
2626
return shell_tools.run(*args, log_run_to_stderr=False, **kwargs)
2727

2828

29-
def run_cmd(*args, **kwargs):
30-
return shell_tools.run_cmd(*args, log_run_to_stderr=False, **kwargs)
31-
32-
33-
def run_shell(*args, **kwargs):
34-
return shell_tools.run_shell(*args, log_run_to_stderr=False, **kwargs)
35-
36-
3729
@only_on_posix
3830
def test_run_raises_on_failure():
3931
assert run('true').returncode == 0
@@ -59,52 +51,6 @@ def test_run_with_command_logging():
5951
assert catch_stderr.getvalue() == "run: ('echo', '-n', '[...]')\n"
6052

6153

62-
@only_on_posix
63-
def test_run_cmd_raise_on_fail():
64-
assert run_cmd('true') == (None, None, 0)
65-
assert run_cmd('true', raise_on_fail=False) == (None, None, 0)
66-
67-
with pytest.raises(subprocess.CalledProcessError):
68-
run_cmd('false')
69-
assert run_cmd('false', raise_on_fail=False) == (None, None, 1)
70-
71-
72-
@only_on_posix
73-
def test_run_shell_raise_on_fail():
74-
assert run_shell('true') == (None, None, 0)
75-
assert run_shell('true', raise_on_fail=False) == (None, None, 0)
76-
77-
with pytest.raises(subprocess.CalledProcessError):
78-
run_shell('false')
79-
assert run_shell('false', raise_on_fail=False) == (None, None, 1)
80-
81-
82-
@only_on_posix
83-
def test_run_cmd_capture():
84-
assert run_cmd('echo', 'test', out=None) == (None, None, 0)
85-
assert run_cmd('echo', 'test', out=shell_tools.TeeCapture()) == ('test\n', None, 0)
86-
assert run_cmd('echo', 'test', out=None, err=shell_tools.TeeCapture()) == (None, '', 0)
87-
88-
89-
@only_on_posix
90-
def test_run_shell_capture():
91-
assert run_shell('echo test 1>&2', err=None) == (None, None, 0)
92-
assert run_shell('echo test 1>&2', err=shell_tools.TeeCapture()) == (None, 'test\n', 0)
93-
assert run_shell('echo test 1>&2', err=None, out=shell_tools.TeeCapture()) == ('', None, 0)
94-
95-
96-
@only_on_posix
97-
def test_run_shell_does_not_deadlock_on_large_outputs():
98-
assert run_shell(
99-
r"""python3 -c "import sys;"""
100-
r"""print((('o' * 99) + '\n') * 10000);"""
101-
r"""print((('e' * 99) + '\n') * 10000, file=sys.stderr)"""
102-
'"',
103-
out=None,
104-
err=None,
105-
) == (None, None, 0)
106-
107-
10854
@only_on_posix
10955
def test_output_of():
11056
assert shell_tools.output_of('true') == ''

0 commit comments

Comments
 (0)