Fix merge conflicts
Markus Gerstel
2 years ago
11 | 11 | continue |
12 | 12 | filename = os.path.normpath(os.path.join(base, f)) |
13 | 13 | try: |
14 | with open(filename, "r") as fh: | |
14 | with open(filename) as fh: | |
15 | 15 | ast.parse(fh.read()) |
16 | 16 | except SyntaxError as se: |
17 | 17 | failures += 1 |
25 | 25 | vmImage: ubuntu-latest |
26 | 26 | strategy: |
27 | 27 | matrix: |
28 | python35: | |
29 | PYTHON_VERSION: 3.5 | |
30 | 28 | python36: |
31 | 29 | PYTHON_VERSION: 3.6 |
32 | 30 | python37: |
42 | 40 | vmImage: macOS-latest |
43 | 41 | strategy: |
44 | 42 | matrix: |
45 | python35: | |
46 | PYTHON_VERSION: 3.5 | |
47 | 43 | python36: |
48 | 44 | PYTHON_VERSION: 3.6 |
49 | 45 | python37: |
59 | 55 | vmImage: windows-latest |
60 | 56 | strategy: |
61 | 57 | matrix: |
62 | python35: | |
63 | PYTHON_VERSION: 3.5 | |
64 | 58 | python36: |
65 | 59 | PYTHON_VERSION: 3.6 |
66 | 60 | python37: |
8 | 8 | directory: "/" # Location of package manifests |
9 | 9 | schedule: |
10 | 10 | interval: "monthly" |
11 | ignore: | |
12 | - dependency-name: "mock" | |
13 | # mock 4 requires Python 3.6+ | |
14 | versions: [">=4"] | |
15 | - dependency-name: "twine" | |
16 | # twine 2 requires Python 3.6+ | |
17 | versions: [">=2"] |
11 | 11 | - python: 3.8 |
12 | 12 | - python: 3.7 |
13 | 13 | - python: 3.6 |
14 | - python: 3.5 | |
15 | 14 | - os: osx |
16 | 15 | language: generic |
17 | 16 | env: CONDA=3.8 TOXENV=py38 |
21 | 20 | - os: osx |
22 | 21 | language: generic |
23 | 22 | env: CONDA=3.6 TOXENV=py36 |
24 | - os: osx | |
25 | language: generic | |
26 | env: CONDA=3.5 TOXENV=py35 | |
27 | 23 | |
28 | 24 | allow_failures: |
29 | 25 | - env: OPTIONAL=1 |
0 | 0 | ======= |
1 | 1 | History |
2 | 2 | ======= |
3 | ||
4 | 2.3.0 (????-??-??) | |
5 | ------------------ | |
6 | * Python 3.6+ only, support for Python 3.5 has been dropped | |
3 | 7 | |
4 | 8 | 2.2.0 (2020-09-07) |
5 | 9 | ------------------ |
27 | 31 | |
28 | 32 | 2.0.0 (2020-06-24) |
29 | 33 | ------------------ |
30 | ||
31 | 34 | * Python 3.5+ only, support for Python 2.7 has been dropped |
32 | 35 | * Deprecated function alias run_process() has been removed |
33 | 36 | * Fixed a stability issue on Windows |
34 | 37 | |
35 | 38 | 1.1.0 (2019-11-04) |
36 | 39 | ------------------ |
37 | ||
38 | 40 | * Add Python 3.8 support, drop Python 3.4 support |
39 | 41 | |
40 | 42 | 1.0.2 (2019-05-20) |
41 | 43 | ------------------ |
42 | ||
43 | 44 | * Stop environment override variables leaking into the process environment |
44 | 45 | |
45 | 46 | 1.0.1 (2019-04-16) |
46 | 47 | ------------------ |
47 | ||
48 | 48 | * Minor fixes on the return object (implement equality, |
49 | 49 | mark as unhashable) |
50 | 50 | |
51 | 51 | 1.0.0 (2019-03-25) |
52 | 52 | ------------------ |
53 | ||
54 | 53 | * Support file system path objects (PEP-519) in arguments |
55 | 54 | * Change the return object to make it similar to |
56 | 55 | subprocess.CompletedProcess, introduced with Python 3.5+ |
57 | 56 | |
58 | 57 | 0.9.1 (2019-02-22) |
59 | 58 | ------------------ |
60 | ||
61 | 59 | * Have deprecation warnings point to correct code locations |
62 | 60 | |
63 | 61 | 0.9.0 (2018-12-07) |
64 | 62 | ------------------ |
65 | ||
66 | 63 | * Trap UnicodeEncodeError when printing output. Offending characters |
67 | 64 | are replaced and a warning is logged once. Hints at incorrectly set |
68 | 65 | PYTHONIOENCODING. |
69 | 66 | |
70 | 67 | 0.8.1 (2018-12-04) |
71 | 68 | ------------------ |
72 | ||
73 | 69 | * Fix a few deprecation warnings |
74 | 70 | |
75 | 71 | 0.8.0 (2018-10-09) |
76 | 72 | ------------------ |
77 | ||
78 | 73 | * Add parameter working_directory to set the working directory |
79 | 74 | of the subprocess |
80 | 75 | |
81 | 76 | 0.7.2 (2018-10-05) |
82 | 77 | ------------------ |
83 | ||
84 | 78 | * Officially support Python 3.7 |
85 | 79 | |
86 | 80 | 0.7.1 (2018-09-03) |
87 | 81 | ------------------ |
88 | ||
89 | 82 | * Accept environment variable overriding with numeric values. |
90 | 83 | |
91 | 84 | 0.7.0 (2018-05-13) |
92 | 85 | ------------------ |
93 | ||
94 | 86 | * Unicode fixes. Fix crash on invalid UTF-8 input. |
95 | 87 | * Clarify that stdout/stderr values are returned as bytestrings. |
96 | 88 | * Callbacks receive the data decoded as UTF-8 unicode strings |
100 | 92 | |
101 | 93 | 0.6.1 (2018-05-02) |
102 | 94 | ------------------ |
103 | ||
104 | 95 | * Maintenance release to add some tests for executable resolution. |
105 | 96 | |
106 | 97 | 0.6.0 (2018-05-02) |
107 | 98 | ------------------ |
108 | ||
109 | 99 | * Fix Win32 API executable resolution for commands containing a dot ('.') in |
110 | 100 | addition to a file extension (say '.bat'). |
111 | 101 | |
112 | 102 | 0.5.1 (2018-04-27) |
113 | 103 | ------------------ |
114 | ||
115 | 104 | * Fix Win32API dependency installation on Windows. |
116 | 105 | |
117 | 106 | 0.5.0 (2018-04-26) |
118 | 107 | ------------------ |
119 | ||
120 | 108 | * New keyword 'win32resolve' which only takes effect on Windows and is enabled |
121 | 109 | by default. This causes procrunner to call the Win32 API FindExecutable() |
122 | 110 | function to try and lookup non-.exe files with the corresponding name. This |
125 | 113 | |
126 | 114 | 0.4.0 (2018-04-23) |
127 | 115 | ------------------ |
128 | ||
129 | 116 | * Python 2.7 support on Windows. Python3 not yet supported on Windows. |
130 | 117 | |
131 | 118 | 0.3.0 (2018-04-17) |
132 | 119 | ------------------ |
133 | ||
134 | 120 | * run_process() renamed to run() |
135 | 121 | * Python3 compatibility fixes |
136 | 122 | |
137 | 123 | 0.2.0 (2018-03-12) |
138 | 124 | ------------------ |
139 | ||
140 | 125 | * Procrunner is now Python3 3.3-3.6 compatible. |
141 | 126 | |
142 | 127 | 0.1.0 (2018-03-12) |
143 | 128 | ------------------ |
144 | ||
145 | 129 | * First release on PyPI. |
4 | 4 | # For Python versions available on Appveyor, see |
5 | 5 | # http://www.appveyor.com/docs/installed-software#python |
6 | 6 | |
7 | - PYTHON: "C:\\Python35" | |
8 | 7 | - PYTHON: "C:\\Python36" |
9 | 8 | - PYTHON: "C:\\Python37" |
10 | 9 | - PYTHON: "C:\\Python38" |
11 | - PYTHON: "C:\\Python35-x64" | |
12 | 10 | - PYTHON: "C:\\Python36-x64" |
13 | 11 | - PYTHON: "C:\\Python37-x64" |
14 | 12 | - PYTHON: "C:\\Python38-x64" |
274 | 274 | return obj |
275 | 275 | |
276 | 276 | |
277 | def _windows_resolve(command): | |
277 | def _windows_resolve(command, path=None): | |
278 | 278 | """ |
279 | 279 | Try and find the full path and file extension of the executable to run. |
280 | 280 | This is so that e.g. calls to 'somescript' will point at 'somescript.cmd' |
289 | 289 | if not command or not isinstance(command[0], str): |
290 | 290 | return command |
291 | 291 | |
292 | found_executable = shutil.which(command[0]) | |
292 | found_executable = shutil.which(command[0], path=path) | |
293 | 293 | if found_executable: |
294 | 294 | logger.debug("Resolved %s as %s", command[0], found_executable) |
295 | 295 | return (found_executable, *command[1:]) |
298 | 298 | # Special case. shutil.which may not detect file extensions if a full |
299 | 299 | # path is given, so try to resolve the executable explicitly |
300 | 300 | for extension in os.getenv("PATHEXT").split(os.pathsep): |
301 | found_executable = shutil.which(command[0] + extension) | |
301 | found_executable = shutil.which(command[0] + extension, path=path) | |
302 | 302 | if found_executable: |
303 | 303 | return (found_executable, *command[1:]) |
304 | 304 | |
334 | 334 | if key in self._extras: |
335 | 335 | return self._extras[key] |
336 | 336 | if not hasattr(self, key): |
337 | raise KeyError("Unknown attribute {key}".format(key=key)) | |
337 | raise KeyError(f"Unknown attribute {key}") | |
338 | 338 | return getattr(self, key) |
339 | 339 | |
340 | 340 | def __eq__(self, other): |
517 | 517 | command = tuple(_path_resolve(part) for part in command) |
518 | 518 | if win32resolve and sys.platform == "win32": |
519 | 519 | command = _windows_resolve(command) |
520 | if working_directory and sys.version_info < (3, 7): | |
521 | working_directory = os.fspath(working_directory) | |
520 | 522 | |
521 | 523 | p = subprocess.Popen( |
522 | 524 | command, |
523 | 525 | shell=False, |
524 | cwd=_path_resolve(working_directory), | |
526 | cwd=working_directory, | |
525 | 527 | env=env, |
526 | 528 | stdin=stdin_pipe, |
527 | 529 | stdout=subprocess.PIPE, |
0 | 0 | bump2version==1.0.0 |
1 | 1 | coverage==5.3 |
2 | 2 | flake8==3.8.3 |
3 | mock==3.0.5 | |
4 | 3 | pip==20.2.3 |
5 | 4 | pytest==6.1.0 |
6 | 5 | Sphinx==3.2.1 |
12 | 12 | |
13 | 13 | setup_requirements = [] |
14 | 14 | |
15 | test_requirements = ["mock", "pytest"] | |
15 | test_requirements = ["pytest"] | |
16 | 16 | |
17 | 17 | setup( |
18 | 18 | author="Markus Gerstel", |
24 | 24 | "Natural Language :: English", |
25 | 25 | "Operating System :: OS Independent", |
26 | 26 | "Programming Language :: Python :: 3", |
27 | "Programming Language :: Python :: 3.5", | |
28 | 27 | "Programming Language :: Python :: 3.6", |
29 | 28 | "Programming Language :: Python :: 3.7", |
30 | 29 | "Programming Language :: Python :: 3.8", |
39 | 38 | keywords="procrunner", |
40 | 39 | name="procrunner", |
41 | 40 | packages=find_packages(include=["procrunner"]), |
42 | python_requires=">=3.5", | |
41 | python_requires=">=3.6", | |
43 | 42 | setup_requires=setup_requirements, |
44 | 43 | test_suite="tests", |
45 | 44 | tests_require=test_requirements, |
0 | 0 | import copy |
1 | import mock | |
1 | from unittest import mock | |
2 | 2 | import os |
3 | import pathlib | |
3 | 4 | import procrunner |
4 | 5 | import pytest |
5 | 6 | import sys |
85 | 86 | timeout=0.5, |
86 | 87 | callback_stdout=mock.sentinel.callback_stdout, |
87 | 88 | callback_stderr=mock.sentinel.callback_stderr, |
88 | working_directory=mock.sentinel.cwd, | |
89 | working_directory=pathlib.Path("somecwd"), | |
89 | 90 | raise_timeout_exception=True, |
90 | 91 | ) |
91 | 92 | |
92 | 93 | assert mock_subprocess.Popen.called |
93 | 94 | assert mock_subprocess.Popen.call_args[1]["env"] == os.environ |
94 | assert mock_subprocess.Popen.call_args[1]["cwd"] == mock.sentinel.cwd | |
95 | assert mock_subprocess.Popen.call_args[1]["cwd"] in ( | |
96 | pathlib.Path("somecwd"), | |
97 | "somecwd", | |
98 | ) | |
95 | 99 | mock_streamreader.assert_has_calls( |
96 | 100 | [ |
97 | 101 | mock.call( |
57 | 57 | |
58 | 58 | |
59 | 59 | @pytest.mark.skipif(sys.platform != "win32", reason="windows specific test only") |
60 | def test_name_resolution_for_complex_cases(tmpdir): | |
61 | tmpdir.chdir() | |
62 | ||
60 | def test_name_resolution_for_complex_cases(tmp_path): | |
63 | 61 | bat = "simple_bat_extension" |
64 | 62 | cmd = "simple_cmd_extension" |
65 | 63 | exe = "simple_exe_extension" |
66 | 64 | dotshort = "more_complex_filename_with_a.dot" |
67 | 65 | dotlong = "more_complex_filename.withadot" |
68 | 66 | |
69 | (tmpdir / bat + ".bat").ensure() | |
70 | (tmpdir / cmd + ".cmd").ensure() | |
71 | (tmpdir / exe + ".exe").ensure() | |
72 | (tmpdir / dotshort + ".bat").ensure() | |
73 | (tmpdir / dotlong + ".cmd").ensure() | |
67 | (tmp_path / (bat + ".bat")).touch() | |
68 | (tmp_path / (cmd + ".cmd")).touch() | |
69 | (tmp_path / (exe + ".exe")).touch() | |
70 | (tmp_path / (dotshort + ".bat")).touch() | |
71 | (tmp_path / (dotlong + ".cmd")).touch() | |
74 | 72 | |
75 | 73 | def is_valid(command): |
76 | 74 | assert len(command) == 1 |
77 | assert os.path.exists(command[0]) | |
75 | assert os.path.exists(tmp_path / command[0]) | |
78 | 76 | |
79 | is_valid(procrunner._windows_resolve([bat])) | |
80 | is_valid(procrunner._windows_resolve([cmd])) | |
81 | is_valid(procrunner._windows_resolve([exe])) | |
82 | is_valid(procrunner._windows_resolve([dotshort])) | |
83 | is_valid(procrunner._windows_resolve([dotlong])) | |
77 | is_valid(procrunner._windows_resolve([bat], path=os.fspath(tmp_path))) | |
78 | is_valid(procrunner._windows_resolve([cmd], path=os.fspath(tmp_path))) | |
79 | is_valid(procrunner._windows_resolve([exe], path=os.fspath(tmp_path))) | |
80 | is_valid(procrunner._windows_resolve([dotshort], path=os.fspath(tmp_path))) | |
81 | is_valid(procrunner._windows_resolve([dotlong], path=os.fspath(tmp_path))) |
39 | 39 | assert err == "" |
40 | 40 | |
41 | 41 | |
42 | def test_running_wget(tmpdir): | |
43 | tmpdir.chdir() | |
42 | def test_running_wget(tmp_path): | |
44 | 43 | command = ["wget", "https://www.google.com", "-O", "-"] |
45 | 44 | try: |
46 | result = procrunner.run(command) | |
45 | result = procrunner.run(command, working_directory=tmp_path) | |
47 | 46 | except OSError as e: |
48 | 47 | if e.errno == 2: |
49 | 48 | pytest.skip("wget not available") |
53 | 52 | assert b"google" in result.stdout |
54 | 53 | |
55 | 54 | |
56 | def test_path_object_resolution(tmpdir): | |
55 | def test_path_object_resolution(tmp_path): | |
57 | 56 | sentinel_value = b"sentinel" |
58 | tmpdir.join("tempfile").write(sentinel_value) | |
59 | tmpdir.join("reader.py").write("with open('tempfile') as fh:\n print(fh.read())") | |
57 | tmp_path.joinpath("tempfile").write_bytes(sentinel_value) | |
58 | tmp_path.joinpath("reader.py").write_text( | |
59 | "with open('tempfile') as fh:\n print(fh.read())" | |
60 | ) | |
60 | 61 | assert "LEAK_DETECTOR" not in os.environ |
61 | 62 | result = procrunner.run( |
62 | [sys.executable, tmpdir.join("reader.py")], | |
63 | [sys.executable, tmp_path / "reader.py"], | |
63 | 64 | environment_override={"PYTHONHASHSEED": "random", "LEAK_DETECTOR": "1"}, |
64 | working_directory=tmpdir, | |
65 | working_directory=tmp_path, | |
65 | 66 | ) |
66 | 67 | assert result.returncode == 0 |
67 | 68 | assert not result.stderr |