diff --git a/.azure-pipelines/syntax-validation.py b/.azure-pipelines/syntax-validation.py index 6483683..9359ad8 100644 --- a/.azure-pipelines/syntax-validation.py +++ b/.azure-pipelines/syntax-validation.py @@ -12,7 +12,7 @@ continue filename = os.path.normpath(os.path.join(base, f)) try: - with open(filename, "r") as fh: + with open(filename) as fh: ast.parse(fh.read()) except SyntaxError as se: failures += 1 diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 288782f..2666f34 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -26,8 +26,6 @@ vmImage: ubuntu-latest strategy: matrix: - python35: - PYTHON_VERSION: 3.5 python36: PYTHON_VERSION: 3.6 python37: @@ -43,8 +41,6 @@ vmImage: macOS-latest strategy: matrix: - python35: - PYTHON_VERSION: 3.5 python36: PYTHON_VERSION: 3.6 python37: @@ -60,8 +56,6 @@ vmImage: windows-latest strategy: matrix: - python35: - PYTHON_VERSION: 3.5 python36: PYTHON_VERSION: 3.6 python37: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 853aac6..55f99c9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,10 +9,3 @@ directory: "/" # Location of package manifests schedule: interval: "monthly" - ignore: - - dependency-name: "mock" - # mock 4 requires Python 3.6+ - versions: [">=4"] - - dependency-name: "twine" - # twine 2 requires Python 3.6+ - versions: [">=2"] diff --git a/.travis.yml b/.travis.yml index 776ea00..523d7a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,6 @@ - python: 3.8 - python: 3.7 - python: 3.6 - - python: 3.5 - os: osx language: generic env: CONDA=3.8 TOXENV=py38 @@ -22,9 +21,6 @@ - os: osx language: generic env: CONDA=3.6 TOXENV=py36 - - os: osx - language: generic - env: CONDA=3.5 TOXENV=py35 allow_failures: - env: OPTIONAL=1 diff --git a/HISTORY.rst b/HISTORY.rst index 73f9981..3c2b0d4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,10 @@ ======= History ======= + +2.3.0 (????-??-??) +------------------ +* Python 3.6+ only, support for Python 3.5 has been dropped 2.2.0 (2020-09-07) ------------------ @@ -28,70 +32,58 @@ 2.0.0 (2020-06-24) ------------------ - * Python 3.5+ only, support for Python 2.7 has been dropped * Deprecated function alias run_process() has been removed * Fixed a stability issue on Windows 1.1.0 (2019-11-04) ------------------ - * Add Python 3.8 support, drop Python 3.4 support 1.0.2 (2019-05-20) ------------------ - * Stop environment override variables leaking into the process environment 1.0.1 (2019-04-16) ------------------ - * Minor fixes on the return object (implement equality, mark as unhashable) 1.0.0 (2019-03-25) ------------------ - * Support file system path objects (PEP-519) in arguments * Change the return object to make it similar to subprocess.CompletedProcess, introduced with Python 3.5+ 0.9.1 (2019-02-22) ------------------ - * Have deprecation warnings point to correct code locations 0.9.0 (2018-12-07) ------------------ - * Trap UnicodeEncodeError when printing output. Offending characters are replaced and a warning is logged once. Hints at incorrectly set PYTHONIOENCODING. 0.8.1 (2018-12-04) ------------------ - * Fix a few deprecation warnings 0.8.0 (2018-10-09) ------------------ - * Add parameter working_directory to set the working directory of the subprocess 0.7.2 (2018-10-05) ------------------ - * Officially support Python 3.7 0.7.1 (2018-09-03) ------------------ - * Accept environment variable overriding with numeric values. 0.7.0 (2018-05-13) ------------------ - * Unicode fixes. Fix crash on invalid UTF-8 input. * Clarify that stdout/stderr values are returned as bytestrings. * Callbacks receive the data decoded as UTF-8 unicode strings @@ -101,23 +93,19 @@ 0.6.1 (2018-05-02) ------------------ - * Maintenance release to add some tests for executable resolution. 0.6.0 (2018-05-02) ------------------ - * Fix Win32 API executable resolution for commands containing a dot ('.') in addition to a file extension (say '.bat'). 0.5.1 (2018-04-27) ------------------ - * Fix Win32API dependency installation on Windows. 0.5.0 (2018-04-26) ------------------ - * New keyword 'win32resolve' which only takes effect on Windows and is enabled by default. This causes procrunner to call the Win32 API FindExecutable() function to try and lookup non-.exe files with the corresponding name. This @@ -126,21 +114,17 @@ 0.4.0 (2018-04-23) ------------------ - * Python 2.7 support on Windows. Python3 not yet supported on Windows. 0.3.0 (2018-04-17) ------------------ - * run_process() renamed to run() * Python3 compatibility fixes 0.2.0 (2018-03-12) ------------------ - * Procrunner is now Python3 3.3-3.6 compatible. 0.1.0 (2018-03-12) ------------------ - * First release on PyPI. diff --git a/appveyor.yml b/appveyor.yml index 9ae4a75..0cccda9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,11 +5,9 @@ # For Python versions available on Appveyor, see # http://www.appveyor.com/docs/installed-software#python - - PYTHON: "C:\\Python35" - PYTHON: "C:\\Python36" - PYTHON: "C:\\Python37" - PYTHON: "C:\\Python38" - - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36-x64" - PYTHON: "C:\\Python37-x64" - PYTHON: "C:\\Python38-x64" diff --git a/procrunner/__init__.py b/procrunner/__init__.py index f5dd08e..1a8b176 100644 --- a/procrunner/__init__.py +++ b/procrunner/__init__.py @@ -275,7 +275,7 @@ return obj -def _windows_resolve(command): +def _windows_resolve(command, path=None): """ Try and find the full path and file extension of the executable to run. This is so that e.g. calls to 'somescript' will point at 'somescript.cmd' @@ -290,7 +290,7 @@ if not command or not isinstance(command[0], str): return command - found_executable = shutil.which(command[0]) + found_executable = shutil.which(command[0], path=path) if found_executable: logger.debug("Resolved %s as %s", command[0], found_executable) return (found_executable, *command[1:]) @@ -299,7 +299,7 @@ # Special case. shutil.which may not detect file extensions if a full # path is given, so try to resolve the executable explicitly for extension in os.getenv("PATHEXT").split(os.pathsep): - found_executable = shutil.which(command[0] + extension) + found_executable = shutil.which(command[0] + extension, path=path) if found_executable: return (found_executable, *command[1:]) @@ -335,7 +335,7 @@ if key in self._extras: return self._extras[key] if not hasattr(self, key): - raise KeyError("Unknown attribute {key}".format(key=key)) + raise KeyError(f"Unknown attribute {key}") return getattr(self, key) def __eq__(self, other): @@ -518,11 +518,13 @@ command = tuple(_path_resolve(part) for part in command) if win32resolve and sys.platform == "win32": command = _windows_resolve(command) + if working_directory and sys.version_info < (3, 7): + working_directory = os.fspath(working_directory) p = subprocess.Popen( command, shell=False, - cwd=_path_resolve(working_directory), + cwd=working_directory, env=env, stdin=stdin_pipe, stdout=subprocess.PIPE, diff --git a/requirements_dev.txt b/requirements_dev.txt index 4d6c108..c25cd9e 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,7 +1,6 @@ bump2version==1.0.0 coverage==5.3 flake8==3.8.3 -mock==3.0.5 pip==20.2.3 pytest==6.1.0 Sphinx==3.2.1 diff --git a/setup.py b/setup.py index 600e8c4..09c9259 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup_requirements = [] -test_requirements = ["mock", "pytest"] +test_requirements = ["pytest"] setup( author="Markus Gerstel", @@ -25,7 +25,6 @@ "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", @@ -40,7 +39,7 @@ keywords="procrunner", name="procrunner", packages=find_packages(include=["procrunner"]), - python_requires=">=3.5", + python_requires=">=3.6", setup_requires=setup_requirements, test_suite="tests", tests_require=test_requirements, diff --git a/tests/test_procrunner.py b/tests/test_procrunner.py index b103183..8acec7c 100644 --- a/tests/test_procrunner.py +++ b/tests/test_procrunner.py @@ -1,6 +1,7 @@ import copy -import mock +from unittest import mock import os +import pathlib import procrunner import pytest import sys @@ -86,13 +87,16 @@ timeout=0.5, callback_stdout=mock.sentinel.callback_stdout, callback_stderr=mock.sentinel.callback_stderr, - working_directory=mock.sentinel.cwd, + working_directory=pathlib.Path("somecwd"), raise_timeout_exception=True, ) assert mock_subprocess.Popen.called assert mock_subprocess.Popen.call_args[1]["env"] == os.environ - assert mock_subprocess.Popen.call_args[1]["cwd"] == mock.sentinel.cwd + assert mock_subprocess.Popen.call_args[1]["cwd"] in ( + pathlib.Path("somecwd"), + "somecwd", + ) mock_streamreader.assert_has_calls( [ mock.call( diff --git a/tests/test_procrunner_resolution.py b/tests/test_procrunner_resolution.py index 2237c7c..e099be4 100644 --- a/tests/test_procrunner_resolution.py +++ b/tests/test_procrunner_resolution.py @@ -58,27 +58,25 @@ @pytest.mark.skipif(sys.platform != "win32", reason="windows specific test only") -def test_name_resolution_for_complex_cases(tmpdir): - tmpdir.chdir() - +def test_name_resolution_for_complex_cases(tmp_path): bat = "simple_bat_extension" cmd = "simple_cmd_extension" exe = "simple_exe_extension" dotshort = "more_complex_filename_with_a.dot" dotlong = "more_complex_filename.withadot" - (tmpdir / bat + ".bat").ensure() - (tmpdir / cmd + ".cmd").ensure() - (tmpdir / exe + ".exe").ensure() - (tmpdir / dotshort + ".bat").ensure() - (tmpdir / dotlong + ".cmd").ensure() + (tmp_path / (bat + ".bat")).touch() + (tmp_path / (cmd + ".cmd")).touch() + (tmp_path / (exe + ".exe")).touch() + (tmp_path / (dotshort + ".bat")).touch() + (tmp_path / (dotlong + ".cmd")).touch() def is_valid(command): assert len(command) == 1 - assert os.path.exists(command[0]) + assert os.path.exists(tmp_path / command[0]) - is_valid(procrunner._windows_resolve([bat])) - is_valid(procrunner._windows_resolve([cmd])) - is_valid(procrunner._windows_resolve([exe])) - is_valid(procrunner._windows_resolve([dotshort])) - is_valid(procrunner._windows_resolve([dotlong])) + is_valid(procrunner._windows_resolve([bat], path=os.fspath(tmp_path))) + is_valid(procrunner._windows_resolve([cmd], path=os.fspath(tmp_path))) + is_valid(procrunner._windows_resolve([exe], path=os.fspath(tmp_path))) + is_valid(procrunner._windows_resolve([dotshort], path=os.fspath(tmp_path))) + is_valid(procrunner._windows_resolve([dotlong], path=os.fspath(tmp_path))) diff --git a/tests/test_procrunner_system.py b/tests/test_procrunner_system.py index eebc312..0dada84 100644 --- a/tests/test_procrunner_system.py +++ b/tests/test_procrunner_system.py @@ -40,11 +40,10 @@ assert err == "" -def test_running_wget(tmpdir): - tmpdir.chdir() +def test_running_wget(tmp_path): command = ["wget", "https://www.google.com", "-O", "-"] try: - result = procrunner.run(command) + result = procrunner.run(command, working_directory=tmp_path) except OSError as e: if e.errno == 2: pytest.skip("wget not available") @@ -54,15 +53,17 @@ assert b"google" in result.stdout -def test_path_object_resolution(tmpdir): +def test_path_object_resolution(tmp_path): sentinel_value = b"sentinel" - tmpdir.join("tempfile").write(sentinel_value) - tmpdir.join("reader.py").write("with open('tempfile') as fh:\n print(fh.read())") + tmp_path.joinpath("tempfile").write_bytes(sentinel_value) + tmp_path.joinpath("reader.py").write_text( + "with open('tempfile') as fh:\n print(fh.read())" + ) assert "LEAK_DETECTOR" not in os.environ result = procrunner.run( - [sys.executable, tmpdir.join("reader.py")], + [sys.executable, tmp_path / "reader.py"], environment_override={"PYTHONHASHSEED": "random", "LEAK_DETECTOR": "1"}, - working_directory=tmpdir, + working_directory=tmp_path, ) assert result.returncode == 0 assert not result.stderr diff --git a/tox.ini b/tox.ini index b015248..fc76a2d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35, py36, py37, py38, py39, flake8 +envlist = py36, py37, py38, py39, flake8 [travis] python = @@ -7,7 +7,6 @@ 3.8: py38 3.7: py37 3.6: py36 - 3.5: py35 [testenv:azure] basepython = python