iripau 1.0.0__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {iripau-1.0.0 → iripau-1.1.0}/PKG-INFO +2 -1
  2. iripau-1.1.0/iripau/requests.py +81 -0
  3. {iripau-1.0.0 → iripau-1.1.0}/iripau/subprocess.py +53 -18
  4. {iripau-1.0.0 → iripau-1.1.0}/iripau.egg-info/PKG-INFO +2 -1
  5. {iripau-1.0.0 → iripau-1.1.0}/iripau.egg-info/SOURCES.txt +2 -0
  6. iripau-1.1.0/iripau.egg-info/requires.txt +2 -0
  7. {iripau-1.0.0 → iripau-1.1.0}/pyproject.toml +2 -1
  8. iripau-1.1.0/tests/test_requests.py +72 -0
  9. {iripau-1.0.0 → iripau-1.1.0}/tests/test_subprocess.py +48 -22
  10. iripau-1.0.0/iripau.egg-info/requires.txt +0 -1
  11. {iripau-1.0.0 → iripau-1.1.0}/LICENSE +0 -0
  12. {iripau-1.0.0 → iripau-1.1.0}/README.md +0 -0
  13. {iripau-1.0.0 → iripau-1.1.0}/iripau/__init__.py +0 -0
  14. {iripau-1.0.0 → iripau-1.1.0}/iripau/executable.py +0 -0
  15. {iripau-1.0.0 → iripau-1.1.0}/iripau/functools.py +0 -0
  16. {iripau-1.0.0 → iripau-1.1.0}/iripau/logging.py +0 -0
  17. {iripau-1.0.0 → iripau-1.1.0}/iripau/random.py +0 -0
  18. {iripau-1.0.0 → iripau-1.1.0}/iripau/shutil.py +0 -0
  19. {iripau-1.0.0 → iripau-1.1.0}/iripau/threading.py +0 -0
  20. {iripau-1.0.0 → iripau-1.1.0}/iripau.egg-info/dependency_links.txt +0 -0
  21. {iripau-1.0.0 → iripau-1.1.0}/iripau.egg-info/top_level.txt +0 -0
  22. {iripau-1.0.0 → iripau-1.1.0}/setup.cfg +0 -0
  23. {iripau-1.0.0 → iripau-1.1.0}/tests/test_command.py +0 -0
  24. {iripau-1.0.0 → iripau-1.1.0}/tests/test_executable.py +0 -0
  25. {iripau-1.0.0 → iripau-1.1.0}/tests/test_functools.py +0 -0
  26. {iripau-1.0.0 → iripau-1.1.0}/tests/test_logging.py +0 -0
  27. {iripau-1.0.0 → iripau-1.1.0}/tests/test_random.py +0 -0
  28. {iripau-1.0.0 → iripau-1.1.0}/tests/test_shutil.py +0 -0
  29. {iripau-1.0.0 → iripau-1.1.0}/tests/test_threading.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iripau
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Python utilities focused on command execution
5
5
  Author: Ricardo Quezada
6
6
  Maintainer: Ricardo Quezada
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python
14
14
  Requires-Python: >=3.7
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
+ Requires-Dist: curlify
17
18
  Requires-Dist: psutil
18
19
  Dynamic: license-file
19
20
 
@@ -0,0 +1,81 @@
1
+ """
2
+ Utilities for the requests module
3
+ """
4
+
5
+ import requests
6
+
7
+ from curlify import to_curl
8
+
9
+ from iripau.subprocess import TeeStreams, Popen
10
+
11
+
12
+ def curlify(
13
+ response, compressed=False, verify=True, pretty=False,
14
+ hide_output=False, headers_to_hide=[], headers_to_omit=[],
15
+ stdout_tees: TeeStreams = [], add_global_stdout_tees=True,
16
+ stderr_tees: TeeStreams = [], add_global_stderr_tees=True,
17
+ prompt_tees: TeeStreams = [], add_global_prompt_tees=True,
18
+ echo=None
19
+ ):
20
+ """ Simulate the request was executed by a curl subprocess.
21
+ The command and output can be echoed and/or sent to files as described
22
+ in subprocess.run
23
+ """
24
+ request = response.request
25
+ if headers_to_hide or headers_to_omit:
26
+ request = request.copy()
27
+
28
+ for header in headers_to_omit:
29
+ if header in request.headers:
30
+ del request.headers[header]
31
+
32
+ for header in headers_to_hide:
33
+ if header in request.headers:
34
+ request.headers[header] = "***"
35
+
36
+ stdout = hide_output and b"***" or response.content
37
+ if not stdout.endswith(b"\n"):
38
+ stdout += b"\n"
39
+ stderr = ""
40
+
41
+ Popen.simulate(
42
+ cmd=to_curl(request, compressed, verify, pretty),
43
+ stdout=stdout,
44
+ stderr=stderr,
45
+ comment=f"{response.status_code} - {response.reason}",
46
+ stdout_tees=stdout_tees,
47
+ stderr_tees=stderr_tees,
48
+ prompt_tees=prompt_tees,
49
+ add_global_stdout_tees=add_global_stdout_tees,
50
+ add_global_stderr_tees=add_global_stderr_tees,
51
+ add_global_prompt_tees=add_global_prompt_tees,
52
+ echo=echo
53
+ )
54
+
55
+
56
+ class Session(requests.Session):
57
+ """ A requests.Session that accepts curlify arguments in the request method """
58
+
59
+ def request(
60
+ self, *args, compressed=False, pretty=False,
61
+ hide_output=False, headers_to_hide=[], headers_to_omit=[],
62
+ stdout_tees: TeeStreams = [], add_global_stdout_tees=True,
63
+ stderr_tees: TeeStreams = [], add_global_stderr_tees=True,
64
+ prompt_tees: TeeStreams = [], add_global_prompt_tees=True,
65
+ echo=None, **kwargs
66
+ ):
67
+ response = super().request(*args, **kwargs)
68
+
69
+ verify = kwargs.get("verify")
70
+ if verify is None:
71
+ verify = self.verify
72
+
73
+ curlify(
74
+ response, compressed, verify, pretty,
75
+ hide_output, headers_to_hide, headers_to_omit,
76
+ stdout_tees, add_global_stdout_tees,
77
+ stderr_tees, add_global_stderr_tees,
78
+ prompt_tees, add_global_prompt_tees,
79
+ echo
80
+ )
81
+ return response
@@ -37,6 +37,7 @@ GLOBAL_PROMPTS = set()
37
37
 
38
38
 
39
39
  TeeStream = Union[io.IOBase, Callable[[], io.IOBase]]
40
+ TeeStreams = Iterable[TeeStream]
40
41
 
41
42
 
42
43
  class PipeFile(SpooledTemporaryFile):
@@ -123,27 +124,21 @@ class Popen(subprocess.Popen):
123
124
 
124
125
  def __init__(
125
126
  self, args, *, cwd=None, env=None, encoding=None, errors=None, text=None,
126
- stdout_tees: Iterable[TeeStream] = [], add_global_stdout_tees=True,
127
- stderr_tees: Iterable[TeeStream] = [], add_global_stderr_tees=True,
128
- prompt_tees: Iterable[TeeStream] = [], add_global_prompt_tees=True,
127
+ stdout_tees: TeeStreams = [], add_global_stdout_tees=True,
128
+ stderr_tees: TeeStreams = [], add_global_stderr_tees=True,
129
+ prompt_tees: TeeStreams = [], add_global_prompt_tees=True,
129
130
  echo=None, alias=None, comment=None, **kwargs
130
131
  ):
131
132
  stdout = kwargs.get("stdout")
132
133
  stderr = kwargs.get("stderr")
133
134
 
134
- stdout_tees, stderr_tees, prompt_tees = self._get_tee_sets(
135
+ stdout_tees, stderr_tees, prompt_tees, err2out = self._get_tee_sets(
135
136
  stdout_tees, add_global_stdout_tees,
136
137
  stderr_tees, add_global_stderr_tees,
137
138
  prompt_tees, add_global_prompt_tees,
138
139
  echo, stdout, stderr
139
140
  )
140
141
 
141
- if stderr is STDOUT:
142
- err2out = True
143
- stderr_tees = set()
144
- else:
145
- err2out = False
146
-
147
142
  stdout_fds = {tee.fileno() for tee in stdout_tees}
148
143
  stderr_fds = {tee.fileno() for tee in stderr_tees}
149
144
  prompt_fds = {tee.fileno() for tee in prompt_tees}
@@ -175,7 +170,7 @@ class Popen(subprocess.Popen):
175
170
  self.stderr_process = stderr_process
176
171
 
177
172
  @staticmethod
178
- def _get_tee_files(tees: Iterable[TeeStream]):
173
+ def _get_tee_files(tees: TeeStreams):
179
174
  return set(callable(tee) and tee() or tee for tee in tees)
180
175
 
181
176
  @classmethod
@@ -217,12 +212,52 @@ class Popen(subprocess.Popen):
217
212
  if stderr_tees:
218
213
  stderr_tees.add(sys.stderr)
219
214
 
215
+ if stderr is STDOUT:
216
+ err2out = True
217
+ stderr_tees = set()
218
+ else:
219
+ err2out = False
220
+
220
221
  return (
221
222
  cls._get_tee_files(stdout_tees),
222
223
  cls._get_tee_files(stderr_tees),
223
- cls._get_tee_files(prompt_tees)
224
+ cls._get_tee_files(prompt_tees),
225
+ err2out
226
+ )
227
+
228
+ @classmethod
229
+ def simulate(
230
+ cls, cmd, stdout, stderr, encoding=None, errors=None, text=None, comment=None,
231
+ stdout_tees: TeeStreams = [], add_global_stdout_tees=True,
232
+ stderr_tees: TeeStreams = [], add_global_stderr_tees=True,
233
+ prompt_tees: TeeStreams = [], add_global_prompt_tees=True,
234
+ echo=None
235
+ ):
236
+ stdout_tees, stderr_tees, prompt_tees, err2out = cls._get_tee_sets(
237
+ stdout_tees, add_global_stdout_tees,
238
+ stderr_tees, add_global_stderr_tees,
239
+ prompt_tees, add_global_prompt_tees,
240
+ echo, DEVNULL, DEVNULL
224
241
  )
225
242
 
243
+ if not (stdout_tees or stderr_tees or prompt_tees):
244
+ return
245
+
246
+ stdout_fds = {tee.fileno() for tee in stdout_tees}
247
+ stderr_fds = {tee.fileno() for tee in stderr_tees}
248
+ prompt_fds = {tee.fileno() for tee in prompt_tees}
249
+
250
+ if prompt_fds:
251
+ stream_prompts(prompt_fds, cmd, None, None, err2out, comment)
252
+
253
+ if stdout_fds:
254
+ with Tee(PIPE, stdout_fds, DEVNULL, encoding, errors, text) as tee:
255
+ tee.communicate(stdout)
256
+
257
+ if stderr_fds:
258
+ with Tee(PIPE, stderr_fds, DEVNULL, encoding, errors, text) as tee:
259
+ tee.communicate(stderr)
260
+
226
261
  def get_pids(self):
227
262
  """ Return the pid for all of the processes in the tree """
228
263
  output = run(
@@ -382,7 +417,7 @@ if subprocess.run(
382
417
  stderr=DEVNULL
383
418
  ).stdout.splitlines()[-2:]
384
419
 
385
- def stream_prompts(fds: Iterable[str], cmd, cwd=None, env=None, err2out=False, comment=None):
420
+ def stream_prompts(fds: Iterable[int], cmd, cwd=None, env=None, err2out=False, comment=None):
386
421
  """ Write shell prompt and command into file descriptors fds """
387
422
  fds = normalize_outerr_fds(fds)
388
423
  custom_env = {"CPS1": PS1, "CPS2": PS2}
@@ -392,7 +427,7 @@ if subprocess.run(
392
427
  "(\n"
393
428
  " IFS= read -r \"line\"\n"
394
429
  " echo \"${CPS1@P}${line}\"\n"
395
- " while IFS= read line; do\n"
430
+ " while IFS= read -r \"line\"; do\n"
396
431
  " echo \"${CPS2@P}${line}\"\n"
397
432
  " done\n"
398
433
  ") | " + quote(Tee.get_cmd(fds - {1}))
@@ -400,7 +435,7 @@ if subprocess.run(
400
435
  subprocess.run(
401
436
  ["bash", "-c", script],
402
437
  text=True,
403
- input=shellify(cmd, err2out, comment),
438
+ input=shellify(cmd, err2out, comment) + "\n",
404
439
  stdout=None if 1 in fds else DEVNULL,
405
440
  stderr=None if 2 in fds else DEVNULL,
406
441
  pass_fds=fds - {1, 2},
@@ -409,7 +444,7 @@ if subprocess.run(
409
444
  check=True
410
445
  )
411
446
  else: # Use hard-coded PS1 and PS2 strings
412
- def stream_prompts(fds: Iterable[str], cmd, cwd=None, env=None, err2out=False, comment=None):
447
+ def stream_prompts(fds: Iterable[int], cmd, cwd=None, env=None, err2out=False, comment=None):
413
448
  """ Write shell prompt and command into file descriptors fds """
414
449
  cmd = shellify(cmd, err2out, comment) + "\n"
415
450
  input = "$ " + "> ".join(cmd.splitlines(keepends=True))
@@ -449,7 +484,7 @@ def _output_context(kwargs, key, encoding, errors, text):
449
484
 
450
485
  def run(
451
486
  args, *, input=None, capture_output=False, timeout=None, check=False,
452
- encoding=None, errors=None, text=None, sigterm_timeout=10, **kwargs
487
+ encoding=None, errors=None, text=None, sigterm_timeout=10, comment=None, **kwargs
453
488
  ):
454
489
  """ A subprocess.run that instantiates this module's Popen """
455
490
  if input is not None:
@@ -463,7 +498,7 @@ def run(
463
498
  kwargs["stdout"] = FILE
464
499
  kwargs["stderr"] = FILE
465
500
 
466
- comment = f"timeout={timeout}" if timeout else None
501
+ comment = " ".join((comment or "", f"timeout={timeout}" if timeout else "")).strip()
467
502
  with (
468
503
  _output_context(kwargs, "stdout", encoding, errors, text) as stdout_file,
469
504
  _output_context(kwargs, "stderr", encoding, errors, text) as stderr_file,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iripau
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Python utilities focused on command execution
5
5
  Author: Ricardo Quezada
6
6
  Maintainer: Ricardo Quezada
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python
14
14
  Requires-Python: >=3.7
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
+ Requires-Dist: curlify
17
18
  Requires-Dist: psutil
18
19
  Dynamic: license-file
19
20
 
@@ -6,6 +6,7 @@ iripau/executable.py
6
6
  iripau/functools.py
7
7
  iripau/logging.py
8
8
  iripau/random.py
9
+ iripau/requests.py
9
10
  iripau/shutil.py
10
11
  iripau/subprocess.py
11
12
  iripau/threading.py
@@ -19,6 +20,7 @@ tests/test_executable.py
19
20
  tests/test_functools.py
20
21
  tests/test_logging.py
21
22
  tests/test_random.py
23
+ tests/test_requests.py
22
24
  tests/test_shutil.py
23
25
  tests/test_subprocess.py
24
26
  tests/test_threading.py
@@ -0,0 +1,2 @@
1
+ curlify
2
+ psutil
@@ -4,8 +4,9 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "iripau"
7
- version = "1.0.0"
7
+ version = "1.1.0"
8
8
  dependencies = [
9
+ "curlify",
9
10
  "psutil"
10
11
  ]
11
12
  requires-python = ">=3.7"
@@ -0,0 +1,72 @@
1
+ """
2
+ Tests to validate iripau.requests module
3
+ """
4
+
5
+ import pytest
6
+
7
+ from iripau.requests import Session
8
+
9
+
10
+ class TestRequests:
11
+
12
+ @pytest.mark.parametrize("hide_output", [False, True], ids=["show_output", "hide_output"])
13
+ @pytest.mark.parametrize("session_verify", [False, True], ids=["request", "session"])
14
+ @pytest.mark.parametrize("verify", [False, True], ids=["insecure", "secure"])
15
+ def test_curlify(self, verify, session_verify, hide_output, capfd):
16
+ session = Session()
17
+
18
+ if session_verify:
19
+ session.verify = verify
20
+
21
+ response = session.post(
22
+ "https://dummyjson.com/test",
23
+ verify=None if session_verify else verify,
24
+ headers={
25
+ "API-Key": "QWERTY123",
26
+ "Accept": "application/json",
27
+ "Authorization": "Bearer 23U746F5R23745RG78345EDR3"
28
+ },
29
+ data={
30
+ "name": "The Name",
31
+ "status": "Old$"
32
+ },
33
+ hide_output=hide_output,
34
+ headers_to_hide=["API-Key", "Authorization"],
35
+ headers_to_omit=["User-Agent", "Accept-Encoding", "Connection"],
36
+ echo=True
37
+ )
38
+
39
+ out, err = capfd.readouterr()
40
+ if verify:
41
+ assert "--insecure" not in out
42
+ else:
43
+ assert "--insecure" in out
44
+
45
+ if hide_output:
46
+ assert out.endswith("***\n")
47
+ else:
48
+ assert not out.endswith("***\n")
49
+
50
+ # Omitted headers
51
+ assert "-H 'Accept: application/json'" in out
52
+ assert "-H 'API-Key: ***'" in out
53
+ assert "-H 'Authorization: ***'" in out
54
+
55
+ # Hidden headers
56
+ assert "-H 'Accept-Encoding: gzip, deflate'" not in out
57
+ assert "-H 'Connection: keep-alive'" not in out
58
+ assert "-H 'User-Agent: python-requests/2.32.3'" not in out
59
+
60
+ # Because of using data argument in the request
61
+ assert "-H 'Content-Type: application/x-www-form-urlencoded'" in out
62
+ assert "-d 'name=The+Name&status=Old%24'" in out
63
+
64
+ assert not err
65
+
66
+ # The request object was not touched
67
+ assert "Accept-Encoding" in response.request.headers
68
+ assert "Connection" in response.request.headers
69
+ assert "User-Agent" in response.request.headers
70
+
71
+ assert response.request.headers["API-Key"] != "***"
72
+ assert response.request.headers["Authorization"] != "***"
@@ -292,7 +292,8 @@ class TestSubprocess:
292
292
  @pytest.mark.parametrize("stderr", [None, STDOUT], ids=["no_redirect", "redirect"])
293
293
  @pytest.mark.parametrize("stdout", [None, PIPE, FILE], ids=["no_capture", "pipe", "file"])
294
294
  @pytest.mark.parametrize("echo", [False, True], ids=["no_echo", "echo"])
295
- def test_run_tee(self, echo, stdout, stderr, extra_tees, manual_echo, capfd):
295
+ @pytest.mark.parametrize("simulation", [False, True], ids=["reality", "simulation"])
296
+ def test_run_tee(self, simulation, echo, stdout, stderr, extra_tees, manual_echo, capfd):
296
297
  redirect = stderr is STDOUT
297
298
  stdout_command = "echo This goes to stdout"
298
299
  stderr_command = "echo This goes to stderr >&2"
@@ -314,9 +315,6 @@ class TestSubprocess:
314
315
  stderr_tees.add(sys.stderr)
315
316
 
316
317
  kwargs = {
317
- "stdout": stdout,
318
- "stderr": stderr,
319
- "shell": True,
320
318
  "text": True,
321
319
  "echo": echo,
322
320
  "stdout_tees": stdout_tees | all_tees,
@@ -324,21 +322,6 @@ class TestSubprocess:
324
322
  "prompt_tees": prompt_tees | all_tees
325
323
  }
326
324
 
327
- expected_stdout_1 = expected_stderr_1 = expected_stdout_2 = expected_stderr_2 = None
328
- if stdout is not None:
329
- expected_stdout_1 = "This goes to stdout\n"
330
- expected_stdout_2 = "This goes to stderr\n" if redirect else ""
331
-
332
- count = 30
333
- for _ in range(count):
334
- output = run(stdout_command, **kwargs)
335
- assert expected_stdout_1 == output.stdout
336
- assert expected_stderr_1 == output.stderr
337
-
338
- output = run(stderr_command, **kwargs)
339
- assert expected_stdout_2 == output.stdout
340
- assert expected_stderr_2 == output.stderr
341
-
342
325
  expected_stdout_command_prompt = get_prompt_and_command(stdout_command, redirect) + "\n"
343
326
  expected_stderr_command_prompt = get_prompt_and_command(stderr_command, redirect) + "\n"
344
327
  expected_prompt = expected_stdout_command_prompt + expected_stderr_command_prompt
@@ -352,8 +335,11 @@ class TestSubprocess:
352
335
  expected_stderr = ""
353
336
  if echo or manual_echo:
354
337
  expected_captured_stdout = expected_output
338
+ elif stdout is None:
339
+ expected_captured_stdout = "" if simulation else expected_stdout
355
340
  else:
356
- expected_captured_stdout = expected_stdout if stdout is None else ""
341
+ expected_captured_stdout = ""
342
+ expected_captured_stderr = ""
357
343
  else:
358
344
  expected_stdout = "This goes to stdout\n"
359
345
  expected_stderr = "This goes to stderr\n"
@@ -362,8 +348,48 @@ class TestSubprocess:
362
348
  expected_stdout_command_prompt + "This goes to stdout\n" +
363
349
  expected_stderr_command_prompt
364
350
  )
351
+ expected_captured_stderr = expected_stderr
352
+ elif stdout is None:
353
+ if simulation:
354
+ expected_captured_stdout = ""
355
+ expected_captured_stderr = ""
356
+ else:
357
+ expected_captured_stdout = expected_stdout
358
+ expected_captured_stderr = expected_stderr
365
359
  else:
366
- expected_captured_stdout = expected_stdout if stdout is None else ""
360
+ expected_captured_stdout = ""
361
+ expected_captured_stderr = "" if simulation else expected_stderr
362
+
363
+ count = 1
364
+ if simulation:
365
+ fake_stdout_command = stdout_command
366
+ fake_stderr_command = stderr_command
367
+ fake_stdout_1 = "This goes to stdout\n"
368
+ fake_stderr_1 = ""
369
+ fake_stdout_2 = ""
370
+ fake_stderr_2 = "This goes to stderr\n"
371
+ if redirect: # Simulate redirection
372
+ fake_stdout_command += " 2>&1"
373
+ fake_stderr_command += " 2>&1"
374
+ fake_stdout_2, fake_stderr_2 = fake_stderr_2, fake_stdout_2
375
+
376
+ for _ in range(count):
377
+ Popen.simulate(fake_stdout_command, fake_stdout_1, fake_stderr_1, **kwargs)
378
+ Popen.simulate(fake_stderr_command, fake_stdout_2, fake_stderr_2, **kwargs)
379
+ else:
380
+ expected_stdout_1 = expected_stderr_1 = expected_stdout_2 = expected_stderr_2 = None
381
+ if stdout is not None:
382
+ expected_stdout_1 = "This goes to stdout\n"
383
+ expected_stdout_2 = "This goes to stderr\n" if redirect else ""
384
+
385
+ for _ in range(count):
386
+ output = run(stdout_command, stdout=stdout, stderr=stderr, shell=True, **kwargs)
387
+ assert expected_stdout_1 == output.stdout
388
+ assert expected_stderr_1 == output.stderr
389
+
390
+ output = run(stderr_command, stdout=stdout, stderr=stderr, shell=True, **kwargs)
391
+ assert expected_stdout_2 == output.stdout
392
+ assert expected_stderr_2 == output.stderr
367
393
 
368
394
  expected_content = expected_stdout * count
369
395
  for file in stdout_tees - {sys.stdout}:
@@ -383,7 +409,7 @@ class TestSubprocess:
383
409
 
384
410
  out, err = capfd.readouterr()
385
411
  assert expected_captured_stdout * count == out
386
- assert expected_stderr * count == err
412
+ assert expected_captured_stderr * count == err
387
413
 
388
414
  @pytest.mark.parametrize("echo", [False, True], ids=["no_echo", "echo"])
389
415
  def test_run_alias(self, echo, capfd):
@@ -1 +0,0 @@
1
- psutil
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes