iripau 0.1.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-0.1.0 → iripau-1.1.0}/PKG-INFO +3 -2
  2. iripau-1.1.0/iripau/requests.py +81 -0
  3. {iripau-0.1.0 → iripau-1.1.0}/iripau/subprocess.py +55 -21
  4. {iripau-0.1.0 → iripau-1.1.0}/iripau.egg-info/PKG-INFO +3 -2
  5. {iripau-0.1.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-0.1.0 → iripau-1.1.0}/pyproject.toml +3 -2
  8. iripau-1.1.0/tests/test_requests.py +72 -0
  9. {iripau-0.1.0 → iripau-1.1.0}/tests/test_subprocess.py +48 -22
  10. iripau-0.1.0/iripau.egg-info/requires.txt +0 -1
  11. {iripau-0.1.0 → iripau-1.1.0}/LICENSE +0 -0
  12. {iripau-0.1.0 → iripau-1.1.0}/README.md +0 -0
  13. {iripau-0.1.0 → iripau-1.1.0}/iripau/__init__.py +0 -0
  14. {iripau-0.1.0 → iripau-1.1.0}/iripau/executable.py +0 -0
  15. {iripau-0.1.0 → iripau-1.1.0}/iripau/functools.py +0 -0
  16. {iripau-0.1.0 → iripau-1.1.0}/iripau/logging.py +0 -0
  17. {iripau-0.1.0 → iripau-1.1.0}/iripau/random.py +0 -0
  18. {iripau-0.1.0 → iripau-1.1.0}/iripau/shutil.py +0 -0
  19. {iripau-0.1.0 → iripau-1.1.0}/iripau/threading.py +0 -0
  20. {iripau-0.1.0 → iripau-1.1.0}/iripau.egg-info/dependency_links.txt +0 -0
  21. {iripau-0.1.0 → iripau-1.1.0}/iripau.egg-info/top_level.txt +0 -0
  22. {iripau-0.1.0 → iripau-1.1.0}/setup.cfg +0 -0
  23. {iripau-0.1.0 → iripau-1.1.0}/tests/test_command.py +0 -0
  24. {iripau-0.1.0 → iripau-1.1.0}/tests/test_executable.py +0 -0
  25. {iripau-0.1.0 → iripau-1.1.0}/tests/test_functools.py +0 -0
  26. {iripau-0.1.0 → iripau-1.1.0}/tests/test_logging.py +0 -0
  27. {iripau-0.1.0 → iripau-1.1.0}/tests/test_random.py +0 -0
  28. {iripau-0.1.0 → iripau-1.1.0}/tests/test_shutil.py +0 -0
  29. {iripau-0.1.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: 0.1.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
@@ -9,11 +9,12 @@ Project-URL: Repository, https://github.com/ricardo-galo/iripau
9
9
  Project-URL: Bug Tracker, https://github.com/ricardo-galo/iripau/issues
10
10
  Project-URL: Changelog, https://github.com/ricardo-galo/iripau/blob/master/CHANGELOG.md
11
11
  Keywords: subprocess,command,local,remote,ssh,Linux,real-time,log,logging,threading,utilities
12
- Classifier: Development Status :: 4 - Beta
12
+ Classifier: Development Status :: 5 - Production/Stable
13
13
  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
@@ -5,7 +5,6 @@ This module relies on the following system utilities being installed:
5
5
  * bash
6
6
  * kill
7
7
  * pstree
8
- * sudo
9
8
  * tee
10
9
  """
11
10
 
@@ -38,6 +37,7 @@ GLOBAL_PROMPTS = set()
38
37
 
39
38
 
40
39
  TeeStream = Union[io.IOBase, Callable[[], io.IOBase]]
40
+ TeeStreams = Iterable[TeeStream]
41
41
 
42
42
 
43
43
  class PipeFile(SpooledTemporaryFile):
@@ -124,27 +124,21 @@ class Popen(subprocess.Popen):
124
124
 
125
125
  def __init__(
126
126
  self, args, *, cwd=None, env=None, encoding=None, errors=None, text=None,
127
- stdout_tees: Iterable[TeeStream] = [], add_global_stdout_tees=True,
128
- stderr_tees: Iterable[TeeStream] = [], add_global_stderr_tees=True,
129
- 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,
130
130
  echo=None, alias=None, comment=None, **kwargs
131
131
  ):
132
132
  stdout = kwargs.get("stdout")
133
133
  stderr = kwargs.get("stderr")
134
134
 
135
- stdout_tees, stderr_tees, prompt_tees = self._get_tee_sets(
135
+ stdout_tees, stderr_tees, prompt_tees, err2out = self._get_tee_sets(
136
136
  stdout_tees, add_global_stdout_tees,
137
137
  stderr_tees, add_global_stderr_tees,
138
138
  prompt_tees, add_global_prompt_tees,
139
139
  echo, stdout, stderr
140
140
  )
141
141
 
142
- if stderr is STDOUT:
143
- err2out = True
144
- stderr_tees = set()
145
- else:
146
- err2out = False
147
-
148
142
  stdout_fds = {tee.fileno() for tee in stdout_tees}
149
143
  stderr_fds = {tee.fileno() for tee in stderr_tees}
150
144
  prompt_fds = {tee.fileno() for tee in prompt_tees}
@@ -176,7 +170,7 @@ class Popen(subprocess.Popen):
176
170
  self.stderr_process = stderr_process
177
171
 
178
172
  @staticmethod
179
- def _get_tee_files(tees: Iterable[TeeStream]):
173
+ def _get_tee_files(tees: TeeStreams):
180
174
  return set(callable(tee) and tee() or tee for tee in tees)
181
175
 
182
176
  @classmethod
@@ -218,12 +212,52 @@ class Popen(subprocess.Popen):
218
212
  if stderr_tees:
219
213
  stderr_tees.add(sys.stderr)
220
214
 
215
+ if stderr is STDOUT:
216
+ err2out = True
217
+ stderr_tees = set()
218
+ else:
219
+ err2out = False
220
+
221
221
  return (
222
222
  cls._get_tee_files(stdout_tees),
223
223
  cls._get_tee_files(stderr_tees),
224
- 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
225
241
  )
226
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
+
227
261
  def get_pids(self):
228
262
  """ Return the pid for all of the processes in the tree """
229
263
  output = run(
@@ -239,13 +273,13 @@ class Popen(subprocess.Popen):
239
273
 
240
274
  def terminate_tree(self):
241
275
  run(
242
- ["sudo", "kill"] + self.get_pids(),
276
+ ["kill"] + self.get_pids(),
243
277
  stdin=DEVNULL, stdout=DEVNULL, stderr=DEVNULL,
244
278
  )
245
279
 
246
280
  def kill_tree(self):
247
281
  run(
248
- ["sudo", "kill", "-9"] + self.get_pids(),
282
+ ["kill", "-9"] + self.get_pids(),
249
283
  stdin=DEVNULL, stdout=DEVNULL, stderr=DEVNULL,
250
284
  )
251
285
 
@@ -383,7 +417,7 @@ if subprocess.run(
383
417
  stderr=DEVNULL
384
418
  ).stdout.splitlines()[-2:]
385
419
 
386
- 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):
387
421
  """ Write shell prompt and command into file descriptors fds """
388
422
  fds = normalize_outerr_fds(fds)
389
423
  custom_env = {"CPS1": PS1, "CPS2": PS2}
@@ -393,7 +427,7 @@ if subprocess.run(
393
427
  "(\n"
394
428
  " IFS= read -r \"line\"\n"
395
429
  " echo \"${CPS1@P}${line}\"\n"
396
- " while IFS= read line; do\n"
430
+ " while IFS= read -r \"line\"; do\n"
397
431
  " echo \"${CPS2@P}${line}\"\n"
398
432
  " done\n"
399
433
  ") | " + quote(Tee.get_cmd(fds - {1}))
@@ -401,7 +435,7 @@ if subprocess.run(
401
435
  subprocess.run(
402
436
  ["bash", "-c", script],
403
437
  text=True,
404
- input=shellify(cmd, err2out, comment),
438
+ input=shellify(cmd, err2out, comment) + "\n",
405
439
  stdout=None if 1 in fds else DEVNULL,
406
440
  stderr=None if 2 in fds else DEVNULL,
407
441
  pass_fds=fds - {1, 2},
@@ -410,7 +444,7 @@ if subprocess.run(
410
444
  check=True
411
445
  )
412
446
  else: # Use hard-coded PS1 and PS2 strings
413
- 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):
414
448
  """ Write shell prompt and command into file descriptors fds """
415
449
  cmd = shellify(cmd, err2out, comment) + "\n"
416
450
  input = "$ " + "> ".join(cmd.splitlines(keepends=True))
@@ -450,7 +484,7 @@ def _output_context(kwargs, key, encoding, errors, text):
450
484
 
451
485
  def run(
452
486
  args, *, input=None, capture_output=False, timeout=None, check=False,
453
- encoding=None, errors=None, text=None, sigterm_timeout=10, **kwargs
487
+ encoding=None, errors=None, text=None, sigterm_timeout=10, comment=None, **kwargs
454
488
  ):
455
489
  """ A subprocess.run that instantiates this module's Popen """
456
490
  if input is not None:
@@ -464,7 +498,7 @@ def run(
464
498
  kwargs["stdout"] = FILE
465
499
  kwargs["stderr"] = FILE
466
500
 
467
- comment = f"timeout={timeout}" if timeout else None
501
+ comment = " ".join((comment or "", f"timeout={timeout}" if timeout else "")).strip()
468
502
  with (
469
503
  _output_context(kwargs, "stdout", encoding, errors, text) as stdout_file,
470
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: 0.1.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
@@ -9,11 +9,12 @@ Project-URL: Repository, https://github.com/ricardo-galo/iripau
9
9
  Project-URL: Bug Tracker, https://github.com/ricardo-galo/iripau/issues
10
10
  Project-URL: Changelog, https://github.com/ricardo-galo/iripau/blob/master/CHANGELOG.md
11
11
  Keywords: subprocess,command,local,remote,ssh,Linux,real-time,log,logging,threading,utilities
12
- Classifier: Development Status :: 4 - Beta
12
+ Classifier: Development Status :: 5 - Production/Stable
13
13
  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 = "0.1.0"
7
+ version = "1.1.0"
8
8
  dependencies = [
9
+ "curlify",
9
10
  "psutil"
10
11
  ]
11
12
  requires-python = ">=3.7"
@@ -21,7 +22,7 @@ license = "MPL-2.0"
21
22
  license-files = ["LICENSE"]
22
23
  keywords = ["subprocess", "command", "local", "remote", "ssh", "Linux", "real-time", "log", "logging", "threading", "utilities"]
23
24
  classifiers = [
24
- "Development Status :: 4 - Beta",
25
+ "Development Status :: 5 - Production/Stable",
25
26
  "Programming Language :: Python"
26
27
  ]
27
28
 
@@ -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