pax_global_header00006660000000000000000000000064141705240500014510gustar00rootroot0000000000000052 comment=abae165d333d1f653cd2cc4b3f3cf3fdc35c6fbb EasyProcess-1.1/000077500000000000000000000000001417052405000136115ustar00rootroot00000000000000EasyProcess-1.1/.github/000077500000000000000000000000001417052405000151515ustar00rootroot00000000000000EasyProcess-1.1/.github/workflows/000077500000000000000000000000001417052405000172065ustar00rootroot00000000000000EasyProcess-1.1/.github/workflows/main.yml000066400000000000000000000035001417052405000206530ustar00rootroot00000000000000# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python package on: schedule: # * is a special character in YAML so you have to quote this string - cron: '30 5 1 * *' push: pull_request: jobs: build: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - "ubuntu-18.04" - "macos-10.15" - "macos-11" - "windows-2019" - "windows-2022" python-version: - "3.9" - "3.10" stdfile: - "pipe" - "tempfile" include: - python-version: "3.6" os: ubuntu-20.04 stdfile: "tempfile" - python-version: "3.7" os: ubuntu-20.04 stdfile: "tempfile" - python-version: "3.8" os: ubuntu-20.04 stdfile: "tempfile" - python-version: "3.9" os: ubuntu-20.04 stdfile: "tempfile" - python-version: "3.10" os: ubuntu-20.04 stdfile: "tempfile" steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install . pip install -r requirements-test.txt - name: Test with pytest (tempfiles) if: matrix.stdfile == 'tempfile' run: | cd tests pytest -v . - name: Test with pytest (pipes) if: matrix.stdfile == 'pipe' run: | cd tests pytest -v . env: EASYPROCESS_USE_TEMP_FILES: 0 - name: Lint if: matrix.os == 'ubuntu-20.04' run: | ./lint.sh EasyProcess-1.1/.gitignore000066400000000000000000000004761417052405000156100ustar00rootroot00000000000000*.py[c|o] *.egg *.egg-info /build /dist /nbproject/ pip-log.txt /.project /.pydevproject /include /lib /lib64 /bin /virtualenv *.bak */build/* */_build/* */_build/latex/* *.class *.png .version nosetests.xml .* !.git* !.travis* !.coveragerc /distribute_setup.py sloccount.sc *.prefs MANIFEST *.log docs/api/* EasyProcess-1.1/.travis.yml.bak000066400000000000000000000025261417052405000164630ustar00rootroot00000000000000language: python os: linux matrix: include: - name: 3.7_xenial python: 3.7 dist: xenial - name: 3.8_xenial python: 3.8 dist: xenial - name: 3.7_bionic python: 3.7 dist: bionic - name: 3.8_focal python: 3.8 dist: focal - name: 3.9_focal python: 3.9 dist: focal - name: "Python 3.7 on macOS" os: osx osx_image: xcode11.2 # Python 3.7.4 running on macOS 10.14.4 language: shell # 'language: python' is an error on Travis CI macOS env: PATH=/Users/travis/Library/Python/3.7/bin:$PATH PIPUSER=--user - name: "Python 3.8 on Windows" os: windows # Windows 10.0.17134 N/A Build 17134 language: shell # 'language: python' is an error on Travis CI Windows before_install: - choco install python --version 3.8 - python -m pip install --upgrade pip env: PATH=/c/Python38:/c/Python38/Scripts:$PATH addons: apt: packages: - xvfb - imagemagick install: - PYTHON=python3 - if [ ${TRAVIS_OS_NAME} == "windows" ]; then PYTHON=python; fi - $PYTHON -m pip install $PIPUSER --upgrade -r requirements-test.txt - $PYTHON -m pip install $PIPUSER --upgrade . script: - cd tests - $PYTHON -m pytest -v . - EASYPROCESS_USE_TEMP_FILES=0 $PYTHON -m pytest -v . EasyProcess-1.1/LICENSE.txt000066400000000000000000000024351417052405000154400ustar00rootroot00000000000000Copyright (c) 2011, ponty All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. EasyProcess-1.1/MANIFEST.in000066400000000000000000000000561417052405000153500ustar00rootroot00000000000000include LICENSE* recursive-include tests *.py EasyProcess-1.1/README.md000066400000000000000000000121071417052405000150710ustar00rootroot00000000000000EasyProcess is an easy to use python subprocess interface. Links: * home: https://github.com/ponty/EasyProcess * PYPI: https://pypi.python.org/pypi/EasyProcess ![workflow](https://github.com/ponty/EasyProcess/actions/workflows/main.yml/badge.svg) Features: - layer on top of [subprocess](https://docs.python.org/library/subprocess.html) module - easy to start, stop programs - easy to get standard output/error, return code of programs - command can be list (preferred) or string (command string is converted to list using shlex.split) - logging - timeout - shell is not supported - pipes are not supported - stdout/stderr is set only after the subprocess has finished - stop() does not kill whole subprocess tree - unicode support - supported python versions: 3.7, 3.8, 3.9, 3.10 - [Method chaining](https://en.wikipedia.org/wiki/Method_chaining) Installation: ```console $ python3 -m pip install EasyProcess ``` Usage ===== Examples: ```py # easyprocess/examples/hello.py from easyprocess import EasyProcess cmd = ["echo", "hello"] s = EasyProcess(cmd).call().stdout print(s) ``` Output: ```console $ python3 -m easyprocess.examples.hello hello ``` ```py # easyprocess/examples/cmd.py import sys from easyprocess import EasyProcess python = sys.executable print("-- Run program, wait for it to complete, get stdout:") s = EasyProcess([python, "-c", "print(3)"]).call().stdout print(s) print("-- Run program, wait for it to complete, get stderr:") s = EasyProcess([python, "-c", "import sys;sys.stderr.write('4\\n')"]).call().stderr print(s) print("-- Run program, wait for it to complete, get return code:") s = EasyProcess([python, "--version"]).call().return_code print(s) print("-- Run program, wait 1.5 second, stop it, get stdout:") prog = """ import time for i in range(10): print(i, flush=True) time.sleep(1) """ s = EasyProcess([python, "-c", prog]).start().sleep(1.5).stop().stdout print(s) ``` Output: ```console $ python3 -m easyprocess.examples.cmd -- Run program, wait for it to complete, get stdout: 3 -- Run program, wait for it to complete, get stderr: 4 -- Run program, wait for it to complete, get return code: 0 -- Run program, wait 1.5 second, stop it, get stdout: 0 1 ``` Shell commands -------------- Shell commands are not supported. ``echo`` is a shell command on Windows (there is no echo.exe), but it is a program on Linux. return_code ----------- `EasyProcess.return_code` is None until `EasyProcess.stop` or `EasyProcess.wait` is called. With ---- By using `with` statement the process is started and stopped automatically: ```python from easyprocess import EasyProcess with EasyProcess(["ping", "127.0.0.1"]) as proc: # start() # communicate with proc pass # stopped ``` Equivalent: ```python from easyprocess import EasyProcess proc = EasyProcess(["ping", "127.0.0.1"]).start() try: # communicate with proc pass finally: proc.stop() ``` Full example: ```py # easyprocess/examples/with.py import os import sys import urllib.request from os.path import abspath, dirname from time import sleep from easyprocess import EasyProcess webserver_code = """ from http.server import HTTPServer, CGIHTTPRequestHandler srv = HTTPServer(server_address=("", 8080), RequestHandlerClass=CGIHTTPRequestHandler) srv.serve_forever() """ os.chdir(dirname(abspath(__file__))) with EasyProcess([sys.executable, "-c", webserver_code]): sleep(2) # wait for server html = urllib.request.urlopen("http://localhost:8080").read().decode("utf-8") print(html) ``` Output: ```console $ python3 -m easyprocess.examples.with Directory listing for /

Directory listing for /



``` Timeout ------- ```py # easyprocess/examples/timeout.py import sys from easyprocess import EasyProcess python = sys.executable prog = """ import time for i in range(3): print(i, flush=True) time.sleep(1) """ print("-- no timeout") stdout = EasyProcess([python, "-c", prog]).call().stdout print(stdout) print("-- timeout=1.5s") stdout = EasyProcess([python, "-c", prog]).call(timeout=1.5).stdout print(stdout) print("-- timeout=50s") stdout = EasyProcess([python, "-c", prog]).call(timeout=50).stdout print(stdout) ``` Output: ```console $ python3 -m easyprocess.examples.timeout -- no timeout 0 1 2 -- timeout=1.5s 0 1 -- timeout=50s 0 1 2 ```EasyProcess-1.1/Vagrantfile000066400000000000000000000007761417052405000160100ustar00rootroot00000000000000Vagrant.configure(2) do |config| config.vm.box = "ubuntu/focal64" config.vm.provider "virtualbox" do |vb| #vb.gui = true # vb.memory = "2048" vb.name = "easyprocess_2004" # https://bugs.launchpad.net/cloud-images/+bug/1829625 # vb.customize ["modifyvm", :id, "--uart1", "0x3F8", "4"] # vb.customize ["modifyvm", :id, "--uartmode1", "file", "./ttyS0.log"] end config.vm.provision "shell", path: "vagrant.sh" config.ssh.extra_args = ["-t", "cd /vagrant; bash --login"] end EasyProcess-1.1/clean.py000077500000000000000000000007251417052405000152540ustar00rootroot00000000000000#!/usr/bin/env python3 import pathlib import shutil [shutil.rmtree(p) for p in pathlib.Path(".").glob(".tox")] [shutil.rmtree(p) for p in pathlib.Path(".").glob("dist")] [shutil.rmtree(p) for p in pathlib.Path(".").glob("*.egg-info")] [shutil.rmtree(p) for p in pathlib.Path(".").glob("build")] [p.unlink() for p in pathlib.Path(".").rglob("*.py[co]")] [p.rmdir() for p in pathlib.Path(".").rglob("__pycache__")] [p.unlink() for p in pathlib.Path(".").rglob("*.log")] EasyProcess-1.1/doc/000077500000000000000000000000001417052405000143565ustar00rootroot00000000000000EasyProcess-1.1/doc/gen/000077500000000000000000000000001417052405000151275ustar00rootroot00000000000000EasyProcess-1.1/doc/gen/python3_-m_easyprocess.examples.cmd.txt000066400000000000000000000004111417052405000246600ustar00rootroot00000000000000$ python3 -m easyprocess.examples.cmd -- Run program, wait for it to complete, get stdout: 3 -- Run program, wait for it to complete, get stderr: 4 -- Run program, wait for it to complete, get return code: 0 -- Run program, wait 1.5 second, stop it, get stdout: 0 1EasyProcess-1.1/doc/gen/python3_-m_easyprocess.examples.hello.txt000066400000000000000000000000551417052405000252240ustar00rootroot00000000000000$ python3 -m easyprocess.examples.hello helloEasyProcess-1.1/doc/gen/python3_-m_easyprocess.examples.timeout.txt000066400000000000000000000001461417052405000256100ustar00rootroot00000000000000$ python3 -m easyprocess.examples.timeout -- no timeout 0 1 2 -- timeout=1.5s 0 1 -- timeout=50s 0 1 2EasyProcess-1.1/doc/gen/python3_-m_easyprocess.examples.with.txt000066400000000000000000000012341417052405000250740ustar00rootroot00000000000000$ python3 -m easyprocess.examples.with Directory listing for /

Directory listing for /



EasyProcess-1.1/doc/generate-doc.py000066400000000000000000000025771417052405000173000ustar00rootroot00000000000000import glob import logging import os from entrypoint2 import entrypoint from easyprocess import EasyProcess commands = [ "python3 -m easyprocess.examples.hello", "python3 -m easyprocess.examples.cmd", "python3 -m easyprocess.examples.timeout", "python3 -m easyprocess.examples.with", ] def empty_dir(dir): files = glob.glob(os.path.join(dir, "*")) for f in files: os.remove(f) @entrypoint def main(): gendir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "gen") logging.info("gendir: %s", gendir) os.makedirs(gendir, exist_ok=True) empty_dir(gendir) pls = [] try: os.chdir("gen") for cmd in commands: logging.info("cmd: %s", cmd) fname_base = cmd.replace(" ", "_") fname = fname_base + ".txt" logging.info("cmd: %s", cmd) print("file name: %s" % fname) with open(fname, "w") as f: f.write("$ " + cmd + "\n") p = EasyProcess(cmd).call() f.write(p.stdout) f.write(p.stderr) pls += [p] finally: os.chdir("..") for p in pls: p.stop() embedme = EasyProcess(["npx", "embedme", "../README.md"]) embedme.call() print(embedme.stdout) assert embedme.return_code == 0 assert "but file does not exist" not in embedme.stdout EasyProcess-1.1/easyprocess/000077500000000000000000000000001417052405000161515ustar00rootroot00000000000000EasyProcess-1.1/easyprocess/__init__.py000066400000000000000000000266031417052405000202710ustar00rootroot00000000000000"""Easy to use python subprocess interface.""" import logging import os.path import subprocess import tempfile import time from typing import Any, List, Optional, Union from easyprocess.about import __version__ from easyprocess.unicodeutil import split_command, unidecode log = logging.getLogger(__name__) log.debug("version=%s", __version__) class EasyProcessError(Exception): def __init__(self, easy_process, msg=""): self.easy_process = easy_process self.msg = msg def __str__(self): return self.msg + " " + repr(self.easy_process) def _rm_ending_lf(s): if s.endswith("\n"): s = s[:-1] if s.endswith("\r"): s = s[:-1] return s class EasyProcess(object): """ .. module:: easyprocess simple interface for :mod:`subprocess` shell is not supported (shell=False) .. warning:: unicode is supported only for string list command (Python2.x) (check :mod:`shlex` for more information) :param cmd: string ('ls -l') or list of strings (['ls','-l']) :param cwd: working directory :param use_temp_files: use temp files (True) or pipes (False) for stdout and stderr, pipes can cause deadlock in some cases (see unit tests) :param env: If *env* is not ``None``, it must be a mapping that defines the environment variables for the new process; these are used instead of inheriting the current process' environment, which is the default behavior. (check :mod:`subprocess` for more information) """ def __init__( self, cmd: Union[List[str], str], cwd: Optional[str] = None, use_temp_files: bool = True, env=None, ): self.use_temp_files = use_temp_files # for testing EASYPROCESS_USE_TEMP_FILES = os.environ.get("EASYPROCESS_USE_TEMP_FILES") if EASYPROCESS_USE_TEMP_FILES: log.debug("EASYPROCESS_USE_TEMP_FILES=%s", EASYPROCESS_USE_TEMP_FILES) # '0'->false, '1'->true self.use_temp_files = bool(int(EASYPROCESS_USE_TEMP_FILES)) self._outputs_processed = False self.env = env self.popen: Optional[subprocess.Popen] = None self.stdout = None self.stderr = None self._stdout_file: Any = None self._stderr_file: Any = None self.is_started = False self.oserror: Optional[OSError] = None self.cmd_param = cmd # self._thread: Optional[threading.Thread] = None self.timeout_happened = False self.cwd = cwd self.cmd = split_command(cmd) # self.cmd_as_string = " ".join(self.cmd) self.enable_stdout_log = True self.enable_stderr_log = True # log.debug('param: "%s" ', self.cmd_param) log.debug("command: %s", self.cmd) # log.debug('joined command: %s', self.cmd_as_string) if not len(self.cmd): raise EasyProcessError(self, "empty command!") def __repr__(self): msg = '<%s cmd_param=%s cmd=%s oserror=%s return_code=%s stdout="%s" stderr="%s" timeout_happened=%s>' % ( self.__class__.__name__, self.cmd_param, self.cmd, self.oserror, self.return_code, self.stdout, self.stderr, self.timeout_happened, ) return msg @property def pid(self) -> Optional[int]: """ PID (:attr:`subprocess.Popen.pid`) :rtype: int """ if self.popen: return self.popen.pid return None @property def return_code(self) -> Optional[int]: """ returncode (:attr:`subprocess.Popen.returncode`) :rtype: int """ if self.popen: return self.popen.returncode return None def call(self, timeout: Optional[float] = None) -> "EasyProcess": """Run command with arguments. Wait for command to complete. same as: 1. :meth:`start` 2. :meth:`wait` 3. :meth:`stop` :rtype: self """ try: self.start().wait(timeout=timeout) finally: if self.is_alive(): self.stop() return self def start(self) -> "EasyProcess": """start command in background and does not wait for it. :rtype: self """ if self.is_started: raise EasyProcessError(self, "process was started twice!") stdout: Any = None stderr: Any = None if self.use_temp_files: self._stdout_file = tempfile.TemporaryFile(prefix="stdout_") self._stderr_file = tempfile.TemporaryFile(prefix="stderr_") stdout = self._stdout_file stderr = self._stderr_file else: stdout = subprocess.PIPE stderr = subprocess.PIPE # cmd = list(map(uniencode, self.cmd)) try: self.popen = subprocess.Popen( self.cmd, stdout=stdout, stderr=stderr, cwd=self.cwd, env=self.env, ) except OSError as oserror: log.debug("OSError exception: %s", oserror) self.oserror = oserror raise EasyProcessError(self, "start error") self.is_started = True log.debug("process was started (pid=%s)", self.pid) return self def is_alive(self) -> bool: """ poll process using :meth:`subprocess.Popen.poll` It updates stdout/stderr/return_code if process has stopped earlier. :rtype: bool """ if self.popen: alive = self.popen.poll() is None if not alive: # collect stdout/stderr/return_code if proc stopped self._wait4process() return alive else: return False def wait(self, timeout: Optional[float] = None) -> "EasyProcess": """Wait for command to complete. :rtype: self """ # Timeout (threading) discussion: https://stackoverflow.com/questions/1191374/subprocess-with-timeout self._wait4process(timeout) # if timeout is not None: # if not self._thread: # self._thread = threading.Thread(target=self._wait4process) # self._thread.daemon = True # self._thread.start() # if self._thread: # self._thread.join(timeout=timeout) # self.timeout_happened = self.timeout_happened or self._thread.is_alive() # else: # # no timeout and no existing thread # self._wait4process() return self def _wait4process(self, timeout=None): if self._outputs_processed: return if not self.popen: return if self.use_temp_files: try: self.popen.wait(timeout=timeout) except subprocess.TimeoutExpired: self.timeout_happened = True log.debug("timeout") return self._stdout_file.seek(0) self._stderr_file.seek(0) self.stdout = self._stdout_file.read() self.stderr = self._stderr_file.read() self._stdout_file.close() self._stderr_file.close() else: # This will deadlock when using stdout=PIPE and/or stderr=PIPE # and the child process generates enough output to a pipe such # that it blocks waiting for the OS pipe buffer to accept more data. # Use communicate() to avoid that. # self.popen.wait() # self.stdout = self.popen.stdout.read() # self.stderr = self.popen.stderr.read() try: (self.stdout, self.stderr) = self.popen.communicate(timeout=timeout) except subprocess.TimeoutExpired: self.timeout_happened = True log.debug("timeout") return log.debug("process has ended, return code=%s", self.return_code) self.stdout = _rm_ending_lf(unidecode(self.stdout)) self.stderr = _rm_ending_lf(unidecode(self.stderr)) self._outputs_processed = True # def limit_str(s): # if len(s) > self.max_bytes_to_log: # warn = '[middle of output was removed, max_bytes_to_log=%s]'%(self.max_bytes_to_log) # s = s[:self.max_bytes_to_log / 2] + warn + s[-self.max_bytes_to_log / 2:] # return s if self.enable_stdout_log: log.debug("stdout=%s", self.stdout) if self.enable_stderr_log: log.debug("stderr=%s", self.stderr) def stop(self) -> "EasyProcess": """Kill process and wait for command to complete. same as: 1. :meth:`sendstop` 2. :meth:`wait` :rtype: self """ self.sendstop().wait() # if self.is_alive() and kill_after is not None: # self.sendstop(kill=True).wait() return self def sendstop(self) -> "EasyProcess": """ Kill process (:meth:`subprocess.Popen.terminate`). Do not wait for command to complete. :rtype: self """ if not self.is_started: raise EasyProcessError(self, "process was not started!") log.debug('stopping process (pid=%s cmd="%s")', self.pid, self.cmd) if self.popen: if self.is_alive(): log.debug("process is active -> calling kill()") self.popen.kill() # signame = "SIGKILL" if kill else "SIGTERM" # log.debug("process is active -> sending " + signame) # try: # try: # if kill: # self.popen.kill() # else: # self.popen.terminate() # except AttributeError: # os.kill(self.popen.pid, signal.SIGKILL) # except OSError as oserror: # log.debug("exception in terminate:%s", oserror) else: log.debug("process was already stopped") else: log.debug("process was not started") return self def sleep(self, sec: float) -> "EasyProcess": """ sleeping (same as :func:`time.sleep`) :rtype: self """ time.sleep(sec) return self def wrap(self, func, delay=0): """ returns a function which: 1. start process 2. call func, save result 3. stop process 4. returns result similar to :keyword:`with` statement :rtype: """ def wrapped(): self.start() if delay: self.sleep(delay) x = None try: x = func() except OSError as oserror: log.debug("OSError exception:%s", oserror) self.oserror = oserror raise EasyProcessError(self, "wrap error!") finally: self.stop() return x return wrapped def __enter__(self): """used by the :keyword:`with` statement""" self.start() return self def __exit__(self, *exc_info): """used by the :keyword:`with` statement""" self.stop() EasyProcess-1.1/easyprocess/about.py000066400000000000000000000000241417052405000176310ustar00rootroot00000000000000__version__ = "1.1" EasyProcess-1.1/easyprocess/examples/000077500000000000000000000000001417052405000177675ustar00rootroot00000000000000EasyProcess-1.1/easyprocess/examples/__init__.py000066400000000000000000000000001417052405000220660ustar00rootroot00000000000000EasyProcess-1.1/easyprocess/examples/cmd.py000066400000000000000000000013311417052405000211020ustar00rootroot00000000000000import sys from easyprocess import EasyProcess python = sys.executable print("-- Run program, wait for it to complete, get stdout:") s = EasyProcess([python, "-c", "print(3)"]).call().stdout print(s) print("-- Run program, wait for it to complete, get stderr:") s = EasyProcess([python, "-c", "import sys;sys.stderr.write('4\\n')"]).call().stderr print(s) print("-- Run program, wait for it to complete, get return code:") s = EasyProcess([python, "--version"]).call().return_code print(s) print("-- Run program, wait 1.5 second, stop it, get stdout:") prog = """ import time for i in range(10): print(i, flush=True) time.sleep(1) """ s = EasyProcess([python, "-c", prog]).start().sleep(1.5).stop().stdout print(s) EasyProcess-1.1/easyprocess/examples/hello.py000066400000000000000000000001511417052405000214410ustar00rootroot00000000000000from easyprocess import EasyProcess cmd = ["echo", "hello"] s = EasyProcess(cmd).call().stdout print(s) EasyProcess-1.1/easyprocess/examples/log.py000066400000000000000000000003721417052405000211240ustar00rootroot00000000000000import logging import sys from easyprocess import EasyProcess python = sys.executable # turn on logging logging.basicConfig(level=logging.DEBUG) EasyProcess([python, "--version"]).call() EasyProcess(["ping", "localhost"]).start().sleep(1).stop() EasyProcess-1.1/easyprocess/examples/timeout.py000066400000000000000000000007301417052405000220270ustar00rootroot00000000000000import sys from easyprocess import EasyProcess python = sys.executable prog = """ import time for i in range(3): print(i, flush=True) time.sleep(1) """ print("-- no timeout") stdout = EasyProcess([python, "-c", prog]).call().stdout print(stdout) print("-- timeout=1.5s") stdout = EasyProcess([python, "-c", prog]).call(timeout=1.5).stdout print(stdout) print("-- timeout=50s") stdout = EasyProcess([python, "-c", prog]).call(timeout=50).stdout print(stdout) EasyProcess-1.1/easyprocess/examples/ver.py000066400000000000000000000002421417052405000211330ustar00rootroot00000000000000import sys from easyprocess import EasyProcess python = sys.executable v = EasyProcess([python, "--version"]).call().stderr print("your python version:%s" % v) EasyProcess-1.1/easyprocess/examples/with.py000066400000000000000000000010501417052405000213100ustar00rootroot00000000000000import os import sys import urllib.request from os.path import abspath, dirname from time import sleep from easyprocess import EasyProcess webserver_code = """ from http.server import HTTPServer, CGIHTTPRequestHandler srv = HTTPServer(server_address=("", 8080), RequestHandlerClass=CGIHTTPRequestHandler) srv.serve_forever() """ os.chdir(dirname(abspath(__file__))) with EasyProcess([sys.executable, "-c", webserver_code]): sleep(2) # wait for server html = urllib.request.urlopen("http://localhost:8080").read().decode("utf-8") print(html) EasyProcess-1.1/easyprocess/py.typed000066400000000000000000000000001417052405000176360ustar00rootroot00000000000000EasyProcess-1.1/easyprocess/unicodeutil.py000066400000000000000000000012571417052405000210540ustar00rootroot00000000000000import logging import shlex from typing import List log = logging.getLogger(__name__) class EasyProcessUnicodeError(Exception): pass def split_command(cmd, posix=None) -> List[str]: """ - cmd is string list -> nothing to do - cmd is string -> split it using shlex :param cmd: string ('ls -l') or list of strings (['ls','-l']) :rtype: string list """ if not isinstance(cmd, str): # cmd is string list pass else: if posix is None: posix = True cmd = shlex.split(cmd, posix=posix) return cmd # def uniencode(s): # return s def unidecode(s): s = s.decode("utf-8", "ignore") return s EasyProcess-1.1/format-code.sh000077500000000000000000000001771417052405000163550ustar00rootroot00000000000000#!/bin/bash set -e autoflake -i -r --remove-all-unused-imports . autoflake -i -r --remove-unused-variables . isort . black . EasyProcess-1.1/lint.sh000077500000000000000000000001611417052405000151140ustar00rootroot00000000000000#!/bin/bash set -e python3 -m flake8 . --max-complexity=10 --max-line-length=127 python3 -m mypy "easyprocess" EasyProcess-1.1/pytest.ini000066400000000000000000000002161417052405000156410ustar00rootroot00000000000000[pytest] log_level=DEBUG log_format=%(asctime)s.%(msecs)03d %(name)s %(levelname)s %(message)s log_date_format=%Y-%m-%d %H:%M:%S #log_cli=trueEasyProcess-1.1/requirements-doc.txt000066400000000000000000000001451417052405000176400ustar00rootroot00000000000000autoflake isort black>=21.12b0 # pytest # pytest-timeout pyvirtualdisplay entrypoint2 pillow # mypy EasyProcess-1.1/requirements-test.txt000066400000000000000000000001061417052405000200470ustar00rootroot00000000000000pytest pytest-timeout pyvirtualdisplay entrypoint2 pillow mypy flake8 EasyProcess-1.1/setup.py000066400000000000000000000026331417052405000153270ustar00rootroot00000000000000import os from setuptools import setup NAME = "easyprocess" # get __version__ __version__ = None exec(open(os.path.join(NAME, "about.py")).read()) VERSION = __version__ PYPI_NAME = "EasyProcess" URL = "https://github.com/ponty/easyprocess" DESCRIPTION = "Easy to use Python subprocess interface." LONG_DESCRIPTION = """Easy to use Python subprocess interface. Documentation: https://github.com/ponty/easyprocess/tree/""" LONG_DESCRIPTION += VERSION PACKAGES = [ NAME, NAME + ".examples", ] classifiers = [ # Get more strings from # https://pypi.python.org/pypi?%3Aaction=list_classifiers "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ] setup( name=PYPI_NAME, version=VERSION, description=DESCRIPTION, long_description=LONG_DESCRIPTION, long_description_content_type="text/x-rst", classifiers=classifiers, keywords="subprocess interface", author="ponty", # author_email='', url=URL, license="BSD", packages=PACKAGES, package_data={ NAME: ["py.typed"], }, ) EasyProcess-1.1/tests/000077500000000000000000000000001417052405000147535ustar00rootroot00000000000000EasyProcess-1.1/tests/test_fast/000077500000000000000000000000001417052405000167475ustar00rootroot00000000000000EasyProcess-1.1/tests/test_fast/test_deadlock.py000066400000000000000000000032411417052405000221260ustar00rootroot00000000000000import os import sys import threading from time import sleep import pytest from pyvirtualdisplay.display import Display from easyprocess import EasyProcess python = sys.executable # requirement: apt install imagemagick # deadlock # popen.communicate() hangs # no deadlock with temp_files PROG = """ from PIL import Image Image.new("RGB",(99, 99)).show() """ EASYPROCESS_USE_TEMP_FILES = os.environ.get("EASYPROCESS_USE_TEMP_FILES") def test_dummy(): pass # skip these tests for Windows/Mac # and when 'use_temp_files' is forced by env variable if sys.platform.startswith("linux") and not EASYPROCESS_USE_TEMP_FILES: def test_has_imagemagick(): assert EasyProcess(["display", "-version"]).call().return_code == 0 @pytest.mark.timeout(120) def test_deadlock_temp_files(): with Display(): p = EasyProcess( [ python, "-c", PROG, ], use_temp_files=True, ) p.start() sleep(2) # hangs with pipes p.stop() @pytest.mark.timeout(120) def test_deadlock_pipe(): with Display(): p = EasyProcess( [ python, "-c", PROG, ], use_temp_files=False, ) p.start() sleep(2) def start(): # hangs with pipes p.stop() thread = threading.Thread(target=start) thread.start() sleep(6) assert thread.is_alive() thread.join() EasyProcess-1.1/tests/test_fast/test_env.py000066400000000000000000000006621417052405000211540ustar00rootroot00000000000000import json import sys from easyprocess import EasyProcess python = sys.executable def pass_env(e): prog = "import os,json;print(json.dumps(dict(os.environ)))" s = EasyProcess([python, "-c", prog], env=e).call().stdout return json.loads(s) def test_env(): assert len(pass_env(None)) > 0 e = pass_env(None) assert pass_env(e).get("FOO") is None e["FOO"] = "2" assert pass_env(e).get("FOO") == "2" EasyProcess-1.1/tests/test_fast/test_examples.py000066400000000000000000000015511417052405000222000ustar00rootroot00000000000000import sys from easyprocess import EasyProcess def test(): # skip these tests for Windows/Mac if not sys.platform.startswith("linux"): return assert ( EasyProcess([sys.executable, "-m", "easyprocess.examples.ver"]) .call() .return_code == 0 ) assert ( EasyProcess([sys.executable, "-m", "easyprocess.examples.log"]) .call() .return_code == 0 ) assert ( EasyProcess([sys.executable, "-m", "easyprocess.examples.cmd"]) .call() .return_code == 0 ) assert ( EasyProcess([sys.executable, "-m", "easyprocess.examples.hello"]) .call() .return_code == 0 ) assert ( EasyProcess([sys.executable, "-m", "easyprocess.examples.timeout"]) .call() .return_code == 0 ) EasyProcess-1.1/tests/test_fast/test_proc.py000066400000000000000000000042021417052405000213210ustar00rootroot00000000000000from __future__ import with_statement import sys import time import pytest from easyprocess import EasyProcess python = sys.executable def test_call(): assert EasyProcess("ls -la").call().return_code == 0 assert EasyProcess(["ls", "-la"]).call().return_code == 0 def test_start(): p = EasyProcess("ls -la").start() time.sleep(0.2) assert p.stop().return_code == 0 def test_start2(): p = EasyProcess("echo hi").start() time.sleep(0.2) # no wait() -> no results assert p.return_code is None assert p.stdout is None @pytest.mark.timeout(1) def test_start3(): p = EasyProcess("sleep 10").start() assert p.return_code is None def test_alive(): assert EasyProcess("ping 127.0.0.1 -c 2").is_alive() is False assert EasyProcess("ping 127.0.0.1 -c 2").start().is_alive() assert EasyProcess("ping 127.0.0.1 -c 2").start().stop().is_alive() is False assert EasyProcess("ping 127.0.0.1 -c 2").call().is_alive() is False def test_std(): assert EasyProcess("echo hello").call().stdout == "hello" assert EasyProcess([python, "-c", "print(42)"]).call().stdout == "42" def test_wait(): assert EasyProcess("echo hello").wait().return_code is None assert EasyProcess("echo hello").wait().stdout is None assert EasyProcess("echo hello").start().wait().return_code == 0 assert EasyProcess("echo hello").start().wait().stdout == "hello" # def test_xephyr(): # EasyProcess('Xephyr -help').check(return_code=1) def test_wrap(): def f(): return EasyProcess("echo hi").call().stdout assert EasyProcess("ping 127.0.0.1").wrap(f)() == "hi" def test_with(): with EasyProcess("ping 127.0.0.1") as x: assert x.is_alive() assert x.return_code != 0 assert not x.is_alive() def test_parse(): assert EasyProcess("ls -la").cmd == ["ls", "-la"] assert EasyProcess('ls "abc"').cmd == ["ls", "abc"] assert EasyProcess('ls "ab c"').cmd == ["ls", "ab c"] def test_stop(): p = EasyProcess("ls -la").start() time.sleep(0.2) assert p.stop().return_code == 0 assert p.stop().return_code == 0 assert p.stop().return_code == 0 EasyProcess-1.1/tests/test_fast/test_returncode.py000066400000000000000000000024071417052405000225350ustar00rootroot00000000000000from easyprocess import EasyProcess def test_return_code(): # process has finished but no stop() or wait() was called assert EasyProcess("echo hello").start().sleep(0.5).return_code is None # wait() assert EasyProcess("echo hello").start().wait().return_code == 0 # stop() after process has finished assert EasyProcess("echo hello").start().sleep(0.5).stop().return_code == 0 # stop() before process has finished assert EasyProcess("sleep 2").start().stop().return_code != 0 # same as start().wait().stop() assert EasyProcess("echo hello").call().return_code == 0 def test_is_alive1(): # early exit p = EasyProcess("echo hello").start().sleep(0.5) assert p.return_code is None assert p.stdout is None assert p.stderr is None assert p.is_alive() is False # is_alive collects ouputs if proc stopped assert p.return_code == 0 assert p.stdout == "hello" assert p.stderr == "" def test_is_alive2(): # no exit p = EasyProcess("sleep 10").start() assert p.return_code is None assert p.stdout is None assert p.stderr is None assert p.is_alive() # is_alive collects ouputs if proc stopped assert p.return_code is None assert p.stdout is None assert p.stderr is None EasyProcess-1.1/tests/test_fast/test_started.py000066400000000000000000000015641417052405000220340ustar00rootroot00000000000000import pytest from easyprocess import EasyProcess, EasyProcessError def test_is_started(): assert EasyProcess("ls -la").is_started is False assert EasyProcess("ls -la").start().is_started assert EasyProcess("ls -la").call().is_started assert EasyProcess("ls -la").start().wait().is_started assert EasyProcess("ls -la").start().stop().is_started def test_raise(): with pytest.raises(EasyProcessError): EasyProcess("ls -la").start().start() with pytest.raises(EasyProcessError): EasyProcess("ls -la").stop() with pytest.raises(EasyProcessError): EasyProcess("ls -la").sendstop() # .assertRaises(EasyProcessError, lambda : EasyProcess('ls # -la').start().stop().stop()) with pytest.raises(EasyProcessError): EasyProcess("ls -la").start().wrap(lambda: None)() EasyProcess("ls -la").wrap(lambda: None)() EasyProcess-1.1/tests/test_fast/test_timeout.py000066400000000000000000000103631417052405000220510ustar00rootroot00000000000000import sys import time import pytest from easyprocess import EasyProcess python = sys.executable def test_timeout(): p = EasyProcess("sleep 1").start() p.wait(0.2) assert p.is_alive() p.wait(0.2) assert p.is_alive() p.wait(2) assert not p.is_alive() assert EasyProcess("sleep 0.3").call().return_code == 0 assert EasyProcess("sleep 0.3").call(timeout=0.1).return_code != 0 assert EasyProcess("sleep 0.3").call(timeout=1).return_code == 0 assert EasyProcess("sleep 0.3").call().timeout_happened is False assert EasyProcess("sleep 0.3").call(timeout=0.1).timeout_happened assert EasyProcess("sleep 0.3").call(timeout=1).timeout_happened is False @pytest.mark.timeout(10) def test_time_cli1(): hdr = "import logging;logging.basicConfig(level=logging.DEBUG);from easyprocess import EasyProcess;" p = EasyProcess( [ python, "-c", hdr + "EasyProcess('sleep 15').start()", ] ) p.call() assert p.return_code == 0 @pytest.mark.timeout(10) def test_time_cli2(): hdr = "import logging;logging.basicConfig(level=logging.DEBUG);from easyprocess import EasyProcess;" p = EasyProcess( [ python, "-c", hdr + "EasyProcess('sleep 15').call(timeout=0.5)", ] ) p.call() assert p.return_code == 0 @pytest.mark.timeout(10) def test_time2(): p = EasyProcess("sleep 15").call(timeout=1) assert p.is_alive() is False assert p.timeout_happened assert p.return_code != 0 assert p.stdout == "" @pytest.mark.timeout(10) def test_timeout_out(): p = EasyProcess( [python, "-c", "import time;print( 'start');time.sleep(15);print( 'end')"] ).call(timeout=1) assert p.is_alive() is False assert p.timeout_happened assert p.return_code != 0 assert p.stdout == "" @pytest.mark.timeout(3) def test_time3(): EasyProcess("sleep 15").start() ignore_term = """ import signal; import time; signal.signal(signal.SIGTERM, lambda *args: None); while True: time.sleep(0.5); """ # @pytest.mark.timeout(10) # def test_force_timeout(): # proc = EasyProcess([python, "-c", ignore_term]).start() # # Calling stop() right away actually stops python before it # # has a change to actually compile and run the input code, # # meaning the signal handlers aren't registered yet. Give it # # a moment to setup # time.sleep(1) # proc.stop(kill_after=1) # assert proc.is_alive() is False # assert proc.return_code != 0 @pytest.mark.timeout(30) def test_kill(): proc = EasyProcess([python, "-c", ignore_term]).start() # Calling stop() right away actually stops python before it # has a change to actually compile and run the input code, # meaning the signal handlers aren't registered yet. Give it # a moment to setup time.sleep(3) proc.stop() assert proc.is_alive() is False assert proc.return_code != 0 # @pytest.mark.timeout(10) # def test_force_0_timeout(): # proc = EasyProcess([python, "-c", ignore_term]).start() # time.sleep(1) # proc.stop(kill_after=0) # assert proc.is_alive() is False # assert proc.return_code != 0 @pytest.mark.timeout(10) def test_force_timeout2(): proc = EasyProcess([python, "-c", ignore_term]).call(timeout=1) assert proc.is_alive() is False assert proc.return_code != 0 # @pytest.mark.timeout(10) # def test_stop_wait(): # proc = EasyProcess([python, "-c", ignore_term]).start() # time.sleep(1) # proc.sendstop().wait(timeout=1) # # On windows, Popen.terminate actually behaves like kill, # # so don't check that our hanging process code is actually hanging. # # The end result is still what we want. On other platforms, leave # # this assertion to make sure we are correctly testing the ability # # to stop a hung process # if not sys.platform.startswith("win"): # assert proc.is_alive() is True # proc.stop(kill_after=1) # assert proc.is_alive() is False # assert proc.return_code != 0 @pytest.mark.timeout(30) def test_stop_wait(): proc = EasyProcess([python, "-c", ignore_term]).start() time.sleep(3) proc.sendstop().wait(timeout=3) assert proc.is_alive() is False assert proc.return_code != 0 EasyProcess-1.1/tests/test_fast/test_unicode.py000066400000000000000000000062641417052405000220160ustar00rootroot00000000000000import sys from easyprocess import EasyProcess from easyprocess.unicodeutil import split_command OMEGA = "\u03A9" python = sys.executable def platform_is_win(): return sys.platform == "win32" def py_minor(): return sys.version_info[1] def test_str(): assert EasyProcess("ls -la").call().return_code == 0 def test_ls(): assert EasyProcess(["ls", "-la"]).call().return_code == 0 def test_parse(): assert EasyProcess("ls -la").cmd == ["ls", "-la"] assert EasyProcess('ls "abc"').cmd == ["ls", "abc"] assert EasyProcess('ls "ab c"').cmd == ["ls", "ab c"] def test_split(): # list -> list assert split_command([str("x"), str("y")]) == ["x", "y"] assert split_command([str("x"), "y"]) == ["x", "y"] assert split_command([str("x"), OMEGA]) == ["x", OMEGA] # str -> list assert split_command(str("x y")) == ["x", "y"] assert split_command("x y") == ["x", "y"] assert split_command("x " + OMEGA) == ["x", OMEGA] # split windows paths #12 assert split_command("c:\\temp\\a.exe someArg", posix=False) == [ "c:\\temp\\a.exe", "someArg", ] def test_echo(): assert EasyProcess("echo hi").call().stdout == "hi" if not platform_is_win(): assert EasyProcess("echo " + OMEGA).call().stdout == OMEGA assert EasyProcess(["echo", OMEGA]).call().stdout == OMEGA def test_argv(): assert ( EasyProcess([python, "-c", r"import sys;assert sys.argv[1]=='123'", "123"]) .call() .return_code == 0 ) assert ( EasyProcess([python, "-c", r"import sys;assert sys.argv[1]=='\u03a9'", OMEGA]) .call() .return_code == 0 ) if py_minor() > 6: # sys.stdout.reconfigure from py3.7 def test_py_print(): assert ( EasyProcess( [ python, "-c", r"import sys;sys.stdout.reconfigure(encoding='utf-8');print('\u03a9')", ] ) .call() .stdout == OMEGA ) assert ( EasyProcess( [ python, "-c", r"import sys;sys.stdout.reconfigure(encoding='utf-8');print(sys.argv[1])", OMEGA, ] ) .call() .stdout == OMEGA ) def test_py_stdout_write(): assert ( EasyProcess( [ python, "-c", r"import sys;sys.stdout.buffer.write('\u03a9'.encode('utf-8'))", ] ) .call() .stdout == OMEGA ) def test_invalid_stdout(): """invalid utf-8 byte in stdout.""" # https://en.wikipedia.org/wiki/UTF-8#Codepage_layout # 0x92 continuation byte cmd = [python, "-c", "import sys;sys.stdout.buffer.write(b'\\x92')"] p = EasyProcess(cmd).call() assert p.return_code == 0 assert p.stdout == "" # 0xFF must never appear in a valid UTF-8 sequence cmd = [python, "-c", "import sys;sys.stdout.buffer.write(b'\\xFF')"] p = EasyProcess(cmd).call() assert p.return_code == 0 assert p.stdout == "" EasyProcess-1.1/tests/test_stress.py000066400000000000000000000006011417052405000177040ustar00rootroot00000000000000from easyprocess import EasyProcess def test_call(): for x in range(1000): # test for: # OSError exception:[Errno 24] Too many open files print("index=", x) assert EasyProcess("echo hi").call().return_code == 0 # def test_start(self): # for x in range(1000): # print('index=', x) # EasyProcess('echo hi').start() EasyProcess-1.1/tests/test_stress2.py000066400000000000000000000004771417052405000200010ustar00rootroot00000000000000import pytest from easyprocess import EasyProcess # if run with coverage: # Fatal Python error: deallocating None @pytest.mark.timeout(1000) def test_timeout(): # pragma: no cover for x in range(1000): print("index=", x) assert EasyProcess("sleep 5").call(timeout=0.05).return_code != 0 EasyProcess-1.1/tox.ini000066400000000000000000000013731417052405000151300ustar00rootroot00000000000000[tox] envlist = py310 py310-{tempfile,pipe} py39 py39-{tempfile,pipe} py38 py37 py36 py3-doc py3-lint # Workaround for Vagrant #toxworkdir={toxinidir}/.tox # default toxworkdir={homedir}/.tox/easyprocess [testenv] deps = -rrequirements-test.txt setenv = pipe: EASYPROCESS_USE_TEMP_FILES = 0 tempfile: DUMMY = 1 changedir=tests commands= {envpython} -m pytest -v . [testenv:py3-doc] allowlist_externals=bash changedir=doc deps = -rrequirements-doc.txt commands= bash -c "cd ..;./format-code.sh" {envpython} generate-doc.py --debug [testenv:py3-lint] allowlist_externals=bash changedir=. deps = -rrequirements-test.txt commands= bash -c "./lint.sh" EasyProcess-1.1/vagrant.sh000066400000000000000000000014211417052405000156050ustar00rootroot00000000000000#!/bin/bash export DEBIAN_FRONTEND=noninteractive sudo update-locale LANG=en_US.UTF-8 LANGUAGE=en.UTF-8 # echo 'export export LC_ALL=C' >> /home/vagrant/.profile # install python versions sudo add-apt-repository --yes ppa:deadsnakes/ppa sudo apt-get update sudo apt-get install -y python3.6-dev sudo apt-get install -y python3.7-dev sudo apt-get install -y python3.8-dev sudo apt-get install -y python3-distutils sudo apt-get install -y python3.9-dev sudo apt-get install -y python3.9-distutils sudo apt-get install -y python3.10-dev sudo apt-get install -y python3.10-distutils # tools sudo apt-get install -y mc python3-pip xvfb # test dependencies sudo pip3 install tox sudo apt-get install -y imagemagick # doc dependencies sudo apt-get install -y npm sudo npm install -g npx