Codebase list python-procrunner / fb086983-6445-4395-b47d-97f9e23f48c5/main
New upstream release. Debian Janitor 2 years ago
26 changed file(s) with 742 addition(s) and 387 deletion(s). Raw diff Collapse all Expand all
0 steps:
1 - task: UsePythonVersion@0
2 inputs:
3 versionSpec: '$(PYTHON_VERSION)'
4 displayName: 'Use Python $(PYTHON_VERSION)'
5
6 - script: |
7 python -m pip install --upgrade pip
8 pip install tox
9 displayName: "Set up tox"
10
11 - script: |
12 tox -e azure
13 displayName: "Run tests"
14
15 - task: PublishTestResults@2
16 condition: succeededOrFailed()
17 inputs:
18 testResultsFiles: '**/test-*.xml'
19 testRunTitle: 'Publish test results for Python $(PYTHON_VERSION)'
0 import os
1 import subprocess
2
3 # Flake8 validation
4 failures = 0
5 try:
6 flake8 = subprocess.run(
7 [
8 "flake8",
9 "--exit-zero",
10 "--max-line-length=88",
11 "--select=E401,E711,E712,E713,E714,E721,E722,E901,F401,F402,F403,F405,F631,F632,F633,F811,F812,F821,F822,F841,F901,W191,W291,W292,W293,W602,W603,W604,W605,W606",
12 ],
13 capture_output=True,
14 check=True,
15 encoding="latin-1",
16 timeout=300,
17 )
18 except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
19 print(
20 "##vso[task.logissue type=error;]flake8 validation failed with",
21 str(e.__class__.__name__),
22 )
23 print(e.stdout)
24 print(e.stderr)
25 print("##vso[task.complete result=Failed;]flake8 validation failed")
26 exit()
27 for line in flake8.stdout.split("\n"):
28 if ":" not in line:
29 continue
30 filename, lineno, column, error = line.split(":", maxsplit=3)
31 errcode, error = error.strip().split(" ", maxsplit=1)
32 filename = os.path.normpath(filename)
33 failures += 1
34 print(
35 f"##vso[task.logissue type=error;sourcepath={filename};"
36 f"linenumber={lineno};columnnumber={column};code={errcode};]" + error
37 )
38
39 if failures:
40 print(f"##vso[task.logissue type=warning]Found {failures} flake8 violation(s)")
41 print(f"##vso[task.complete result=Failed;]Found {failures} flake8 violation(s)")
0 import ast
1 import os
2 import sys
3
4 print("Python", sys.version, "\n")
5
6 failures = 0
7
8 for base, _, files in os.walk("."):
9 for f in files:
10 if not f.endswith(".py"):
11 continue
12 filename = os.path.normpath(os.path.join(base, f))
13 try:
14 with open(filename) as fh:
15 ast.parse(fh.read())
16 except SyntaxError as se:
17 failures += 1
18 print(
19 f"##vso[task.logissue type=error;sourcepath={filename};"
20 f"linenumber={se.lineno};columnnumber={se.offset};]"
21 f"SyntaxError: {se.msg}"
22 )
23 print(" " + se.text + " " * se.offset + "^")
24 print(f"SyntaxError: {se.msg} in {filename} line {se.lineno}")
25 print()
26
27 if failures:
28 print(f"##vso[task.logissue type=warning]Found {failures} syntax error(s)")
29 print(f"##vso[task.complete result=Failed;]Found {failures} syntax error(s)")
0 trigger:
1 branches:
2 include:
3 - '*'
4 tags:
5 include:
6 - '*'
7
8 stages:
9 - stage: static
10 displayName: Static Analysis
11 jobs:
12 - job: checks
13 displayName: static code analysis
14 pool:
15 vmImage: ubuntu-latest
16 steps:
17 # Use Python >=3.7 for syntax validation
18 - task: UsePythonVersion@0
19 displayName: Set up python
20 inputs:
21 versionSpec: 3.7
22
23 # Run syntax validation on a shallow clone
24 - bash: |
25 python .azure-pipelines/syntax-validation.py
26 displayName: Syntax validation
27
28 # Run flake8 validation on a shallow clone
29 - bash: |
30 pip install flake8
31 python .azure-pipelines/flake8-validation.py
32 displayName: Flake8 validation
33
34 - stage: tests
35 displayName: Run unit tests
36 jobs:
37 - job: linux
38 pool:
39 vmImage: ubuntu-latest
40 strategy:
41 matrix:
42 python36:
43 PYTHON_VERSION: 3.6
44 python37:
45 PYTHON_VERSION: 3.7
46 python38:
47 PYTHON_VERSION: 3.8
48 python39:
49 PYTHON_VERSION: 3.9
50 steps:
51 - template: .azure-pipelines/ci.yml
52
53 - job: macOS
54 pool:
55 vmImage: macOS-latest
56 strategy:
57 matrix:
58 python36:
59 PYTHON_VERSION: 3.6
60 python37:
61 PYTHON_VERSION: 3.7
62 python38:
63 PYTHON_VERSION: 3.8
64 python39:
65 PYTHON_VERSION: 3.9
66 steps:
67 - template: .azure-pipelines/ci.yml
68
69 - job: windows
70 pool:
71 vmImage: windows-latest
72 strategy:
73 matrix:
74 python36:
75 PYTHON_VERSION: 3.6
76 python37:
77 PYTHON_VERSION: 3.7
78 python38:
79 PYTHON_VERSION: 3.8
80 python39:
81 PYTHON_VERSION: 3.9
82 steps:
83 - template: .azure-pipelines/ci.yml
84
85 - stage: deploy
86 displayName: Publish release
87 dependsOn:
88 - tests
89 - static
90 condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))
91 jobs:
92 - job: pypi
93 displayName: Publish pypi release
94 pool:
95 vmImage: ubuntu-latest
96 steps:
97 - task: UsePythonVersion@0
98 displayName: Set up python
99 inputs:
100 versionSpec: 3.8
101
102 - bash: |
103 python -m pip install -r requirements_dev.txt
104 displayName: Install dependencies
105
106 - bash: |
107 python setup.py sdist bdist_wheel
108 ls -la dist
109 displayName: Build python package
110
111 - task: PublishBuildArtifacts@1
112 inputs:
113 pathToPublish: dist/
114 artifactName: python-release
115
116 - task: TwineAuthenticate@1
117 displayName: Set up credentials
118 inputs:
119 pythonUploadServiceConnection: pypi-procrunner
120
121 - bash: |
122 python -m twine upload -r pypi-procrunner --config-file $(PYPIRC_PATH) dist/*.tar.gz dist/*.whl
123 displayName: Publish package
0 [bumpversion]
1 current_version = 2.3.0
2 commit = True
3 tag = True
4
5 [bumpversion:file:setup.py]
6 search = version="{current_version}"
7 replace = version="{new_version}"
8
9 [bumpversion:file:procrunner/__init__.py]
10 search = __version__ = "{current_version}"
11 replace = __version__ = "{new_version}"
12
0 # To get started with Dependabot version updates, you'll need to specify which
1 # package ecosystems to update and where the package manifests are located.
2 # Please see the documentation for all configuration options:
3 # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
4
5 version: 2
6 updates:
7 - package-ecosystem: "pip" # See documentation for possible values
8 directory: "/" # Location of package manifests
9 schedule:
10 interval: "monthly"
0 repos:
1
2 # Automatic source code formatting
3 - repo: https://github.com/psf/black
4 rev: 20.8b1
5 hooks:
6 - id: black
7 args: [--safe, --quiet]
8
9 # Syntax check and some basic flake8
10 - repo: https://github.com/pre-commit/pre-commit-hooks
11 rev: v2.0.0
12 hooks:
13 - id: check-ast
14 - id: check-yaml
15 - id: flake8
16 args: ['--max-line-length=88', '--select=E401,E711,E712,E713,E714,E721,E722,E901,F401,F402,F403,F405,F631,F632,F633,F811,F812,F821,F822,F841,F901,W191,W291,W292,W293,W602,W603,W604,W605,W606']
17 - id: check-merge-conflict
+0
-5
.pyup.yml less more
0 # autogenerated pyup.io config file
1 # see https://pyup.io/docs/configuration/ for all available options
2
3 schedule: every month
4
44 matrix:
55 include:
66 - python: 3.8
7 dist: xenial
8 sudo: true
97 - python: 3.7
10 dist: xenial
11 sudo: true
128 - python: 3.6
13 - python: 3.5
14 - python: 2.7
15 - python: pypy
169 - os: osx
1710 language: generic
1811 env: CONDA=3.8 TOXENV=py38
2215 - os: osx
2316 language: generic
2417 env: CONDA=3.6 TOXENV=py36
25 - os: osx
26 language: generic
27 env: CONDA=3.5 TOXENV=py35
28 - os: osx
29 language: generic
30 env: CONDA=2.7 TOXENV=py27
3118
3219 allow_failures:
3320 - env: OPTIONAL=1
3724 before_install: |
3825 if [ ! -z "$CONDA" ]; then
3926 if [ "$TRAVIS_OS_NAME" == "osx" ]; then
40 curl https://repo.continuum.io/miniconda/Miniconda3-latest-MacOSX-x86_64.sh --output miniconda.sh
27 curl https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh --output miniconda.sh
4128 fi
4229 chmod +x miniconda.sh
4330 ./miniconda.sh -b
5441
5542 # Command to run tests, e.g. python setup.py test
5643 script: tox
57
58 # Assuming you have installed the travis-ci CLI tool, after you
59 # create the Github repo and add it to Travis, run the
60 # following command to finish PyPI deployment setup:
61 # $ travis encrypt --add deploy.password
62 deploy:
63 provider: pypi
64 distributions: sdist bdist_wheel
65 user: mgerstel
66 password:
67 secure: qN3EYVlH22eLPZp5CSvc5Bz8bpP0l0wBZ8tcYMa6kBrqelYdIgqkuZESR/oCMu1YBA3AbcKJNwMuzuWf8RNGAFebD820OJThdP4czRMCv6LDbWABnv12lFHQLQSW1fMQVvb2arjnE/Ew7BFq70p0wOlIJJRwu6CoeOXW/sMeVYMivxdaHmgORq3cdMluFAy4amVb3Fc8i7mxAM0QGklO7x/UWJR/IEpUk2RlUbXL+HrzNoEjRtDeMxoCR84gKZTjVeUQ/iIQSuWwxlt7v1FNENj6ZEbE7+PS8/ylIVfPufbCr8tEEv8W58QcxQ5xPJC2g85ulsN5dM9/9FekhpyKa25B/4wKUNq5T8rKJ8WZ6hMiGffW8rmAfrGTmrBaojKBi0pb9VfXJ5KXUcunVXwQaAn2L80jLLHNsAo94ZxeoowD1eJox9Wh1NtNc+NiUv8K6spOIBsur7G5GY4JVA/yZ7w+DweEfQEp8/SEdVhK0vEYSYT4FnJHuAAmNgeedyAtoes4+a5bYYUM4qrz2OC78NWQWAnnsZhD4Y/TulkavWSexVCqfSePRoK3gcCs+rXwiM69XkMbL1Wgj1gNou+XMkntayH2ZDHkkyJi5F7ls4nqMH5RON9FfVygJMoHDRqh0p4RV25IzJ4FSYqKihHNBO31/URnU2ihpW7n8kM+mbM=
68 on:
69 tags: true
70 repo: DiamondLightSource/python-procrunner
71 python: 3.8
100100 1. The pull request should include tests.
101101 2. If the pull request adds functionality, the docs should be updated. Put
102102 your new functionality into a function with a docstring, and add the
103 feature to the list in README.rst.
104 3. The pull request should work for Python 2.7, 3.5, 3.6, 3.7, 3.8, and for PyPy. Check
105 https://travis-ci.org/DiamondLightSource/python-procrunner/pull_requests
106 and make sure that the tests pass for all supported Python versions.
103 feature to the list in HISTORY.rst/README.rst.
107104
108105 Tips
109106 ----
11 History
22 =======
33
4 2.3.0 (2020-10-29)
5 ------------------
6 * Add Python 3.9 support, drop Python 3.5 support
7 * Fix a file descriptor leak on subprocess execution
8
9 2.2.0 (2020-09-07)
10 ------------------
11 * Calling the run() function with unnamed arguments (other than the command
12 list as the first argument) is now deprecated. As a number of arguments
13 will be removed in a future version the use of unnamed arguments will
14 cause future confusion. `Use explicit keyword arguments instead (#62). <https://github.com/DiamondLightSource/python-procrunner/pull/62>`_
15 * `The run() function debug argument has been deprecated (#63). <https://github.com/DiamondLightSource/python-procrunner/pull/63>`_
16 This is only used to debug the NonBlockingStream* classes. Those are due
17 to be replaced in a future release, so the argument will no longer serve
18 a purpose. Debugging information remains available via standard logging
19 mechanisms.
20 * Final version supporting Python 3.5
21
22 2.1.0 (2020-09-05)
23 ------------------
24 * `Deprecated array access on the return object (#60). <https://github.com/DiamondLightSource/python-procrunner/pull/60>`_
25 The return object will become a subprocess.CompletedProcess in a future
26 release, which no longer allows array-based access. For a translation table
27 of array elements to attributes please see the pull request linked above.
28 * Add a `new parameter 'raise_timeout_exception' (#61). <https://github.com/DiamondLightSource/python-procrunner/pull/61>`_
29 When set to 'True' a subprocess.TimeoutExpired exception is raised when the
30 process runtime exceeds the timeout threshold. This defaults to 'False' and
31 will be set to 'True' in a future release.
32
33 2.0.0 (2020-06-24)
34 ------------------
35 * Python 3.5+ only, support for Python 2.7 has been dropped
36 * Deprecated function alias run_process() has been removed
37 * Fixed a stability issue on Windows
38
439 1.1.0 (2019-11-04)
540 ------------------
6
741 * Add Python 3.8 support, drop Python 3.4 support
842
943 1.0.2 (2019-05-20)
1044 ------------------
11
1245 * Stop environment override variables leaking into the process environment
1346
1447 1.0.1 (2019-04-16)
1548 ------------------
16
1749 * Minor fixes on the return object (implement equality,
1850 mark as unhashable)
1951
2052 1.0.0 (2019-03-25)
2153 ------------------
22
2354 * Support file system path objects (PEP-519) in arguments
2455 * Change the return object to make it similar to
2556 subprocess.CompletedProcess, introduced with Python 3.5+
2657
2758 0.9.1 (2019-02-22)
2859 ------------------
29
3060 * Have deprecation warnings point to correct code locations
3161
3262 0.9.0 (2018-12-07)
3363 ------------------
34
3564 * Trap UnicodeEncodeError when printing output. Offending characters
3665 are replaced and a warning is logged once. Hints at incorrectly set
3766 PYTHONIOENCODING.
3867
3968 0.8.1 (2018-12-04)
4069 ------------------
41
4270 * Fix a few deprecation warnings
4371
4472 0.8.0 (2018-10-09)
4573 ------------------
46
4774 * Add parameter working_directory to set the working directory
4875 of the subprocess
4976
5077 0.7.2 (2018-10-05)
5178 ------------------
52
5379 * Officially support Python 3.7
5480
5581 0.7.1 (2018-09-03)
5682 ------------------
57
5883 * Accept environment variable overriding with numeric values.
5984
6085 0.7.0 (2018-05-13)
6186 ------------------
62
6387 * Unicode fixes. Fix crash on invalid UTF-8 input.
6488 * Clarify that stdout/stderr values are returned as bytestrings.
6589 * Callbacks receive the data decoded as UTF-8 unicode strings
6993
7094 0.6.1 (2018-05-02)
7195 ------------------
72
7396 * Maintenance release to add some tests for executable resolution.
7497
7598 0.6.0 (2018-05-02)
7699 ------------------
77
78100 * Fix Win32 API executable resolution for commands containing a dot ('.') in
79101 addition to a file extension (say '.bat').
80102
81103 0.5.1 (2018-04-27)
82104 ------------------
83
84105 * Fix Win32API dependency installation on Windows.
85106
86107 0.5.0 (2018-04-26)
87108 ------------------
88
89109 * New keyword 'win32resolve' which only takes effect on Windows and is enabled
90110 by default. This causes procrunner to call the Win32 API FindExecutable()
91111 function to try and lookup non-.exe files with the corresponding name. This
94114
95115 0.4.0 (2018-04-23)
96116 ------------------
97
98117 * Python 2.7 support on Windows. Python3 not yet supported on Windows.
99118
100119 0.3.0 (2018-04-17)
101120 ------------------
102
103121 * run_process() renamed to run()
104122 * Python3 compatibility fixes
105123
106124 0.2.0 (2018-03-12)
107125 ------------------
108
109126 * Procrunner is now Python3 3.3-3.6 compatible.
110127
111128 0.1.0 (2018-03-12)
112129 ------------------
113
114130 * First release on PyPI.
2222 :target: https://procrunner.readthedocs.io/en/latest/?badge=latest
2323 :alt: Documentation Status
2424
25 .. image:: https://pyup.io/repos/github/DiamondLightSource/python-procrunner/shield.svg
26 :target: https://pyup.io/repos/github/DiamondLightSource/python-procrunner/
27 :alt: Updates
28
2925 .. image:: https://img.shields.io/pypi/pyversions/procrunner.svg
3026 :target: https://pypi.python.org/pypi/procrunner
3127 :alt: Supported Python versions
4642 * runs an external process and waits for it to finish
4743 * does not deadlock, no matter the process stdout/stderr output behaviour
4844 * returns the exit code, stdout, stderr (separately, both as bytestrings),
49 and the total process runtime as a dictionary
45 as a subprocess.CompletedProcess object
5046 * process can run in a custom environment, either as a modification of
5147 the current environment or in a new environment from scratch
52 * stdin can be fed to the process, the returned dictionary contains
53 information how much was read by the process
48 * stdin can be fed to the process
5449 * stdout and stderr is printed by default, can be disabled
5550 * stdout and stderr can be passed to any arbitrary function for
5651 live processing (separately, both as unicode strings)
57 * optionally enforces a time limit on the process
52 * optionally enforces a time limit on the process, raising a
53 subprocess.TimeoutExpired exception if it is exceeded.
54
5855
5956 Credits
6057 -------
44 # For Python versions available on Appveyor, see
55 # http://www.appveyor.com/docs/installed-software#python
66
7 - PYTHON: "C:\\Python27"
8 - PYTHON: "C:\\Python35"
9 UNSTABLE: 1
107 - PYTHON: "C:\\Python36"
11 UNSTABLE: 1
128 - PYTHON: "C:\\Python37"
13 UNSTABLE: 1
149 - PYTHON: "C:\\Python38"
15 UNSTABLE: 1
16 - PYTHON: "C:\\Python27-x64"
17 - PYTHON: "C:\\Python35-x64"
18 UNSTABLE: 1
1910 - PYTHON: "C:\\Python36-x64"
20 UNSTABLE: 1
2111 - PYTHON: "C:\\Python37-x64"
22 UNSTABLE: 1
2312 - PYTHON: "C:\\Python38-x64"
24 UNSTABLE: 1
2513
2614 matrix:
2715 allow_failures:
3119 # Upgrade to the latest pip.
3220 - '%PYTHON%\\python.exe -m pip install -U pip setuptools wheel'
3321 - '%PYTHON%\\python.exe -m pip install -r requirements_dev.txt'
34 # Install win32api dependency. Must use the --only-binary switch explicitly
35 # on AppVeyor
36 - "%PYTHON%\\python.exe -m pip install --only-binary pywin32 pywin32"
3722
3823 build: off
3924
4126 # Note that you must use the environment variable %PYTHON% to refer to
4227 # the interpreter you're using - Appveyor does not do anything special
4328 # to put the Python version you want to use on PATH.
44 - "%PYTHON%\\python.exe setup.py test"
29 - "%PYTHON%\\python.exe -m pytest"
4530
4631 after_test:
4732 # This step builds your wheels.
0 python-procrunner (2.3.0-1) UNRELEASED; urgency=low
1
2 * New upstream release.
3
4 -- Debian Janitor <janitor@jelmer.uk> Fri, 17 Sep 2021 11:30:37 -0000
5
06 python-procrunner (1.1.0-1) unstable; urgency=medium
17
28 * First release (Closes: #962456)
00 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
21 #
32 # procrunner documentation build configuration file, created by
43 # sphinx-quickstart on Fri Jun 9 13:47:02 2017.
4746 master_doc = "index"
4847
4948 # General information about the project.
50 project = u"ProcRunner"
51 copyright = u"2018, Markus Gerstel"
52 author = u"Markus Gerstel"
49 project = "ProcRunner"
50 copyright = "2020, Diamond Light Source"
51 author = "Diamond Light Source - Scientific Software"
5352
5453 # The version info for the project you're documenting, acts as replacement
5554 # for |version| and |release|, also used in various other places throughout
128127 (
129128 master_doc,
130129 "procrunner.tex",
131 u"ProcRunner Documentation",
132 u"Markus Gerstel",
130 "procrunner Documentation",
131 "Diamond Light Source - Scientific Software",
133132 "manual",
134133 )
135134 ]
139138
140139 # One entry per manual page. List of tuples
141140 # (source start file, name, description, authors, manual section).
142 man_pages = [(master_doc, "procrunner", u"ProcRunner Documentation", [author], 1)]
141 man_pages = [(master_doc, "procrunner", "procrunner Documentation", [author], 1)]
143142
144143
145144 # -- Options for Texinfo output ----------------------------------------
151150 (
152151 master_doc,
153152 "procrunner",
154 u"ProcRunner Documentation",
153 "procrunner Documentation",
155154 author,
156155 "procrunner",
157 "One line description of project.",
156 "Versatile utility function to run external processes",
158157 "Miscellaneous",
159158 )
160159 ]
88
99 To test for successful completion::
1010
11 assert not result['exitcode']
12 assert result['exitcode'] == 0 # alternatively
11 assert not result.returncode
12 assert result.returncode == 0 # alternatively
13 result.check_returncode() # raises subprocess.CalledProcessError()
1314
1415 To test for no STDERR output::
1516
16 assert not result['stderr']
17 assert result['stderr'] == b'' # alternatively
17 assert not result.stderr
18 assert result.stderr == b'' # alternatively
1819
1920 To run with a specific environment variable set::
2021
0 # -*- coding: utf-8 -*-
1
2 from __future__ import absolute_import, division, print_function
3
40 import codecs
1 import functools
2 import io
53 import logging
64 import os
75 import select
8 import six
6 import shutil
97 import subprocess
108 import sys
119 import time
2018 #
2119 # - runs an external process and waits for it to finish
2220 # - does not deadlock, no matter the process stdout/stderr output behaviour
23 # - returns the exit code, stdout, stderr (separately), and the total process
24 # runtime as a dictionary
21 # - returns the exit code, stdout, stderr (separately) as a
22 # subprocess.CompletedProcess object
2523 # - process can run in a custom environment, either as a modification of
2624 # the current environment or in a new environment from scratch
27 # - stdin can be fed to the process, the returned dictionary contains
28 # information how much was read by the process
25 # - stdin can be fed to the process
2926 # - stdout and stderr is printed by default, can be disabled
3027 # - stdout and stderr can be passed to any arbitrary function for
3128 # live processing
3936 #
4037 # Returns:
4138 #
42 # {'command': ['/bin/ls', '/some/path/containing spaces'],
43 # 'exitcode': 2,
44 # 'runtime': 0.12990689277648926,
45 # 'stderr': '/bin/ls: cannot access /some/path/containing spaces: No such file or directory\n',
46 # 'stdout': '',
47 # 'time_end': '2017-11-12 19:54:49 GMT',
48 # 'time_start': '2017-11-12 19:54:49 GMT',
49 # 'timeout': False}
50 #
39 # ReturnObject(
40 # args=('/bin/ls', '/some/path/containing spaces'),
41 # returncode=2,
42 # stdout=b'',
43 # stderr=b'/bin/ls: cannot access /some/path/containing spaces: No such file or directory\n'
44 # )
45 #
46 # which also offers (albeit deprecated)
47 #
48 # result.runtime == 0.12990689277648926
49 # result.time_end == '2017-11-12 19:54:49 GMT'
50 # result.time_start == '2017-11-12 19:54:49 GMT'
51 # result.timeout == False
5152
5253 __author__ = """Markus Gerstel"""
5354 __email__ = "scientificsoftware@diamond.ac.uk"
54 __version__ = "1.1.0"
55 __version__ = "2.3.0"
5556
5657 logger = logging.getLogger("procrunner")
5758 logger.addHandler(logging.NullHandler())
5859
5960
60 class _LineAggregator(object):
61 class _LineAggregator:
6162 """
6263 Buffer that can be filled with stream data and will aggregate complete
6364 lines. Lines can be printed or passed to an arbitrary callback function.
106107 self._buffer = ""
107108
108109
109 class _NonBlockingStreamReader(object):
110 class _NonBlockingStreamReader:
110111 """Reads a stream in a thread to avoid blocking/deadlocks"""
111112
112113 def __init__(self, stream, output=True, debug=False, notify=None, callback=None):
113114 """Creates and starts a thread which reads from a stream."""
114 self._buffer = six.BytesIO()
115 self._buffer = io.BytesIO()
115116 self._closed = False
116117 self._closing = False
117118 self._debug = debug
130131 else:
131132 if self._closing:
132133 break
134 self._stream.close()
133135 self._terminated = True
134136 la.flush()
135137 if self._debug:
149151 print(linedecode)
150152 if callback:
151153 callback(linedecode)
154 self._stream.close()
152155 self._terminated = True
153156 if self._debug:
154157 logger.debug("Stream reader terminated")
183186 if not self.has_finished():
184187 if self._debug:
185188 logger.debug(
186 "NBSR join after %f seconds, underrun not resolved"
187 % (timeit.default_timer() - underrun_debug_timer)
189 "NBSR join after %f seconds, underrun not resolved",
190 timeit.default_timer() - underrun_debug_timer,
188191 )
189192 raise Exception("thread did not terminate")
190193 if self._debug:
191194 logger.debug(
192 "NBSR underrun resolved after %f seconds"
193 % (timeit.default_timer() - underrun_debug_timer)
195 "NBSR underrun resolved after %f seconds",
196 timeit.default_timer() - underrun_debug_timer,
194197 )
195198 if self._closed:
196199 raise Exception("streamreader double-closed")
200203 return data
201204
202205
203 class _NonBlockingStreamWriter(object):
206 class _NonBlockingStreamWriter:
204207 """Writes to a stream in a thread to avoid blocking/deadlocks"""
205208
206209 def __init__(self, stream, data, debug=False, notify=None):
208211 self._buffer = data
209212 self._buffer_len = len(data)
210213 self._buffer_pos = 0
211 self._debug = debug
212214 self._max_block_len = 4096
213215 self._stream = stream
214216 self._terminated = False
223225 block = self._buffer[self._buffer_pos :]
224226 try:
225227 self._stream.write(block)
226 except IOError as e:
228 except OSError as e:
227229 if (
228230 e.errno == 32
229231 ): # broken pipe, ie. process terminated without reading entire stdin
235237 raise
236238 self._buffer_pos += len(block)
237239 if debug:
238 logger.debug("wrote %d bytes to stream" % len(block))
240 logger.debug("wrote %d bytes to stream", len(block))
239241 self._stream.close()
240242 self._terminated = True
241243 if notify:
272274 return obj
273275
274276
275 def _windows_resolve(command):
277 def _windows_resolve(command, path=None):
276278 """
277279 Try and find the full path and file extension of the executable to run.
278280 This is so that e.g. calls to 'somescript' will point at 'somescript.cmd'
279281 without the need to set shell=True in the subprocess.
280 If the executable contains periods it is a special case. Here the
281 win32api call will fail to resolve the extension automatically, and it
282 has do be done explicitly.
283282
284283 :param command: The command array to be run, with the first element being
285284 the command with or w/o path, with or w/o extension.
287286 correct extension. If the executable cannot be resolved for any
288287 reason the original command array is returned.
289288 """
290 try:
291 import win32api
292 except ImportError:
293 if (2, 8) < sys.version_info < (3, 5):
294 logger.info(
295 "Resolving executable names only supported on Python 2.7 and 3.5+"
296 )
297 else:
298 logger.warning(
299 "Could not resolve executable name: package win32api missing"
300 )
289 if not command or not isinstance(command[0], str):
301290 return command
302291
303 if not command or not isinstance(command[0], six.string_types):
304 return command
305
306 try:
307 _, found_executable = win32api.FindExecutable(command[0])
292 found_executable = shutil.which(command[0], path=path)
293 if found_executable:
308294 logger.debug("Resolved %s as %s", command[0], found_executable)
309 return (found_executable,) + tuple(command[1:])
310 except Exception as e:
311 if not hasattr(e, "winerror"):
312 raise
313 # Keep this error message for later in case we fail to resolve the name
314 logwarning = getattr(e, "strerror", str(e))
315
316 if "." in command[0]:
317 # Special case. The win32api may not properly check file extensions, so
318 # try to resolve the executable explicitly.
295 return (found_executable, *command[1:])
296
297 if "\\" in command[0]:
298 # Special case. shutil.which may not detect file extensions if a full
299 # path is given, so try to resolve the executable explicitly
319300 for extension in os.getenv("PATHEXT").split(os.pathsep):
320 try:
321 _, found_executable = win32api.FindExecutable(command[0] + extension)
322 logger.debug("Resolved %s as %s", command[0], found_executable)
323 return (found_executable,) + tuple(command[1:])
324 except Exception as e:
325 if not hasattr(e, "winerror"):
326 raise
327
328 logger.warning("Error trying to resolve the executable: %s", logwarning)
301 found_executable = shutil.which(command[0] + extension, path=path)
302 if found_executable:
303 return (found_executable, *command[1:])
304
305 logger.warning("Error trying to resolve the executable: %s", command[0])
329306 return command
330307
331308
332 if sys.version_info < (3, 5):
333
334 class _ReturnObjectParent(object):
335 def check_returncode(self):
336 if self.returncode:
337 raise Exception(
338 "Call %r resulted in non-zero exit code %r"
339 % (self.args, self.returncode)
340 )
341
342
343 else:
344 _ReturnObjectParent = subprocess.CompletedProcess
345
346
347 class ReturnObject(dict, _ReturnObjectParent):
309 class ReturnObject(subprocess.CompletedProcess):
348310 """
349311 A subprocess.CompletedProcess-like object containing the executed
350312 command, stdout and stderr (both as bytestrings), and the exitcode.
351 Further values such as process runtime can be accessed as dictionary
352 values.
353313 The check_returncode() function raises an exception if the process
354314 exited with a non-zero exit code.
355315 """
356316
357 def __init__(self, *arg, **kw):
358 super(ReturnObject, self).__init__(*arg, **kw)
359 self.args = self["command"]
360 self.returncode = self["exitcode"]
361 self.stdout = self["stdout"]
362 self.stderr = self["stderr"]
317 def __init__(self, exitcode=None, command=None, stdout=None, stderr=None, **kw):
318 super().__init__(
319 args=command, returncode=exitcode, stdout=stdout, stderr=stderr
320 )
321 self._extras = {
322 "timeout": kw.get("timeout"),
323 "runtime": kw.get("runtime"),
324 "time_start": kw.get("time_start"),
325 "time_end": kw.get("time_end"),
326 }
327
328 def __getitem__(self, key):
329 warnings.warn(
330 "dictionary access to a procrunner return object is deprecated",
331 DeprecationWarning,
332 stacklevel=2,
333 )
334 if key in self._extras:
335 return self._extras[key]
336 if not hasattr(self, key):
337 raise KeyError(f"Unknown attribute {key}")
338 return getattr(self, key)
363339
364340 def __eq__(self, other):
365341 """Override equality operator to account for added fields"""
371347 """This object is not immutable, so mark it as unhashable"""
372348 return None
373349
374 def __ne__(self, other):
375 """Overrides the default implementation (unnecessary in Python 3)"""
376 return not self.__eq__(other)
377
378
350 @property
351 def cmd(self):
352 warnings.warn(
353 "procrunner return object .cmd is deprecated, use .args",
354 DeprecationWarning,
355 stacklevel=2,
356 )
357 return self.args
358
359 @property
360 def command(self):
361 warnings.warn(
362 "procrunner return object .command is deprecated, use .args",
363 DeprecationWarning,
364 stacklevel=2,
365 )
366 return self.args
367
368 @property
369 def exitcode(self):
370 warnings.warn(
371 "procrunner return object .exitcode is deprecated, use .returncode",
372 DeprecationWarning,
373 stacklevel=2,
374 )
375 return self.returncode
376
377 @property
378 def timeout(self):
379 warnings.warn(
380 "procrunner return object .timeout is deprecated",
381 DeprecationWarning,
382 stacklevel=2,
383 )
384 return self._extras["timeout"]
385
386 @property
387 def runtime(self):
388 warnings.warn(
389 "procrunner return object .runtime is deprecated",
390 DeprecationWarning,
391 stacklevel=2,
392 )
393 return self._extras["runtime"]
394
395 @property
396 def time_start(self):
397 warnings.warn(
398 "procrunner return object .time_start is deprecated",
399 DeprecationWarning,
400 stacklevel=2,
401 )
402 return self._extras["time_start"]
403
404 @property
405 def time_end(self):
406 warnings.warn(
407 "procrunner return object .time_end is deprecated",
408 DeprecationWarning,
409 stacklevel=2,
410 )
411 return self._extras["time_end"]
412
413 def update(self, dictionary):
414 self._extras.update(dictionary)
415
416
417 def _deprecate_argument_calling(f):
418 @functools.wraps(f)
419 def wrapper(*args, **kwargs):
420 if len(args) > 1:
421 warnings.warn(
422 "Calling procrunner.run() with unnamed arguments (apart from "
423 "the command) is deprecated. Use keyword arguments instead.",
424 DeprecationWarning,
425 stacklevel=2,
426 )
427 return f(*args, **kwargs)
428
429 return wrapper
430
431
432 @_deprecate_argument_calling
379433 def run(
380434 command,
381435 timeout=None,
382 debug=False,
436 debug=None,
383437 stdin=None,
384438 print_stdout=True,
385439 print_stderr=True,
389443 environment_override=None,
390444 win32resolve=True,
391445 working_directory=None,
446 raise_timeout_exception=False,
392447 ):
393448 """
394449 Run an external process.
398453
399454 :param array command: Command line to be run, specified as array.
400455 :param timeout: Terminate program execution after this many seconds.
401 :param boolean debug: Enable further debug messages.
402 :param stdin: Optional string that is passed to command stdin.
456 :param boolean debug: Enable further debug messages. (deprecated)
457 :param stdin: Optional bytestring that is passed to command stdin.
403458 :param boolean print_stdout: Pass stdout through to sys.stdout.
404459 :param boolean print_stderr: Pass stderr through to sys.stderr.
405460 :param callback_stdout: Optional function which is called for each
415470 extension.
416471 :param string working_directory: If specified, run the executable from
417472 within this working directory.
418 :return: A ReturnObject() containing the executed command, stdout and stderr
419 (both as bytestrings), and the exitcode. Further values such as
420 process runtime can be accessed as dictionary values.
473 :param boolean raise_timeout_exception: Forward compatibility flag. If set
474 then a subprocess.TimeoutExpired exception is raised
475 instead of returning an object that can be checked
476 for a timeout condition. Defaults to False, will be
477 changed to True in a future release.
478 :return: The exit code, stdout, stderr (separately, as byte strings)
479 as a subprocess.CompletedProcess object.
421480 """
422481
423482 time_start = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime())
428487 else:
429488 assert sys.platform != "win32", "stdin argument not supported on Windows"
430489 stdin_pipe = subprocess.PIPE
490 if debug is not None:
491 warnings.warn(
492 "Use of the debug parameter is deprecated", DeprecationWarning, stacklevel=3
493 )
431494
432495 start_time = timeit.default_timer()
433496 if timeout is not None:
434497 max_time = start_time + timeout
498 if not raise_timeout_exception:
499 warnings.warn(
500 "Using procrunner with timeout and without raise_timeout_exception set is deprecated",
501 DeprecationWarning,
502 stacklevel=3,
503 )
435504
436505 if environment is not None:
437506 env = {key: _path_resolve(environment[key]) for key in environment}
448517 command = tuple(_path_resolve(part) for part in command)
449518 if win32resolve and sys.platform == "win32":
450519 command = _windows_resolve(command)
520 if working_directory and sys.version_info < (3, 7):
521 working_directory = os.fspath(working_directory)
451522
452523 p = subprocess.Popen(
453524 command,
454525 shell=False,
455 cwd=_path_resolve(working_directory),
526 cwd=working_directory,
456527 env=env,
457528 stdin=stdin_pipe,
458529 stdout=subprocess.PIPE,
491562 (timeout is None) or (timeit.default_timer() < max_time)
492563 ):
493564 if debug and timeout is not None:
494 logger.debug("still running (T%.2fs)" % (timeit.default_timer() - max_time))
565 logger.debug("still running (T%.2fs)", timeit.default_timer() - max_time)
495566
496567 # wait for some time or until a stream is closed
497568 try:
500571 # which could indicate that the process has terminated.
501572 try:
502573 event = thread_pipe_pool[0].poll(0.5)
503 except IOError as e:
504 # on Windows this raises "IOError: [Errno 109] The pipe has been ended"
574 except BrokenPipeError as e:
575 # on Windows this raises "BrokenPipeError: [Errno 109] The pipe has been ended"
505576 # which is for all intents and purposes equivalent to a True return value.
506 if e.errno != 109:
577 if e.winerror != 109:
507578 raise
508579 event = True
509580 if event:
525596 # timeout condition
526597 timeout_encountered = True
527598 if debug:
528 logger.debug("timeout (T%.2fs)" % (timeit.default_timer() - max_time))
599 logger.debug("timeout (T%.2fs)", timeit.default_timer() - max_time)
529600
530601 # send terminate signal and wait some time for buffers to be read
531602 p.terminate()
532603 if thread_pipe_pool:
533 thread_pipe_pool[0].poll(0.5)
604 try:
605 thread_pipe_pool[0].poll(0.5)
606 except BrokenPipeError as e:
607 # on Windows this raises "BrokenPipeError: [Errno 109] The pipe has been ended"
608 # which is for all intents and purposes equivalent to a True return value.
609 if e.winerror != 109:
610 raise
611 thread_pipe_pool.pop(0)
534612 if not stdout.has_finished() or not stderr.has_finished():
535613 time.sleep(2)
536614 p.poll()
540618 # send kill signal and wait some more time for buffers to be read
541619 p.kill()
542620 if thread_pipe_pool:
543 thread_pipe_pool[0].poll(0.5)
621 try:
622 thread_pipe_pool[0].poll(0.5)
623 except BrokenPipeError as e:
624 # on Windows this raises "BrokenPipeError: [Errno 109] The pipe has been ended"
625 # which is for all intents and purposes equivalent to a True return value.
626 if e.winerror != 109:
627 raise
628 thread_pipe_pool.pop(0)
544629 if not stdout.has_finished() or not stderr.has_finished():
545630 time.sleep(5)
546631 p.poll()
551636 runtime = timeit.default_timer() - start_time
552637 if timeout is not None:
553638 logger.debug(
554 "Process ended after %.1f seconds with exit code %d (T%.2fs)"
555 % (runtime, p.returncode, timeit.default_timer() - max_time)
639 "Process ended after %.1f seconds with exit code %d (T%.2fs)",
640 runtime,
641 p.returncode,
642 timeit.default_timer() - max_time,
556643 )
557644 else:
558645 logger.debug(
559 "Process ended after %.1f seconds with exit code %d"
560 % (runtime, p.returncode)
646 "Process ended after %.1f seconds with exit code %d", runtime, p.returncode
561647 )
562648
563649 stdout = stdout.get_output()
564650 stderr = stderr.get_output()
651
652 if timeout_encountered and raise_timeout_exception:
653 raise subprocess.TimeoutExpired(
654 cmd=command, timeout=timeout, output=stdout, stderr=stderr
655 )
656
565657 time_end = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime())
566
567658 result = ReturnObject(
568 {
569 "exitcode": p.returncode,
570 "command": command,
571 "stdout": stdout,
572 "stderr": stderr,
573 "timeout": timeout_encountered,
574 "runtime": runtime,
575 "time_start": time_start,
576 "time_end": time_end,
577 }
659 exitcode=p.returncode,
660 command=command,
661 stdout=stdout,
662 stderr=stderr,
663 timeout=timeout_encountered,
664 runtime=runtime,
665 time_start=time_start,
666 time_end=time_end,
578667 )
579668 if stdin is not None:
580669 result.update(
585674 )
586675
587676 return result
588
589
590 def run_process_dummy(command, **kwargs):
591 """
592 A stand-in function that returns a valid result dictionary indicating a
593 successful execution. The external process is not run.
594 """
595 warnings.warn(
596 "procrunner.run_process_dummy() is deprecated", DeprecationWarning, stacklevel=2
597 )
598
599 time_start = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime())
600 logger.info("run_process is disabled. Requested command: %s", command)
601
602 result = ReturnObject(
603 {
604 "exitcode": 0,
605 "command": command,
606 "stdout": "",
607 "stderr": "",
608 "timeout": False,
609 "runtime": 0,
610 "time_start": time_start,
611 "time_end": time_start,
612 }
613 )
614 if kwargs.get("stdin") is not None:
615 result.update(
616 {"stdin_bytes_sent": len(kwargs["stdin"]), "stdin_bytes_remain": 0}
617 )
618 return result
619
620
621 def run_process(*args, **kwargs):
622 """API used up to version 0.2.0."""
623 warnings.warn(
624 "procrunner.run_process() is deprecated and has been renamed to run()",
625 DeprecationWarning,
626 stacklevel=2,
627 )
628 return run(*args, **kwargs)
0 [pytest]
1 addopts = -ra
2 junit_family=xunit2
(New empty file)
0 bump2version==0.5.10
1 coverage==4.5.4
2 flake8==3.7.8
3 mock==3.0.5
4 pip==19.1.1
5 pytest==4.5.0 # pyup: <5.0 # for Python 2.7 support
6 pytest-runner==5.1
7 six==1.12.0
8 Sphinx==1.8.5 # pyup: <2.0 # for Python 2.7 support
9 tox==3.13.1
10 twine==1.13.0
11 watchdog==0.9.0
12 wheel==0.33.4
0 bump2version==1.0.1
1 coverage==5.3
2 flake8==3.8.4
3 pip==20.2.4
4 pytest==6.1.2
5 Sphinx==3.2.1
6 tox==3.20.1
7 twine==1.15.0
8 wheel==0.35.1
0 [bumpversion]
1 current_version = 1.1.0
2 commit = True
3 tag = True
4
5 [bumpversion:file:setup.py]
6 search = version="{current_version}"
7 replace = version="{new_version}"
8
9 [bumpversion:file:procrunner/__init__.py]
10 search = __version__ = "{current_version}"
11 replace = __version__ = "{new_version}"
12
13 [bdist_wheel]
14 universal = 1
0 [metadata]
1 project-urls =
2 Documentation = https://procrunner.readthedocs.io/
3 GitHub = https://github.com/DiamondLightSource/python-procrunner
4 Bug-Tracker = https://github.com/DiamondLightSource/python-procrunner/issues
155
166 [flake8]
177 exclude = docs
00 #!/usr/bin/env python
11 # -*- coding: utf-8 -*-
22
3 import sys
43 from setuptools import setup, find_packages
54
65 with open("README.rst") as readme_file:
98 with open("HISTORY.rst") as history_file:
109 history = history_file.read()
1110
12 requirements = [
13 "six",
14 'pywin32; sys_platform=="win32"',
15 ]
11 requirements = []
1612
1713 setup_requirements = []
18 needs_pytest = {"pytest", "test", "ptr"}.intersection(sys.argv)
19 if needs_pytest:
20 setup_requirements.append("pytest-runner")
2114
22 test_requirements = ["mock", "pytest"]
15 test_requirements = ["pytest"]
2316
2417 setup(
2518 author="Markus Gerstel",
3023 "License :: OSI Approved :: BSD License",
3124 "Natural Language :: English",
3225 "Operating System :: OS Independent",
33 "Programming Language :: Python :: 2",
34 "Programming Language :: Python :: 2.7",
3526 "Programming Language :: Python :: 3",
36 "Programming Language :: Python :: 3.5",
3727 "Programming Language :: Python :: 3.6",
3828 "Programming Language :: Python :: 3.7",
3929 "Programming Language :: Python :: 3.8",
40 "Programming Language :: Python :: Implementation :: PyPy",
41 "Programming Language :: Python :: Implementation :: CPython",
30 "Programming Language :: Python :: 3.9",
4231 "Topic :: Software Development :: Libraries :: Python Modules",
4332 ],
4433 description="Versatile utility function to run external processes",
4938 keywords="procrunner",
5039 name="procrunner",
5140 packages=find_packages(include=["procrunner"]),
41 python_requires=">=3.6",
5242 setup_requires=setup_requirements,
5343 test_suite="tests",
5444 tests_require=test_requirements,
5545 url="https://github.com/DiamondLightSource/python-procrunner",
56 version="1.1.0",
46 version="2.3.0",
5747 zip_safe=False,
5848 )
0 from __future__ import absolute_import, division, print_function
1
20 import copy
3 import mock
1 from unittest import mock
42 import os
3 import pathlib
54 import procrunner
65 import pytest
76 import sys
7
8
9 @mock.patch("procrunner._NonBlockingStreamReader")
10 @mock.patch("procrunner.time")
11 @mock.patch("procrunner.subprocess")
12 @mock.patch("procrunner.Pipe")
13 def test_run_command_aborts_after_timeout_legacy(
14 mock_pipe, mock_subprocess, mock_time, mock_streamreader
15 ):
16 mock_pipe.return_value = mock.Mock(), mock.Mock()
17 mock_process = mock.Mock()
18 mock_process.returncode = None
19 mock_subprocess.Popen.return_value = mock_process
20 task = ["___"]
21
22 with pytest.raises(RuntimeError):
23 with pytest.warns(DeprecationWarning, match="timeout"):
24 procrunner.run(task, timeout=-1, debug=False)
25
26 assert mock_subprocess.Popen.called
27 assert mock_process.terminate.called
28 assert mock_process.kill.called
829
930
1031 @mock.patch("procrunner._NonBlockingStreamReader")
2142 task = ["___"]
2243
2344 with pytest.raises(RuntimeError):
24 procrunner.run(task, -1, False)
45 procrunner.run(task, timeout=-1, raise_timeout_exception=True)
2546
2647 assert mock_subprocess.Popen.called
2748 assert mock_process.terminate.called
6283
6384 actual = procrunner.run(
6485 command,
65 0.5,
66 False,
86 timeout=0.5,
6787 callback_stdout=mock.sentinel.callback_stdout,
6888 callback_stderr=mock.sentinel.callback_stderr,
69 working_directory=mock.sentinel.cwd,
89 working_directory=pathlib.Path("somecwd"),
90 raise_timeout_exception=True,
7091 )
7192
7293 assert mock_subprocess.Popen.called
7394 assert mock_subprocess.Popen.call_args[1]["env"] == os.environ
74 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 )
7599 mock_streamreader.assert_has_calls(
76100 [
77101 mock.call(
78102 stream_stdout,
79103 output=mock.ANY,
80 debug=mock.ANY,
104 debug=None,
81105 notify=mock.ANY,
82106 callback=mock.sentinel.callback_stdout,
83107 ),
84108 mock.call(
85109 stream_stderr,
86110 output=mock.ANY,
87 debug=mock.ANY,
111 debug=None,
88112 notify=mock.ANY,
89113 callback=mock.sentinel.callback_stderr,
90114 ),
94118 assert not mock_process.terminate.called
95119 assert not mock_process.kill.called
96120 for key in expected:
97 assert actual[key] == expected[key]
121 with pytest.warns(DeprecationWarning):
122 assert actual[key] == expected[key]
98123 assert actual.args == tuple(command)
99124 assert actual.returncode == mock_process.returncode
100125 assert actual.stdout == mock.sentinel.proc_stdout
105130 def test_default_process_environment_is_parent_environment(mock_subprocess):
106131 mock_subprocess.Popen.side_effect = NotImplementedError() # cut calls short
107132 with pytest.raises(NotImplementedError):
108 procrunner.run([mock.Mock()], -1, False)
133 procrunner.run([mock.Mock()], timeout=-1, raise_timeout_exception=True)
109134 assert mock_subprocess.Popen.call_args[1]["env"] == os.environ
135
136
137 @mock.patch("procrunner.subprocess")
138 def test_using_debug_parameter_raises_warning(mock_subprocess):
139 mock_subprocess.Popen.side_effect = NotImplementedError() # cut calls short
140 with pytest.warns(DeprecationWarning, match="debug"):
141 with pytest.raises(NotImplementedError):
142 procrunner.run([mock.Mock()], debug=True)
143 with pytest.warns(DeprecationWarning, match="debug"):
144 with pytest.raises(NotImplementedError):
145 procrunner.run([mock.Mock()], debug=False)
110146
111147
112148 @mock.patch("procrunner.subprocess")
115151 mock_env = {"key": mock.sentinel.key}
116152 # Pass an environment dictionary
117153 with pytest.raises(NotImplementedError):
118 procrunner.run([mock.Mock()], -1, False, environment=copy.copy(mock_env))
154 procrunner.run(
155 [mock.Mock()],
156 timeout=-1,
157 environment=copy.copy(mock_env),
158 raise_timeout_exception=True,
159 )
119160 assert mock_subprocess.Popen.call_args[1]["env"] == mock_env
120161
121162
128169 with pytest.raises(NotImplementedError):
129170 procrunner.run(
130171 [mock.Mock()],
131 -1,
132 False,
172 timeout=-1,
133173 environment=copy.copy(mock_env1),
134174 environment_override=copy.copy(mock_env2),
175 raise_timeout_exception=True,
135176 )
136177 mock_env_sum = copy.copy(mock_env1)
137178 mock_env_sum.update({key: str(mock_env2[key]) for key in mock_env2})
144185 mock_env2 = {"keyB": str(mock.sentinel.keyB)}
145186 with pytest.raises(NotImplementedError):
146187 procrunner.run(
147 [mock.Mock()], -1, False, environment_override=copy.copy(mock_env2)
188 [mock.Mock()],
189 timeout=-1,
190 environment_override=copy.copy(mock_env2),
191 raise_timeout_exception=True,
148192 )
149193 random_environment_variable = list(os.environ)[0]
150194 if random_environment_variable == list(mock_env2)[0]:
151195 random_environment_variable = list(os.environ)[1]
152 random_environment_value = os.getenv(random_environment_variable)
153196 assert (
154197 random_environment_variable
155198 and random_environment_variable != list(mock_env2)[0]
171214 with pytest.raises(NotImplementedError):
172215 procrunner.run(
173216 [mock.Mock()],
174 -1,
175 False,
217 timeout=-1,
176218 environment_override={
177219 random_environment_variable: "X" + random_environment_value
178220 },
221 raise_timeout_exception=True,
179222 )
180223 assert (
181224 mock_subprocess.Popen.call_args[1]["env"][random_environment_variable]
191234 def test_nonblockingstreamreader_can_read(mock_select):
192235 import time
193236
194 class _stream(object):
237 class _stream:
195238 def __init__(self):
196239 self.data = b""
197240 self.closed = False
262305
263306 def test_return_object_semantics():
264307 ro = procrunner.ReturnObject(
265 {
266 "command": mock.sentinel.command,
267 "exitcode": 0,
268 "stdout": mock.sentinel.stdout,
269 "stderr": mock.sentinel.stderr,
270 }
271 )
272 assert ro["command"] == mock.sentinel.command
308 command=mock.sentinel.command,
309 exitcode=0,
310 stdout=mock.sentinel.stdout,
311 stderr=mock.sentinel.stderr,
312 )
313 with pytest.warns(DeprecationWarning):
314 assert ro["command"] == mock.sentinel.command
273315 assert ro.args == mock.sentinel.command
274 assert ro["exitcode"] == 0
316 with pytest.warns(DeprecationWarning):
317 assert ro["exitcode"] == 0
275318 assert ro.returncode == 0
276 assert ro["stdout"] == mock.sentinel.stdout
319 with pytest.warns(DeprecationWarning):
320 assert ro["stdout"] == mock.sentinel.stdout
277321 assert ro.stdout == mock.sentinel.stdout
278 assert ro["stderr"] == mock.sentinel.stderr
322 with pytest.warns(DeprecationWarning):
323 assert ro["stderr"] == mock.sentinel.stderr
279324 assert ro.stderr == mock.sentinel.stderr
280325
281326 with pytest.raises(KeyError):
282 ro["unknownkey"]
327 with pytest.warns(DeprecationWarning):
328 ro["unknownkey"]
283329 ro.update({"unknownkey": mock.sentinel.key})
284 assert ro["unknownkey"] == mock.sentinel.key
330 with pytest.warns(DeprecationWarning):
331 assert ro["unknownkey"] == mock.sentinel.key
285332
286333
287334 def test_return_object_check_function_passes_on_success():
288335 ro = procrunner.ReturnObject(
289 {
290 "command": mock.sentinel.command,
291 "exitcode": 0,
292 "stdout": mock.sentinel.stdout,
293 "stderr": mock.sentinel.stderr,
294 }
336 command=mock.sentinel.command,
337 exitcode=0,
338 stdout=mock.sentinel.stdout,
339 stderr=mock.sentinel.stderr,
295340 )
296341 ro.check_returncode()
297342
298343
299344 def test_return_object_check_function_raises_on_error():
300345 ro = procrunner.ReturnObject(
301 {
302 "command": mock.sentinel.command,
303 "exitcode": 1,
304 "stdout": mock.sentinel.stdout,
305 "stderr": mock.sentinel.stderr,
306 }
346 command=mock.sentinel.command,
347 exitcode=1,
348 stdout=mock.sentinel.stdout,
349 stderr=mock.sentinel.stderr,
307350 )
308351 with pytest.raises(Exception) as e:
309352 ro.check_returncode()
0 from __future__ import absolute_import, division, print_function
1
20 import os
31 import sys
42
5 import mock
63 import procrunner
74 import pytest
85
4643
4744
4845 @pytest.mark.skipif(sys.platform != "win32", reason="windows specific test only")
49 def test_pywin32_import():
50 import win32api
51
52
53 @pytest.mark.skipif(sys.platform != "win32", reason="windows specific test only")
5446 def test_name_resolution_for_simple_exe():
5547 command = ["cmd.exe", "/c", "echo", "hello"]
5648
6557
6658
6759 @pytest.mark.skipif(sys.platform != "win32", reason="windows specific test only")
68 def test_name_resolution_for_complex_cases(tmpdir):
69 tmpdir.chdir()
70
60 def test_name_resolution_for_complex_cases(tmp_path):
7161 bat = "simple_bat_extension"
7262 cmd = "simple_cmd_extension"
7363 exe = "simple_exe_extension"
7464 dotshort = "more_complex_filename_with_a.dot"
7565 dotlong = "more_complex_filename.withadot"
7666
77 (tmpdir / bat + ".bat").ensure()
78 (tmpdir / cmd + ".cmd").ensure()
79 (tmpdir / exe + ".exe").ensure()
80 (tmpdir / dotshort + ".bat").ensure()
81 (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()
8272
8373 def is_valid(command):
8474 assert len(command) == 1
85 assert os.path.exists(command[0])
75 assert os.path.exists(tmp_path / command[0])
8676
87 is_valid(procrunner._windows_resolve([bat]))
88 is_valid(procrunner._windows_resolve([cmd]))
89 is_valid(procrunner._windows_resolve([exe]))
90 is_valid(procrunner._windows_resolve([dotshort]))
91 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)))
0 from __future__ import absolute_import, division, print_function
1
20 import os
1 import subprocess
32 import sys
3 import timeit
44
55 import procrunner
66 import pytest
3535 else:
3636 assert result.stdout == test_string
3737 out, err = capsys.readouterr()
38 assert out == u"test\ufffdstring\n"
39 assert err == u""
38 assert out == "test\ufffdstring\n"
39 assert err == ""
4040
4141
42 def test_running_wget(tmpdir):
43 tmpdir.chdir()
42 def test_running_wget(tmp_path):
4443 command = ["wget", "https://www.google.com", "-O", "-"]
4544 try:
46 result = procrunner.run(command)
45 result = procrunner.run(command, working_directory=tmp_path)
4746 except OSError as e:
4847 if e.errno == 2:
4948 pytest.skip("wget not available")
5352 assert b"google" in result.stdout
5453
5554
56 def test_path_object_resolution(tmpdir):
55 def test_path_object_resolution(tmp_path):
5756 sentinel_value = b"sentinel"
58 tmpdir.join("tempfile").write(sentinel_value)
59 tmpdir.join("reader.py").write("print(open('tempfile').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 )
6061 assert "LEAK_DETECTOR" not in os.environ
6162 result = procrunner.run(
62 [sys.executable, tmpdir.join("reader.py")],
63 [sys.executable, tmp_path / "reader.py"],
6364 environment_override={"PYTHONHASHSEED": "random", "LEAK_DETECTOR": "1"},
64 working_directory=tmpdir,
65 working_directory=tmp_path,
6566 )
6667 assert result.returncode == 0
6768 assert not result.stderr
6970 assert (
7071 "LEAK_DETECTOR" not in os.environ
7172 ), "overridden environment variable leaked into parent process"
73
74
75 def test_timeout_behaviour_legacy(tmp_path):
76 start = timeit.default_timer()
77 try:
78 with pytest.warns(DeprecationWarning, match="timeout"):
79 result = procrunner.run(
80 [sys.executable, "-c", "import time; time.sleep(5)"],
81 timeout=0.1,
82 working_directory=tmp_path,
83 raise_timeout_exception=False,
84 )
85 except RuntimeError:
86 # This test sometimes fails with a RuntimeError.
87 runtime = timeit.default_timer() - start
88 assert runtime < 3
89 return
90 runtime = timeit.default_timer() - start
91 with pytest.warns(DeprecationWarning, match="\\.timeout"):
92 assert result.timeout
93 assert runtime < 3
94 assert not result.stdout
95 assert not result.stderr
96 assert result.returncode
97
98
99 def test_timeout_behaviour(tmp_path):
100 command = (sys.executable, "-c", "import time; time.sleep(5)")
101 start = timeit.default_timer()
102 try:
103 with pytest.raises(subprocess.TimeoutExpired) as te:
104 procrunner.run(
105 command,
106 timeout=0.1,
107 working_directory=tmp_path,
108 raise_timeout_exception=True,
109 )
110 except RuntimeError:
111 # This test sometimes fails with a RuntimeError.
112 runtime = timeit.default_timer() - start
113 assert runtime < 3
114 return
115 runtime = timeit.default_timer() - start
116 assert runtime < 3
117 assert te.value.stdout == b""
118 assert te.value.stderr == b""
119 assert te.value.timeout == 0.1
120 assert te.value.cmd == command
121
122
123 def test_argument_deprecation(tmp_path):
124 with pytest.warns(DeprecationWarning, match="keyword arguments"):
125 result = procrunner.run(
126 [sys.executable, "-V"],
127 None,
128 working_directory=tmp_path,
129 )
130 assert not result.returncode
131 assert result.stderr or result.stdout
00 [tox]
1 envlist = py27, py35, py36, py37, py38, flake8
1 envlist = py36, py37, py38, flake8
22
33 [travis]
44 python =
55 3.8: py38
66 3.7: py37
77 3.6: py36
8 3.5: py35
9 2.7: py27
8
9 [testenv:azure]
10 basepython = python
11 deps =
12 pytest-azurepipelines
13 pytest-cov
14 -r{toxinidir}/requirements_dev.txt
15 setenv =
16 PYTHONDEVMODE = 1
17 commands =
18 pytest -ra --basetemp={envtmpdir} --cov=procrunner --cov-report=html --cov-report=xml --cov-branch
1019
1120 [testenv:flake8]
1221 basepython = python