From 28ba83f25c59749bc2b1c933a920ef981f809194 Mon Sep 17 00:00:00 2001 From: Jonathan Hartley Date: Mon, 22 May 2023 08:43:45 -0500 Subject: [PATCH 01/21] Fix Changelog entry to link to #665, not #655. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66856845..b39d60f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.0.4 - 5/13/22 -- Allow `ok_code` to be used with `fg` [#655](https://github.com/amoffat/sh/pull/655) +- Allow `ok_code` to be used with `fg` [#665](https://github.com/amoffat/sh/pull/665) - Make sure `new_group` never creates a new session [#675](https://github.com/amoffat/sh/pull/675) ## 2.0.2 / 2.0.3 (misversioned) - 2/13/22 From 39dd2ece3fbe92488a67f5828e5468e753a5a2c2 Mon Sep 17 00:00:00 2001 From: Honnix Date: Mon, 5 Jun 2023 12:47:31 +0200 Subject: [PATCH 02/21] [doc] Add example for Piping to STDIN migration To give a concrete example for users to easily follow. --- MIGRATION.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/MIGRATION.md b/MIGRATION.md index e934838e..fa571397 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -80,6 +80,28 @@ Previously, if the first argument of a sh command was an instance of `RunningCom it was automatically fed into the process's STDIN. This is no longer the case and you must explicitly use `_in=`. +```python +from sh import wc,ls + +print(wc(ls("/home/", "-l"), "-l")) +``` + +Becomes: + +```python +from sh import wc,ls + +print(wc("-l", _in=ls("/home/", "-l"))) +``` + +Or: + +```python +from sh import wc,ls + +print(wc("-l", _in=ls("/home/", "-l", _return_cmd=True))) +``` + ### Workaround None From 8fb5a92698f314f9ccd63b8194906c041b5a6cb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Thu, 29 Jun 2023 19:17:09 +0200 Subject: [PATCH 03/21] Use f-strings --- sh.py | 95 ++++++++++++++++++++------------------------------- tests/test.py | 20 +++++------ 2 files changed, 45 insertions(+), 70 deletions(-) diff --git a/sh.py b/sh.py index e12b5597..fc7da40e 100644 --- a/sh.py +++ b/sh.py @@ -48,6 +48,7 @@ import struct import sys import termios +import textwrap import threading import time import traceback @@ -71,10 +72,9 @@ if "windows" in platform.system().lower(): # pragma: no cover raise ImportError( - "sh %s is currently only supported on linux and osx. \ + f"sh {__version__} is currently only supported on linux and osx. \ please install pbs 0.110 (http://pypi.python.org/pypi/pbs) for windows \ support." - % __version__ ) TEE_STDOUT = {True, "out", 1} @@ -246,24 +246,15 @@ def poll(self, timeout): Poller = PollPoller -def _indent_text(text, num=4): - lines = [] - for line in text.split("\n"): - line = (" " * num) + line - lines.append(line) - return "\n".join(lines) - - class ForkException(Exception): def __init__(self, orig_exc): - tmpl = """ + msg = f""" Original exception: =================== -%s +{textwrap.indent(orig_exc, " ")} """ - msg = tmpl % _indent_text(orig_exc) Exception.__init__(self, msg) @@ -321,25 +312,19 @@ def __init__(self, full_cmd, stdout, stderr, truncate=True): exc_stdout = exc_stdout[: self.truncate_cap] out_delta = len(self.stdout) - len(exc_stdout) if out_delta: - exc_stdout += ( - "... (%d more, please see e.stdout)" % out_delta - ).encode() + exc_stdout += (f"... ({out_delta} more, please see e.stdout)").encode() exc_stderr = self.stderr if truncate: exc_stderr = exc_stderr[: self.truncate_cap] err_delta = len(self.stderr) - len(exc_stderr) if err_delta: - exc_stderr += ( - "... (%d more, please see e.stderr)" % err_delta - ).encode() + exc_stderr += (f"... ({err_delta} more, please see e.stderr)").encode() - msg_tmpl = str("\n\n RAN: {cmd}\n\n STDOUT:\n{stdout}\n\n STDERR:\n{stderr}") - - msg = msg_tmpl.format( - cmd=self.full_cmd, - stdout=exc_stdout.decode(DEFAULT_ENCODING, "replace"), - stderr=exc_stderr.decode(DEFAULT_ENCODING, "replace"), + msg = ( + f"\n\n RAN: {self.full_cmd}" + f"\n\n STDOUT:\n{exc_stdout.decode(DEFAULT_ENCODING, 'replace')}" + f"\n\n STDERR:\n{exc_stderr.decode(DEFAULT_ENCODING, 'replace')}" ) super(ErrorReturnCode, self).__init__(msg) @@ -435,11 +420,10 @@ def get_rc_exc(rc): pass if rc >= 0: - name = "ErrorReturnCode_%d" % rc + name = f"ErrorReturnCode_{rc}" base = ErrorReturnCode else: - signame = SIGNAL_MAPPING[abs(rc)] - name = "SignalException_" + signame + name = f"SignalException_{SIGNAL_MAPPING[abs(rc)]}" base = SignalException exc = ErrorReturnCodeMeta(name, (base,), {"exit_code": rc}) @@ -569,12 +553,12 @@ class Logger(object): def __init__(self, name, context=None): self.name = name - self.log = logging.getLogger("%s.%s" % (SH_LOGGER_NAME, name)) + self.log = logging.getLogger("{SH_LOGGER_NAME}.{name}") self.context = self.sanitize_context(context) def _format_msg(self, msg, *a): if self.context: - msg = "%s: %s" % (self.context, msg) + msg = f"{self.context}: {msg}" return msg % a @staticmethod @@ -603,9 +587,9 @@ def exception(self, msg, *a): def default_logger_str(cmd, call_args, pid=None): if pid: - s = "" % (cmd, pid) + s = f"" else: - s = "" % cmd + s = f"" return s @@ -1089,8 +1073,8 @@ def tty_in_validator(passed_kwargs, merged_kwargs): for tty_type, std in pairs: if tty_type in passed_kwargs and ob_is_tty(passed_kwargs.get(std, None)): error = ( - "`_%s` is a TTY already, so so it doesn't make sense to set up a" - " TTY with `_%s`" % (std, tty_type) + f"`_{std}` is a TTY already, so so it doesn't make sense to set up a" + f" TTY with `_{tty_type}`" ) invalid.append(((tty_type, std), error)) @@ -1163,16 +1147,14 @@ def env_validator(passed_kwargs, merged_kwargs): return invalid if not isinstance(env, Mapping): - invalid.append(("env", "env must be dict-like. Got {!r}".format(env))) + invalid.append(("env", f"env must be dict-like. Got {env!r}")) return invalid for k, v in passed_kwargs["env"].items(): if not isinstance(k, str): - invalid.append(("env", "env key {!r} must be a str".format(k))) + invalid.append(("env", f"env key {k!r} must be a str")) if not isinstance(v, str): - invalid.append( - ("env", "value {!r} of env key {!r} must be a str".format(v, k)) - ) + invalid.append(("env", f"value {v!r} of env key {k!r} must be a str")) return invalid @@ -1378,9 +1360,9 @@ def _extract_call_args(cls, kwargs): if invalid_kwargs: exc_msg = [] for kwarg, error_msg in invalid_kwargs: - exc_msg.append(" %r: %s" % (kwarg, error_msg)) + exc_msg.append(f" {kwarg!r}: {error_msg}") exc_msg = "\n".join(exc_msg) - raise TypeError("Invalid special arguments:\n\n%s\n" % exc_msg) + raise TypeError(f"Invalid special arguments:\n\n{exc_msg}\n") return call_args, kwargs @@ -1416,7 +1398,7 @@ def __eq__(self, other): return str(self) == str(other) def __repr__(self): - return "" % str(self) + return f"" def __enter__(self): self(_with=True) @@ -2041,7 +2023,7 @@ def __init__( sid = os.getsid(0) pgid = os.getpgid(0) - payload = ("%d,%d" % (sid, pgid)).encode(DEFAULT_ENCODING) + payload = (f"{sid},{pgid}").encode(DEFAULT_ENCODING) os.write(session_pipe_write, payload) if ca["tty_out"] and not stdout_is_fd_based and not single_tty: @@ -2143,7 +2125,7 @@ def __init__( except Exception as e: # dump to stderr if we cannot save it to exc_pipe_write - sys.stderr.write("\nFATAL SH ERROR: %s\n" % e) + sys.stderr.write(f"\nFATAL SH ERROR: {e}\n") finally: os._exit(255) @@ -2351,7 +2333,7 @@ def fn(exit_code): self._quit_threads = threading.Event() - thread_name = "background thread for pid %d" % self.pid + thread_name = f"background thread for pid {self.pid}" self._bg_thread_exc_queue = Queue(1) self._background_thread = _start_daemon_thread( background_thread, @@ -2370,7 +2352,7 @@ def fn(exit_code): self._input_thread_exc_queue = Queue(1) if self._stdin_stream: close_before_term = not needs_ctty - thread_name = "STDIN thread for pid %d" % self.pid + thread_name = f"STDIN thread for pid {self.pid}" self._input_thread = _start_daemon_thread( input_thread, thread_name, @@ -2407,7 +2389,7 @@ def output_complete(): loop.call_soon_threadsafe(self.command.aio_output_complete.set) self._output_thread_exc_queue = Queue(1) - thread_name = "STDOUT/ERR thread for pid %d" % self.pid + thread_name = f"STDOUT/ERR thread for pid {self.pid}" self._output_thread = _start_daemon_thread( output_thread, thread_name, @@ -2423,7 +2405,7 @@ def output_complete(): ) def __repr__(self): - return "" % (self.pid, self.cmd[:500]) + return f"" def change_in_bufsize(self, buf): self._stdin_stream.stream_bufferer.change_buffering(buf) @@ -3335,26 +3317,24 @@ def _args(**kwargs): """allows us to temporarily override all the special keyword parameters in a with context""" - kwargs_str = ",".join(["%s=%r" % (k, v) for k, v in kwargs.items()]) + kwargs_str = ",".join([f"{k}={v!r}" for k, v in kwargs.items()]) raise DeprecationWarning( - """ + f""" sh.args() has been deprecated because it was never thread safe. use the following instead: - sh2 = sh({kwargs}) + sh2 = sh({kwargs_str}) sh2.your_command() or - sh2 = sh({kwargs}) + sh2 = sh({kwargs_str}) from sh2 import your_command your_command() -""".format( - kwargs=kwargs_str - ) +""" ) @@ -3499,7 +3479,7 @@ def sudo(orig): # pragma: no cover """a nicer version of sudo that uses getpass to ask for a password, or allows the first argument to be a string password""" - prompt = "[sudo] password for %s: " % getpass.getuser() + prompt = f"[sudo] password for {getpass.getuser()}: " def stdin(): pw = getpass.getpass(prompt=prompt) + "\n" @@ -3614,9 +3594,8 @@ def login_success(content): def run_repl(env): # pragma: no cover - banner = "\n>> sh v{version}\n>> https://github.com/amoffat/sh\n" + print(f"\n>> sh v{__version__}\n>> https://github.com/amoffat/sh\n") - print(banner.format(version=__version__)) while True: try: line = input("sh> ") diff --git a/tests/test.py b/tests/test.py index d18e675a..8ffac583 100644 --- a/tests/test.py +++ b/tests/test.py @@ -100,7 +100,7 @@ def requires_progs(*progs): friendly_missing = ", ".join(missing) return unittest.skipUnless( - len(missing) == 0, "Missing required system programs: %s" % friendly_missing + len(missing) == 0, f"Missing required system programs: {friendly_missing}" ) @@ -115,7 +115,7 @@ def requires_poller(poller): use_select = bool(int(os.environ.get("SH_TESTS_USE_SELECT", "0"))) cur_poller = "select" if use_select else "poll" return unittest.skipUnless( - cur_poller == poller, "Only enabled for select.%s" % cur_poller + cur_poller == poller, f"Only enabled for select.{cur_poller}" ) @@ -2112,17 +2112,15 @@ def test_cwd(self): def test_cwd_fg(self): td = realpath(tempfile.mkdtemp()) py = create_tmp_test( - """ + f""" import sh import os from os.path import realpath orig = realpath(os.getcwd()) print(orig) -sh.pwd(_cwd="{newdir}", _fg=True) +sh.pwd(_cwd="{td}", _fg=True) print(realpath(os.getcwd())) -""".format( - newdir=td - ) +""" ) orig, newdir, restored = python(py.name).strip().split("\n") @@ -3231,12 +3229,10 @@ def test_unicode_path(self): python_name = os.path.basename(sys.executable) py = create_tmp_test( - """#!/usr/bin/env {0} + f"""#!/usr/bin/env {python_name} # -*- coding: utf8 -*- print("字") -""".format( - python_name - ), +""", prefix="字", delete=False, ) @@ -3272,7 +3268,7 @@ def test_signal_exception_aliases(self): import sh - sig_name = "SignalException_%d" % signal.SIGQUIT + sig_name = f"SignalException_{signal.SIGQUIT}" sig = getattr(sh, sig_name) from sh import SignalException_SIGQUIT From baebe57f2845ba62904285931513ef4af4cd00ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Mon, 10 Jul 2023 22:54:45 +0200 Subject: [PATCH 04/21] fix f-strings --- sh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sh.py b/sh.py index fc7da40e..04fe4ad4 100644 --- a/sh.py +++ b/sh.py @@ -553,7 +553,7 @@ class Logger(object): def __init__(self, name, context=None): self.name = name - self.log = logging.getLogger("{SH_LOGGER_NAME}.{name}") + self.log = logging.getLogger(f"{SH_LOGGER_NAME}.{name}") self.context = self.sanitize_context(context) def _format_msg(self, msg, *a): From c24b7f0a90c55766eb2dbe4a8bd04c4cbdd137cd Mon Sep 17 00:00:00 2001 From: John Trimble Date: Sat, 29 Jul 2023 16:01:49 -0700 Subject: [PATCH 05/21] Fix nested contexts duplicating commands --- sh.py | 5 ++++- tests/test.py | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/sh.py b/sh.py index 04fe4ad4..cab0cf9c 100644 --- a/sh.py +++ b/sh.py @@ -1425,7 +1425,10 @@ def __call__(self, *args, **kwargs): pcall_args.pop("with", None) call_args.update(pcall_args) - cmd.extend(prepend.cmd) + # we do not prepend commands used as a 'with' context as they will + # be prepended to any nested commands + if not kwargs.get("_with", False): + cmd.extend(prepend.cmd) cmd.append(self._path) diff --git a/tests/test.py b/tests/test.py index 8ffac583..babaff92 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1101,6 +1101,13 @@ def test_with_context_args(self): out = whoami() self.assertEqual(out.strip(), "") + def test_with_context_nested(self): + echo_path = sh.echo._path + with sh.echo.bake("test1", _with=True): + with sh.echo.bake("test2", _with=True): + out = sh.echo("test3") + self.assertEqual(out.strip(), f"test1 {echo_path} test2 {echo_path} test3") + def test_binary_input(self): py = create_tmp_test( """ From 043ed5db9409f181e508846317363f46234a8743 Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Sun, 6 Aug 2023 22:21:57 -0700 Subject: [PATCH 06/21] fix dockerfile tests --- tests/Dockerfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Dockerfile b/tests/Dockerfile index a4293523..428dc538 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:bionic +FROM ubuntu:focal ARG cache_bust RUN apt update &&\ @@ -11,21 +11,21 @@ ENV LC_ALL en_US.UTF-8 ENV TZ Etc/UTC ENV DEBIAN_FRONTEND noninteractive -RUN apt-get -y install\ +RUN apt -y install\ software-properties-common\ curl\ sudo\ lsof -RUN add-apt-repository ppa:deadsnakes/ppa -RUN apt-get update -RUN apt-get -y install\ +RUN add-apt-repository -y ppa:deadsnakes/ppa +RUN apt update +RUN apt -y install\ python3.8\ python3.9\ python3.10\ python3.11 -RUN apt-get -y install\ +RUN apt -y install\ python3.8-distutils\ python3.9-distutils\ && curl https://bootstrap.pypa.io/get-pip.py | python3.9 - From 631a9499a87ba78ae79f7f6bdc5605036d2c4c3d Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Sun, 6 Aug 2023 22:25:28 -0700 Subject: [PATCH 07/21] changelog update --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b39d60f9..baf4a5d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.0.5 - + +- Allow nested `with` contexts [#690](https://github.com/amoffat/sh/issues/690) + ## 2.0.4 - 5/13/22 - Allow `ok_code` to be used with `fg` [#665](https://github.com/amoffat/sh/pull/665) From c815027cb28afa0bff6caa4d85c976ad4afb5d49 Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Sun, 6 Aug 2023 23:04:24 -0700 Subject: [PATCH 08/21] first pass read the docs --- .gitignore | 1 + .vscode/tasks.json | 29 + docs/Makefile | 20 + docs/source/conf.py | 62 ++ docs/source/examples/done.rst | 24 + docs/source/images/logo-230.png | Bin 0 -> 29629 bytes docs/source/index.rst | 161 +++++ docs/source/reference.rst | 7 + docs/source/sections/architecture.rst | 120 ++++ .../sections/asynchronous_execution.rst | 172 +++++ docs/source/sections/baking.rst | 50 ++ docs/source/sections/command_class.rst | 384 +++++++++++ docs/source/sections/contrib.rst | 179 +++++ docs/source/sections/default_arguments.rst | 55 ++ docs/source/sections/envs.rst | 33 + docs/source/sections/exit_codes.rst | 59 ++ docs/source/sections/faq.rst | 451 +++++++++++++ docs/source/sections/passing_arguments.rst | 44 ++ docs/source/sections/piping.rst | 64 ++ docs/source/sections/redirection.rst | 65 ++ docs/source/sections/special_arguments.rst | 621 ++++++++++++++++++ docs/source/sections/stdin.rst | 29 + docs/source/sections/subcommands.rst | 25 + docs/source/sections/sudo.rst | 149 +++++ docs/source/sections/with.rst | 28 + docs/source/tutorials.rst | 6 + .../tutorials/interacting_with_processes.rst | 243 +++++++ docs/source/tutorials/real_time_output.rst | 66 ++ docs/source/usage.rst | 17 + poetry.lock | 491 +++++++------- pyproject.toml | 19 +- 31 files changed, 3394 insertions(+), 280 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 docs/Makefile create mode 100644 docs/source/conf.py create mode 100644 docs/source/examples/done.rst create mode 100644 docs/source/images/logo-230.png create mode 100644 docs/source/index.rst create mode 100644 docs/source/reference.rst create mode 100644 docs/source/sections/architecture.rst create mode 100644 docs/source/sections/asynchronous_execution.rst create mode 100644 docs/source/sections/baking.rst create mode 100644 docs/source/sections/command_class.rst create mode 100644 docs/source/sections/contrib.rst create mode 100644 docs/source/sections/default_arguments.rst create mode 100644 docs/source/sections/envs.rst create mode 100644 docs/source/sections/exit_codes.rst create mode 100644 docs/source/sections/faq.rst create mode 100644 docs/source/sections/passing_arguments.rst create mode 100644 docs/source/sections/piping.rst create mode 100644 docs/source/sections/redirection.rst create mode 100644 docs/source/sections/special_arguments.rst create mode 100644 docs/source/sections/stdin.rst create mode 100644 docs/source/sections/subcommands.rst create mode 100644 docs/source/sections/sudo.rst create mode 100644 docs/source/sections/with.rst create mode 100644 docs/source/tutorials.rst create mode 100644 docs/source/tutorials/interacting_with_processes.rst create mode 100644 docs/source/tutorials/real_time_output.rst create mode 100644 docs/source/usage.rst diff --git a/.gitignore b/.gitignore index 97725d32..6a6c5488 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ __pycache__/ /.venv/ /build /dist +/docs/build \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..c83293d5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Doc builder", + "type": "shell", + "command": "source ${workspaceFolder}/.venv/bin/activate && find source/ | entr -s 'make clean && make html'", + "options": { + "cwd": "${workspaceFolder}/docs" + }, + "problemMatcher": [], + "group": { + "kind": "build" + }, + "isBackground": true, + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "dedicated", + "showReuseMessage": false, + "clear": true, + "close": true + } + } + ] +} diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..22f3514e --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= -a -W +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..11412a1c --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,62 @@ +# Configuration file for the Sphinx documentation builder. + +from pathlib import Path + +import toml + +_THIS_DIR = Path(__file__).parent +_REPO = _THIS_DIR.parent.parent +_PYPROJECT = _REPO / "pyproject.toml" + +pyproject = toml.load(_PYPROJECT) +nitpicky = True + +nitpick_ignore = [ + ("py:class", "Token"), + ("py:class", "'Token'"), +] + +nitpick_ignore_regex = [ + ("py:class", r".*lark.*"), +] + +version = pyproject["tool"]["poetry"]["version"] +release = version + +# -- Project information + +project = "sh" +copyright = "2023, Andrew Moffat" +author = "Andrew Moffat" + +# -- General configuration + +extensions = [ + "sphinx.ext.duration", + "sphinx.ext.doctest", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", +] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "sphinx": ("https://www.sphinx-doc.org/en/master/", None), + "lark": ("https://lark-parser.readthedocs.io/en/latest/", None), + "jinja2": ("https://jinja.palletsprojects.com/en/latest/", None), +} +intersphinx_disabled_domains = ["std"] + +templates_path = ["_templates"] + +# -- Options for HTML output + +html_theme = "sphinx_rtd_theme" + +# -- Options for EPUB output +epub_show_urls = "footnote" + +autodoc_typehints = "both" + +add_module_names = False diff --git a/docs/source/examples/done.rst b/docs/source/examples/done.rst new file mode 100644 index 00000000..5c43ba23 --- /dev/null +++ b/docs/source/examples/done.rst @@ -0,0 +1,24 @@ +Here's an example of using :ref:`done` to create a multiprocess pool, where +``sh.your_parallel_command`` is executed concurrently at no more than 10 at a +time: + +.. code-block:: python + + import sh + from threading import Semaphore + + pool = Semaphore(10) + + def done(cmd, success, exit_code): + pool.release() + + def do_thing(arg): + pool.acquire() + return sh.your_parallel_command(arg, _bg=True, _done=done) + + procs = [] + for arg in range(100): + procs.append(do_thing(arg)) + + # essentially a join + [p.wait() for p in procs] diff --git a/docs/source/images/logo-230.png b/docs/source/images/logo-230.png new file mode 100644 index 0000000000000000000000000000000000000000..bc9f16ed4846a5bed1950beed3b5848813353b22 GIT binary patch literal 29629 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 literal 0 HcmV?d00001 diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000..dbbc2dff --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,161 @@ + +.. toctree:: + :hidden: + + usage + reference + + sections/contrib + sections/sudo + + tutorials + sections/faq + +.. image:: images/logo-230.png + :alt: Logo + +sh +## + +.. image:: https://img.shields.io/pypi/v/sh.svg?style=flat-square + :target: https://pypi.python.org/pypi/sh + :alt: Version +.. image:: https://img.shields.io/pypi/dm/sh.svg?style=flat-square + :target: https://pypi.python.org/pypi/sh + :alt: Downloads Status +.. image:: https://img.shields.io/pypi/pyversions/sh.svg?style=flat-square + :target: https://pypi.python.org/pypi/sh + :alt: Python Versions +.. image:: https://img.shields.io/travis/amoffat/sh/master.svg?style=flat-square + :target: https://travis-ci.org/amoffat/sh + :alt: Build Status +.. image:: https://img.shields.io/coveralls/amoffat/sh.svg?style=flat-square + :target: https://coveralls.io/r/amoffat/sh?branch=master + :alt: Coverage Status +.. image:: https://img.shields.io/github/stars/amoffat/sh.svg?style=social&label=Star + :target: https://github.com/amoffat/sh + :alt: Github + +sh is a full-fledged subprocess replacement for Python 2.6 - 3.8, PyPy and PyPy3 +that allows you to call any program as if it were a function: + + +.. code-block:: python + + from sh import ifconfig + print(ifconfig("wlan0")) + +Output: + +.. code-block:: none + + wlan0 Link encap:Ethernet HWaddr 00:00:00:00:00:00 + inet addr:192.168.1.100 Bcast:192.168.1.255 Mask:255.255.255.0 + inet6 addr: ffff::ffff:ffff:ffff:fff/64 Scope:Link + UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 + RX packets:0 errors:0 dropped:0 overruns:0 frame:0 + TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:1000 + RX bytes:0 (0 GB) TX bytes:0 (0 GB) + +Note that these aren't Python functions, these are running the binary commands +on your system by dynamically resolving your ``$PATH``, much like Bash does, and +then wrapping the binary in a function. In this way, all the programs on your +system are easily available to you from within Python. + +sh relies on various Unix system calls and only works on Unix-like operating +systems - Linux, macOS, BSDs etc. Specifically, Windows is not supported. + + +Installation +============ + +.. code-block:: none + + pip install sh + + +Quick Reference +=============== + +Passing Arguments +----------------- + +.. code-block:: python + + sh.ls("-l", "/tmp", color="never") + +:ref:`Read More ` + +Exit Codes +---------- + +.. code-block:: python + + try: + sh.ls("/doesnt/exist") + except sh.ErrorReturnCode_2: + print("directory doesn't exist") + +:ref:`Read More ` + +Redirection +----------- + +.. code-block:: python + + sh.ls(_out="/tmp/dir_contents") + + with open("/tmp/dir_contents", "w") as h: + sh.ls(_out=h) + + from io import StringIO + buf = StringIO() + sh.ls(_out=buf) + +:ref:`Read More ` + +Baking +------ + +.. code-block:: python + + my_ls = sh.ls.bake("-l") + + # equivalent + my_ls("/tmp") + sh.ls("-l", "/tmp") + +:ref:`Read More ` + +Piping +------ + +.. code-block:: python + + sh.wc(sh.ls("-1"), "-l") + +:ref:`Read More ` + +Subcommands +----------- + +.. code-block:: python + + # equivalent + sh.git("show", "HEAD") + sh.git.show("HEAD") + +:ref:`Read More ` + + +Background Processes +-------------------- + +.. code-block:: python + + p = sh.find("-name", "sh.py", _bg=True) + # ... do other things ... + p.wait() + +:ref:`Read More ` diff --git a/docs/source/reference.rst b/docs/source/reference.rst new file mode 100644 index 00000000..f7434e76 --- /dev/null +++ b/docs/source/reference.rst @@ -0,0 +1,7 @@ +Reference +========= + +.. toctree:: + sections/special_arguments + sections/architecture + sections/command_class diff --git a/docs/source/sections/architecture.rst b/docs/source/sections/architecture.rst new file mode 100644 index 00000000..267ed69d --- /dev/null +++ b/docs/source/sections/architecture.rst @@ -0,0 +1,120 @@ +.. _architecture: + +Architecture Overview +##################### + +Launch +====== + +When it comes time to launch a process + +#. Open pipes and/or TTYs STDIN/OUT/ERR. +#. Open a pipe for communicating pre-exec exceptions from the child to the + parent. +#. Open a pipe for child/parent launch synchronization. +#. :func:`os.fork` a child process. + +From here, we have two concurrent processes running: + +Child +----- + +#. If :ref:`_bg=True ` is set, we ignore :attr:`signal.SIGHUP`. +#. If :ref:`_new_session=True `, become a session leader with + :func:`os.setsid`, else become a process group leader with + :func:`os.setpgrp`. +#. Write our session id to the a pipe connected to the parent. This is mainly + to synchronize with our parent that our session/group logic has finished. +#. :func:`os.dup2` the file descriptors of our previously-setup TTYs/pipes to + our STDIN/OUT/ERR file descriptors. +#. If we're a session leader and our STDIN is a TTY, via :ref:`_tty_in=True + `, acquire a controlling + terminal, thereby becoming the controlling process of the session. +#. Set our GID/UID if we've set a custom one via :ref:`_uid `. +#. Close all file descriptors greater than STDERR. +#. Call :func:`os.execv`. + +Parent +------ + +#. Check for any exceptions via the exception pipe connected to the child. +#. Block and read our child's session id from a pipe connected to the child. + This synchronizes to us that the child has finished moving between + sessions/groups and we can now accurately determine its current session id + and process group. +#. If we're using a TTY for STDIN, via :ref:`_tty_in=True `, disable + echoing on the TTY, so that data sent to STDIN is not echoed to STDOUT. + +Running +======= + +An instance of :ref:`oproc_class` contains two internal threads, one for STDIN, +and one for STDOUT and STDERR. The purpose of these threads is to handle +reading/writing to the read/write ends of the process's standard descriptors. + +For example, the STDOUT/ERR thread continually runs :func:`select.select` on the +master ends of the TTYs/pipes connected to STDOUT/ERR, and if they're ready to +read, reads the available data and aggregates it into the appropriate place. + +.. _arch_buffers: + +Buffers +------- + +A couple of different buffers must be considered when thinking about how data +flows through an sh process. + +The first buffer is the buffer associated with the underlying pipe or TTY +attached to STDOUT/ERR. In the case of a TTY (the default for output), the +buffer size is 0, so output is immediate -- a byte written by the process is a +byte received by sh. For a pipe, however, the buffer size of the pipe is +typically 4-64kb. :manpage:`pipe(2)`. + +.. seealso:: FAQ: :ref:`faq_tty_out` + +The second buffer is sh's internal buffers, one for STDOUT and one for STDERR. +These buffers aggregate data that has been read from the master end of the TTY +or pipe attached to the output fd, but before that data is sent along to the +appropriate output handler (queue, file object, function, etc). Data sits in +these buffers until we reach the size specified with :ref:`internal_bufsize`, at +which point the buffer flushes to the output handler. + + +Exit +==== + +STDIN Thread Shutdown +--------------------- + +On process completion, our internal threads must complete, as the read end of +STDIN, for example, which is connected to the process, is no longer open, so +writing to the slave end will no longer work. + +STDOUT/ERR Thread Shutdown +-------------------------- + +The STDOUT/ERR thread is a little more complicated, because although the process +is not alive, output data may still exist in the pipe/TTY buffer that must be +collected. So we essentially just :func:`select.select` on the read ends until +they return nothing, indicating that they are complete, then we break out of our +read loop. + +.. _arch_exit_code: + +Exit Code Processing +-------------------- + +The exit code is obtained from the reaped process. If the process ended from a +signal, the exit code is the negative value of that signal. For example, +SIGKILL would result in an exit code -9. + +Done Callback +------------- + +If specified, the :ref:`done` callback is executed with the :ref:`RunningCommand +` instance, a boolean indicating success, and the adjusted exit +code. After the callback returns, error processing continues. In other words, +the done callback is called regardless of success or failure, and there's +nothing it can do to prevent the :ref:`ErrorReturnCode ` +exceptions from being raised after it completes. + diff --git a/docs/source/sections/asynchronous_execution.rst b/docs/source/sections/asynchronous_execution.rst new file mode 100644 index 00000000..d10b9d27 --- /dev/null +++ b/docs/source/sections/asynchronous_execution.rst @@ -0,0 +1,172 @@ +.. _async: + +Asynchronous Execution +###################### + +sh provides a few methods for running commands and obtaining output in a +non-blocking fashion. + +.. _iterable: + +Incremental Iteration +===================== + +You may also create asynchronous commands by iterating over them with the +:ref:`iter` special kwarg. This creates an iterable (specifically, a generator) +that you can loop over: + +.. code-block:: python + + from sh import tail + + # runs forever + for line in tail("-f", "/var/log/some_log_file.log", _iter=True): + print(line) + +By default, :ref:`iter` iterates over STDOUT, but you can change set this +specifically by passing either ``"err"`` or ``"out"`` to :ref:`iter` (instead of +``True``). Also by default, output is line-buffered, so the body of the loop +will only run when your process produces a newline. You can change this by +changing the buffer size of the command's output with :ref:`out_bufsize`. + +.. note:: + + If you need a *fully* non-blocking iterator, use :ref:`iter_noblock`. If + the current iteration would block, :attr:`errno.EWOULDBLOCK` will be + returned, otherwise you'll receive a chunk of output, as normal. + +.. _background: + +Background Processes +==================== + +By default, each running command blocks until completion. If you have a +long-running command, you can put it in the background with the :ref:`_bg=True +` special kwarg: + +.. code-block:: python + + # blocks + sleep(3) + print("...3 seconds later") + + # doesn't block + p = sleep(3, _bg=True) + print("prints immediately!") + p.wait() + print("...and 3 seconds later") + +You'll notice that you need to call :meth:`RunningCommand.wait` in order to exit +after your command exits. + +Commands launched in the background ignore ``SIGHUP``, meaning that when their +controlling process (the session leader, if there is a controlling terminal) +exits, they will not be signalled by the kernel. But because sh commands launch +their processes in their own sessions by default, meaning they are their own +session leaders, ignoring ``SIGHUP`` will normally have no impact. So the only +time ignoring ``SIGHUP`` will do anything is if you use :ref:`_new_session=False +`, in which case the controlling process will probably be the shell +from which you launched python, and exiting that shell would normally send a +``SIGHUP`` to all child processes. + +.. seealso:: + + For more information on the exact launch process, see :ref:`architecture`. + +.. _callbacks: + +Output Callbacks +---------------- + +In combination with :ref:`_bg=True`, sh can use callbacks to process output +incrementally by passing a callable function to :ref:`out` and/or :ref:`err`. +This callable will be called for each line (or chunk) of data that your command +outputs: + +.. code-block:: python + + from sh import tail + + def process_output(line): + print(line) + + p = tail("-f", "/var/log/some_log_file.log", _out=process_output, _bg=True) + p.wait() + +To control whether the callback receives a line or a chunk, use +:ref:`out_bufsize`. To "quit" your callback, simply return ``True``. This +tells the command not to call your callback anymore. + +The line or chunk received by the callback can either be of type ``str`` or +``bytes``. If the output could be decoded using the provided :ref:`encoding`, a +``str`` will be passed to the callback, otherwise it would be raw ``bytes``. + +.. note:: + + Returning ``True`` does not kill the process, it only keeps the callback + from being called again. See :ref:`interactive_callbacks` for how to kill a + process from a callback. + +.. seealso:: :ref:`red_func` + +.. _interactive_callbacks: + +Interactive callbacks +--------------------- + +Commands may communicate with the underlying process interactively through a +specific callback signature +Each command launched through sh has an internal STDIN :class:`queue.Queue` +that can be used from callbacks: + +.. code-block:: python + + def interact(line, stdin): + if line == "What... is the air-speed velocity of an unladen swallow?": + stdin.put("What do you mean? An African or European swallow?") + + elif line == "Huh? I... I don't know that....AAAAGHHHHHH": + cross_bridge() + return True + + else: + stdin.put("I don't know....AAGGHHHHH") + return True + + p = sh.bridgekeeper(_out=interact, _bg=True) + p.wait() + +.. note:: + + If you use a queue, you can signal the end of the input (EOF) with ``None`` + +You can also kill or terminate your process (or send any signal, really) from +your callback by adding a third argument to receive the process object: + +.. code-block:: python + + def process_output(line, stdin, process): + print(line) + if "ERROR" in line: + process.kill() + return True + + p = tail("-f", "/var/log/some_log_file.log", _out=process_output, _bg=True) + +The above code will run, printing lines from ``some_log_file.log`` until the +word ``"ERROR"`` appears in a line, at which point the tail process will be +killed and the script will end. + +.. note:: + + You may also use :meth:`RunningCommand.terminate` to send a SIGTERM, or + :meth:`RunningCommand.signal` to send a general signal. + + +Done Callbacks +-------------- + +A done callback called when the process exits, either normally (through +a success or error exit code) or through a signal. It is *always* called. + +.. include:: /examples/done.rst diff --git a/docs/source/sections/baking.rst b/docs/source/sections/baking.rst new file mode 100644 index 00000000..8f4070fd --- /dev/null +++ b/docs/source/sections/baking.rst @@ -0,0 +1,50 @@ +.. _baking: + +Baking +====== + +sh is capable of "baking" arguments into commands. This is essentially +`partial application `_, +like you might do with :func:`functools.partial`. + +.. code-block:: python + + from sh import ls + + ls = ls.bake("-la") + print(ls) # "/usr/bin/ls -la" + + # resolves to "ls -la /" + print(ls("/")) + +The idea here is that now every call to ``ls`` will have the "-la" arguments +already specified. Baking can become very useful when you combine it with +:ref:`subcommands`: + +.. code-block:: python + + from sh import ssh + + # calling whoami on a server. this is a lot to type out, especially if + # you wanted to call many commands (not just whoami) back to back on + # the same server + iam1 = ssh("myserver.com", "-p 1393", "whoami") + + # wouldn't it be nice to bake the common parameters into the ssh command? + myserver = ssh.bake("myserver.com", p=1393) + + print(myserver) # "/usr/bin/ssh myserver.com -p 1393" + + # resolves to "/usr/bin/ssh myserver.com -p 1393 whoami" + iam2 = myserver.whoami() + + assert(iam1 == iam2) # True! + +Now that the "myserver" callable represents a baked ssh command, you +can call anything on the server easily: + +.. code-block:: python + + # executes "/usr/bin/ssh myserver.com -p 1393 tail /var/log/dumb_daemon.log -n 100" + print(myserver.tail("/var/log/dumb_daemon.log", n=100)) + diff --git a/docs/source/sections/command_class.rst b/docs/source/sections/command_class.rst new file mode 100644 index 00000000..9c00b96e --- /dev/null +++ b/docs/source/sections/command_class.rst @@ -0,0 +1,384 @@ +API +### + + +.. _command_class: + +Command Class +============== + +The ``Command`` class represents a program that exists on the system and can be +run at some point in time. An instance of ``Command`` is never running; an +instance of :ref:`RunningCommand ` is spawned for that. + +An instance of ``Command`` can take the form of a manually instantiated object, +or as an object instantiated by dynamic lookup: + +.. code-block:: python + + import sh + + ls1 = sh.Command("ls") + ls2 = sh.ls + + assert ls1 == ls2 + + +.. py:class:: Command(name, search_paths=None) + + Instantiates a Command instance, where *name* is the name of a program that + exists on the user's ``$PATH``, or is a full path itself. If *search_paths* + is specified, it must be a list of all the paths to look for the program + name. + + .. code-block:: python + + from sh import Command + + ifconfig = Command("ifconfig") + ifconfig = Command("/sbin/ifconfig") + + +.. py:method:: Command.bake(*args, **kwargs) + + Returns a new Command with ``*args`` and ``**kwargs`` baked in as + positional and keyword arguments, respectively. Any future calls to the + returned Command will include ``*args`` and ``**kwargs`` automatically: + + .. code-block:: python + + from sh import ls + + long_ls = ls.bake("-l") + print(ls("/var")) + print(ls("/tmp")) + + + .. seealso:: + + :ref:`baking` + + +Similar to the above, arguments to the ``sh.Command`` must be separate. +e.g. the following does not work:: + + lscmd = sh.Command("/bin/ls -l") + tarcmd = sh.Command("/bin/tar cvf /tmp/test.tar /my/home/directory/") + +You will run into ``CommandNotFound(path)`` exception even when correct full path is specified. +The correct way to do this is to : + +#. build ``Command`` object using *only* the binary +#. pass the arguments to the object *when invoking* + +as follows:: + + lscmd = sh.Command("/bin/ls") + lscmd("-l") + tarcmd = sh.Command("/bin/tar") + tarcmd("cvf", "/tmp/test.tar", "/my/home/directory/") + +.. _running_command: + +RunningCommand Class +==================== + +This represents a :ref:`Command ` instance that has been +or is being executed. It exists as a wrapper around the low-level :ref:`OProc +`. Most of your interaction with sh objects are with instances of +this class + +.. warning:: + + Objects of this class behave very much like strings. This was an + intentional design decision to make the "output" of an executing Command + behave more intuitively. + + Be aware that functions that accept real strings only, for example + ``json.dumps``, will not work on instances of RunningCommand, even though it + look like a string. + +.. _wait_method: + +.. py:method:: RunningCommand.wait(timeout=None) + + :param timeout: An optional non-negative number to wait for the command to complete. If it doesn't complete by the + timeout, we raise :ref:`timeout_exc`. + + Block and wait for the command to finish execution and obtain an exit code. + If the exit code represents a failure, we raise the appropriate exception. + See :ref:`exceptions `. + + .. note:: + + Calling this method multiple times only yields an exception on the first + call. + + This is called automatically by sh unless your command is being executed + :ref:`asynchronously `, in which case, you may want to call this + manually to ensure completion. + + If an instance of :ref:`Command ` is being used as the stdin + argument (see :ref:`piping `), :meth:`wait` is also called on that + instance, and any exceptions resulting from that process are propagated up. + +.. py:attribute:: RunningCommand.process + + The underlying :ref:`OProc ` instance. + +.. py:attribute:: RunningCommand.stdout + + A ``@property`` that calls :meth:`wait` and then returns the contents of + what the process wrote to stdout. + +.. py:attribute:: RunningCommand.stderr + + A ``@property`` that calls :meth:`wait` and then returns the contents of + what the process wrote to stderr. + +.. py:attribute:: RunningCommand.exit_code + + A ``@property`` that calls :meth:`wait` and then returns the process's exit + code. + +.. py:attribute:: RunningCommand.pid + + The process id of the process. + +.. py:attribute:: RunningCommand.sid + + The session id of the process. This will typically be a different session + than the current python process, unless :ref:`_new_session=False + ` was specified. + +.. py:attribute:: RunningCommand.pgid + + The process group id of the process. + +.. py:attribute:: RunningCommand.ctty + + The controlling terminal device, if there is one. + +.. py:method:: RunningCommand.signal(sig_num) + + Sends *sig_num* to the process. Typically used with a value from the + :mod:`signal` module, like :attr:`signal.SIGHUP` (see :manpage:`signal(7)`). + +.. py:method:: RunningCommand.signal_group(sig_num) + + Sends *sig_num* to every process in the process group. Typically used with + a value from the :mod:`signal` module, like :attr:`signal.SIGHUP` (see + :manpage:`signal(7)`). + +.. py:method:: RunningCommand.terminate() + + Shortcut for :meth:`RunningCommand.signal(signal.SIGTERM) + `. + +.. py:method:: RunningCommand.kill() + + Shortcut for :meth:`RunningCommand.signal(signal.SIGKILL) + `. + +.. py:method:: RunningCommand.kill_group() + + Shortcut for :meth:`RunningCommand.signal_group(signal.SIGKILL) + `. + +.. py:method:: RunningCommand.is_alive() + + Returns whether or not the process is still alive. + + :rtype: boolean + +.. _oproc_class: + +OProc Class +=========== + +.. warning:: + + Don't use instances of this class directly. It is being documented here for + posterity, not for direct use. + +.. py:method:: OProc.wait() + + Block until the process completes, aggregate the output, and populate + :attr:`OProc.exit_code`. + +.. py:attribute:: OProc.stdout + + A :class:`collections.deque`, sized to :ref:`_internal_bufsize + ` items, that contains the process's STDOUT. + +.. py:attribute:: OProc.stderr + + A :class:`collections.deque`, sized to :ref:`_internal_bufsize + ` items, that contains the process's STDERR. + +.. py:attribute:: OProc.exit_code + + Contains the process's exit code, or ``None`` if the process has not yet + exited. + +.. py:attribute:: OProc.pid + + The process id of the process. + +.. py:attribute:: OProc.sid + + The session id of the process. This will typically be a different session + than the current python process, unless :ref:`_new_session=False + ` was specified. + +.. py:attribute:: OProc.pgid + + The process group id of the process. + +.. py:attribute:: OProc.ctty + + The controlling terminal device, if there is one. + +.. py:method:: OProc.signal(sig_num) + + Sends *sig_num* to the process. Typically used with a value from the + :mod:`signal` module, like :attr:`signal.SIGHUP` (see :manpage:`signal(7)`). + +.. py:method:: OProc.signal_group(sig_num) + + Sends *sig_num* to every process in the process group. Typically used with + a value from the :mod:`signal` module, like :attr:`signal.SIGHUP` (see + :manpage:`signal(7)`). + +.. py:method:: OProc.terminate() + + Shortcut for :meth:`OProc.signal(signal.SIGTERM) `. + +.. py:method:: OProc.kill() + + Shortcut for :meth:`OProc.signal(signal.SIGKILL) `. + +.. py:method:: OProc.kill_group() + + Shortcut for :meth:`OProc.signal_group(signal.SIGKILL) + `. + +Exceptions +========== + +.. _error_return_code: + +ErrorReturnCode +--------------- + +.. py:class:: ErrorReturnCode + + This is the base class for, as the name suggests, error return codes. It + subclasses :data:`exceptions.Exception`. + +.. py:attribute:: ErrorReturnCode.full_cmd + + The full command that was executed, as a string, so that you can try it on + the commandline if you wish. + +.. py:attribute:: ErrorReturnCode.stdout + + The total aggregated STDOUT for the process. + +.. py:attribute:: ErrorReturnCode.stderr + + The total aggregated STDERR for the process. + +.. py:attribute:: ErrorReturnCode.exit_code + + The process's adjusted exit code. + + .. seealso:: :ref:`arch_exit_code` + + +.. _signal_exc: + +SignalException +--------------- + +Subclasses :ref:`ErrorReturnCode `. Raised when a command +receives a signal that causes it to exit. + +.. _timeout_exc: + +TimeoutException +---------------- + +Raised when a command specifies a non-null :ref:`timeout` and the command times out: + +.. code-block:: python + + import sh + + try: + sh.sleep(10, _timeout=1) + except sh.TimeoutException: + print("we timed out, as expected") + +Also raised when you specify a timeout to :ref:`RunningCommand.wait(timeout=None)`: + +.. code-block:: python + + import sh + + p = sh.sleep(10, _bg=True) + try: + p.wait(timeout=1) + except sh.TimeoutException: + print("we timed out waiting") + p.kill() + +.. _not_found_exc: + +CommandNotFound +--------------- + +This exception is raised in one of the following conditions: + +* The program cannot be found on your path. +* You do not have permissions to execute the program. +* The program is not marked executable. + +The last two bullets may seem strange, but they fall in line with how a shell like Bash behaves when looking up a +program to execute. + +.. note:: + + ``CommandNotFound`` subclasses ``AttributeError``. As such, the `repr` of it is simply the name of the missing + attribute. + + +Helper Functions +================ + +.. py:function:: which(name, search_paths=None) + + Resolves *name* to program's absolute path, or ``None`` if it cannot be + found. If *search_paths* is list of paths, use that list to look for the + program, otherwise use the environment variable ``$PATH``. + +.. py:function:: pushd(directory) + + This function provides a ``with`` context that behaves similar to Bash's + `pushd + `_ + by pushing to the provided directory, and popping out of it at the end of + the context. + + .. code-block:: python + + import sh + + with sh.pushd("/tmp"): + sh.touch("a_file") + + .. note:: + + It should be noted that we use a reentrant lock, so that different threads + using this function will have the correct behavior inside of their ``with`` + contexts. diff --git a/docs/source/sections/contrib.rst b/docs/source/sections/contrib.rst new file mode 100644 index 00000000..1a5139c4 --- /dev/null +++ b/docs/source/sections/contrib.rst @@ -0,0 +1,179 @@ +.. _contrib: + +Contrib Commands +################ + +Contrib is an sh sub-module that provides friendly wrappers to useful commands. +Typically, the commands being wrapped are unintuitive, and the contrib version +makes them intuitive. + +.. note:: + + Contrib commands should be considered generally unstable. They will grow and change as the community figures out the + best interface for them. + +Commands +======== + +Sudo +---- + +Allows you to enter your password from the terminal at runtime, or as a string +in your script. + +.. py:function:: sudo(password=None, *args, **kwargs) + + Call sudo with ``password``, if specified, else ask the executing user for a + password at runtime via :func:`getpass.getpass`. + +.. seealso:: :ref:`contrib_sudo` + +.. _contrib_git: + +Git +--- + +Many git commands use a pager for output, which can cause an unexpected behavior +when run through sh. To account for this, the contrib version sets +``_tty_out=False`` for all git commands. + +.. py:function:: git(*args, **kwargs) + + Call git with STDOUT connected to a pipe, instead of a TTY. + +.. code-block:: python + + from sh.contrib import git + repo_log = git.log() + +.. seealso:: :ref:`faq_tty_out` and :ref:`faq_color_output` + +.. _contrib_ssh: + +SSH +--- + +.. versionadded:: 1.13.0 + +SSH password-based logins :ref:`can be a pain `. This contrib command performs all of the ugly setup and +provides a clean interface to using SSH. + +.. py:function:: ssh(interact=None, password=None, prompt_match=None, login_success=None, *args, **kwargs) + + :param interact: A callback to handle SSH session interaction *after* login is successful. Required. + :param password: A password string or a function that returns a password string. Optional. If not provided, :func:`getpass.getpass` is used. + :param prompt_match: The string to match in order to determine when to provide SSH with the password. Or a function + that matches on the output. Optional. + :param login_success: A function to determine if SSH login is successful. Optional. + +The ``interact`` parameter takes a callback with a signature that is slightly different to the function callbacks for +:ref:`redirection `: + +.. py:function:: fn(content, stdin_queue) + + :param content: An instance of an ephemeral :ref:`SessionContent ` class whose job is to hold the + characters that the SSH session has written to STDOUT. + :param stdin_queue: A :class:`queue.Queue` object to communicate with STDIN programmatically. + +``password`` can be simply a string that will be used to type the password. If it's not provided, it will be read from STDIN +at runtime via :func:`getpass.getpass`. It can also be a callable that returns the password string. + +``prompt_match`` is a string to match before the contrib command will provide the SSH process with the password. It is +optional, and if left unspecified, will default to "password: ". It can also be a callable that is called on a +:ref:`SessionContent ` instance and returns ``True`` or ``False`` for a match. + +``login_success`` is a function that takes a :ref:`SessionContent ` object and returns a boolean for +whether or not a successful login occurred. It is optional, and if unspecified, simply evaluates to ``True``, meaning +any password submission results in a successful login (obviously not always correct). It is recommended that you specify +this. + +.. _session_content: + +.. py:class:: SessionContent() + + This class contains a record lines and characters written to the SSH processes's STDOUT. It should be all you need + from the callbacks to determine how to interact with the SSH process. + +.. py:attribute:: SessionContent.chars + + :type: :class:`collections.deque` + + The previous 50,000 characters. + +.. py:attribute:: SessionContent.lines + + :type: :class:`collections.deque` + + The previous 5,000 lines. + +.. py:attribute:: SessionContent.line_chars + + :type: list + + The characters in the line currently being aggregated. + +.. py:attribute:: SessionContent.cur_line + + :type: string + + A string of the line currently being aggregated. + +.. py:attribute:: SessionContent.last_line + + :type: string + + The previous line. + +.. py:attribute:: SessionContent.cur_char + + :type: string + + The currently written character. + +Extending +========= + +For developers. + +To extend contrib, simply decorate a function in sh with the ``@contrib`` +decorator, and pass in the name of the command you wish to shadow to the +decorator. This method must return an instance of :ref:`Command +`: + +.. code-block:: python + + @contrib("ls") + def my_ls(original): + ls = original.bake("-l") + return ls + +Now you can run your custom contrib command from your scripts, and you'll be +using the command returned from your decorated function: + + +.. code-block:: python + + from sh.contrib import ls + + # executing: ls -l + print(ls("/")) + +For even more flexibility, you can design your contrib command to rewrite its +options based on *executed* arguments. For example, say you only wish to set a +command's argument if another argument is set. You can accomplish it like this: + +.. code-block:: python + + @contrib("ls") + def my_ls(original): + def process(args, kwargs): + if "-a" in args: + args.append("-L") + return args, kwargs + + ls = original.bake("-l") + return ls, process + +Returning a process function along with the command will tell sh to use that +function to preprocess the arguments at execution time using the +:ref:`_arg_preprocess ` special kwarg. diff --git a/docs/source/sections/default_arguments.rst b/docs/source/sections/default_arguments.rst new file mode 100644 index 00000000..d1eba3c5 --- /dev/null +++ b/docs/source/sections/default_arguments.rst @@ -0,0 +1,55 @@ +.. _default_arguments: + +Default Arguments +================= + +Many times, you want to override the default arguments of all commands launched +through sh. For example, suppose you want the output of all commands to be +aggregated into a :class:`io.StringIO` buffer. The naive way would be this: + +.. code-block:: python + + import sh + from io import StringIO + + buf = StringIO() + + sh.ls("/", _out=buf) + sh.whoami(_out=buf) + sh.ps("auxwf", _out=buf) + +Clearly, this gets tedious quickly. Fortunately, we can create execution +contexts that allow us to set default arguments on all commands spawned from +that context: + +.. code-block:: python + + import sh + from io import StringIO + + buf = StringIO() + sh2 = sh(_out=buf) + + sh2.ls("/") + sh2.whoami() + sh2.ps("auxwf") + +Now, anything launched from ``sh2`` will send its output to the ``StringIO`` +instance ``buf``. + +Execution contexts may also be imported from, like it is the top-level sh +module: + +.. code-block:: python + + import sh + from io import StringIO + + buf = StringIO() + sh2 = sh(_out=buf) + + from sh2 import ls, whoami, ps + + ls("/") + whoami() + ps("auxwf") diff --git a/docs/source/sections/envs.rst b/docs/source/sections/envs.rst new file mode 100644 index 00000000..a153e7fa --- /dev/null +++ b/docs/source/sections/envs.rst @@ -0,0 +1,33 @@ +.. _environments: + +Environments +============ + +The :ref:`_env ` special kwarg allows you to pass a dictionary of +environment variables and their corresponding values: + +.. code-block:: python + + import sh + sh.google_chrome(_env={"SOCKS_SERVER": "localhost:1234"}) + + +:ref:`_env ` replaces your process's environment completely. Only the +key-value pairs in :ref:`_env ` will be used for its environment. If you +want to add new environment variables for a process *in addition to* your +existing environment, try something like this: + +.. code-block:: python + + import os + import sh + + new_env = os.environ.copy() + new_env["SOCKS_SERVER"] = "localhost:1234" + + sh.google_chrome(_env=new_env) + +.. seealso:: + + To make an environment apply to all sh commands look into + :ref:`default_arguments`. diff --git a/docs/source/sections/exit_codes.rst b/docs/source/sections/exit_codes.rst new file mode 100644 index 00000000..f19dac6d --- /dev/null +++ b/docs/source/sections/exit_codes.rst @@ -0,0 +1,59 @@ +.. _exit_codes: + +Exit Codes & Exceptions +======================= + +Normal processes exit with exit code 0. This can be seen through a +:attr:`RunningCommand.exit_code`: + +.. code-block:: python + + output = ls("/") + print(output.exit_code) # should be 0 + +If a process terminates, and the exit code is not 0, an exception is generated +dynamically. This lets you catch a specific return code, or catch all error +return codes through the base class :class:`ErrorReturnCode`: + +.. code-block:: python + + try: + print(ls("/some/non-existant/folder")) + except ErrorReturnCode_2: + print("folder doesn't exist!") + create_the_folder() + except ErrorReturnCode: + print("unknown error") + +You can also customize which exit codes indicate an error with :ref:`ok_code`. For example: + +.. code-block:: python + + for i in range(10): + sh.grep("string to check", f"file_{i}.txt", _ok_code=(0, 1)) + +where the :ref:`ok_code` makes a failure to find a match a no-op. + +Signals +------- + +Signals are raised whenever your process terminates from a signal. The +exception raised in this situation is :ref:`signal_exc`, which subclasses +:ref:`error_return_code`. + +.. code-block:: python + + try: + p = sh.sleep(3, _bg=True) + p.kill() + except sh.SignalException_SIGKILL: + print("killed") + +.. note:: + + You can catch :ref:`signal_exc` by using either a number or a signal name. + For example, the following two exception classes are equivalent: + + .. code-block:: python + + assert sh.SignalException_SIGKILL == sh.SignalException_9 diff --git a/docs/source/sections/faq.rst b/docs/source/sections/faq.rst new file mode 100644 index 00000000..d0bff677 --- /dev/null +++ b/docs/source/sections/faq.rst @@ -0,0 +1,451 @@ +.. _faq: + +FAQ +=== + +How do I execute a bash builtin? +-------------------------------- + +.. code-block:: python + + import sh + + sh.bash("-c", "your_builtin") + +Or + +.. code-block:: python + + import sh + + builtins = sh.bash.bake("-c") + builtins("your_builtin") + + +Will Windows be supported? +-------------------------- + +There are no plans to support Windows. + +.. _faq_append: + +How do I append output to a file? +--------------------------------- + +Use a file object opened in the mode you desire: + +.. code-block:: python + + import sh + + h = open("/tmp/output", "a") + + sh.ls("/dir1", _out=h) + sh.ls("/dir2", _out=h) + +.. _faq_color_output: + +Why does my command's output have color? +---------------------------------------- + +Typically the reason for this is that your program detected that its STDOUT was +connected to a TTY, and therefore decided to print color escape sequences in its +output. The typical solution is to use :ref:`_tty_out=False `, which +will force a pipe to be connected to STDOUT, and probably change the behavior of +the program. + +.. seealso:: + + Git is one of the programs that makes extensive use of terminal colors (as + well as pagers) in its output, so we added :ref:`a contrib version + ` for convenience. + +.. _faq_tty_out: + +Why is _tty_out=True the default? +--------------------------------- + +This was a design decision made for two reasons: + +1. To make programs behave in the same way as seen on the commandline. +2. To provide better buffering control than pipes allow. + +For #1, we want sh to produce output that is identical to what the user sees +from the commandline, because that's typically the only output they ever see +from their command. This makes the output easy to understand. + +For #2, using a TTY for STDOUT allows us to precisely control the buffering of a +command's output to sh's internal code. + +.. seealso:: :ref:`arch_buffers` + +Of course, there are some gotchas with TTY STDOUT. One of them is commands that +use a pager, for example: + +.. code-block:: python + + import sh + print(sh.git.log()) + + +This will sometimes raise a ``SignalException_SIGPIPE``. The reason is because +``git log`` detects a TTY STDOUT and forks the system’s pager (typically +``less``) to handle the output. The pager checks for a controlling terminal, +and, finding none, exits with exit code 1. The exit of the pager means no more +readers on ``git log``’s output, and thus a ``SIGPIPE`` is received. + +One solution to the ``git log`` problem above is simply to use +``_tty_out=False``. Another option, specifically for git, is to use the +``git --no-pager`` option: + +.. code-block:: python + + import sh + print(sh.git('--no-pager', 'log')) + + +Why doesn't "*" work as a command argument? +------------------------------------------- + +Glob expansion is a feature of a shell, like Bash, and is performed by the shell +before passing the results to the program to be exec'd. Because sh is not a +shell, but rather tool to execute programs directly, we do not handle glob +expansion like a shell would. + +So in order to use ``"*"`` like you would on the commandline, pass it into +:func:`glob.glob` first: + +.. code-block:: python + + import sh + import glob + sh.ls(glob.glob("*.py")) + + +.. _faq_path: + +How do I call a program that isn't in ``$PATH``? +------------------------------------------------ + +Use the :meth:`Command` constructor to instantiate an instance of Command +directly, then execute that: + +.. code-block:: python + + import sh + cmd = sh.Command("/path/to/command") + cmd("-v", "arg1") + +How do I execute a program with a dash in its name? +--------------------------------------------------- + +If it's in your ``$PATH``, substitute the dash for an underscore: + +.. code-block:: python + + import sh + sh.google_chrome("http://google.com") + +The above will run ``google-chrome http://google.com`` + +.. note:: + + If a program named ``google_chrome`` exists on your system, that will be + called instead. In that case, in order to execute the program with a dash + in the name, you'll have to use the method described :ref:`here. + ` + +.. _faq_special: + +How do I execute a program with a special character in its name? +---------------------------------------------------------------- + +Programs with non-alphanumeric, non-dash characters in their names cannot be +executed directly as an attribute on the sh module. For example, **this will not +work:** + +.. code-block:: python + + import sh + sh.mkfs.ext4() + +The reason should be fairly obvious. In Python, characters like ``.`` have +special meaning, in this case, attribute access. What sh is trying to do in the +above example is find the program "mkfs" (which may or may not exist) and then +perform a :ref:`subcommand lookup ` with the name "ext4". In other +words, it will try to call ``mkfs`` with the argument ``ext4``, which is +probably not what you want. + +The workaround is instantiating the :ref:`Command Class ` with +the string of the program you're looking for: + +.. code-block:: python + + import sh + mkfsext4 = sh.Command("mkfs.ext4") + mkfsext4() # run it + +.. _faq_pipe_syntax: + + +Why not use ``|`` to pipe commands? +----------------------------------- + +I prefer the syntax of sh to resemble function composition instead of a +pipeline. One of the goals of sh is to make executing processes more like +calling functions, not making function calls more like Bash. + +Why isn't piping asynchronous by default? +----------------------------------------- + +There is a non-obvious reason why async piping is not possible by default. +Consider the following example: + +.. code-block:: python + + import sh + + sh.cat(sh.echo("test\n1\n2\n3\n")) + +When this is run, ``sh.echo`` executes and finishes, then the entire output +string is fed into ``sh.cat``. What we would really like is each +newline-delimited chunk to flow to ``sh.cat`` incrementally. + +But for this example to flow data asynchronously from echo to cat, the echo +command would need to *not block.* But how can the inner command know the +context of its execution, to know to block sometimes but not other times? It +can't know that without something explicit. + +This is why the :ref:`piped` special kwarg was introduced. By default, commands +executed block until they are finished, so in order for an inner command to not +block, ``_piped=True`` signals to the inner command that it should not block. +This way, the inner command starts running, then very shortly after, the outer +command starts running, and both are running simultaneously. Data can then flow +from the inner command to the outer command asynchronously: + +.. code-block:: python + + import sh + + sh.cat(sh.echo("test\n1\n2\n3\n", _piped=True)) + +Again, this example is contrived -- a better example would be a long-running +command that produces a lot of output that you wish to pipe through another +program incrementally. + +How do I run a command and connect it to sys.stdout and sys.stdin? +------------------------------------------------------------------ + +There are two ways to do this + +.. seealso:: :ref:`fg` + +You can use :data:`sys.stdin`, :data:`sys.stdout`, and :data:`sys.stderr` as +arguments to :ref:`in`, :ref:`out`, :ref:`err`, respectively, and it *should* +mostly work as expected: + +.. code-block:: python + + import sh + import sys + sh.your_command(_in=sys.stdin, _out=sys.stdout) + +There are a few reasons why this probably won't work. The first reason is that +:data:`sys.stdin` is probably a controlling TTY (attached to the shell that +launched the python process), and probably not set in raw mode +:manpage:`termios(3)`, which means that, among other things, input is buffered +by newlines. + +The real solution is to use :ref:`_fg=True `: + +.. code-block:: python + + import sh + sh.top(_fg=True) + + +.. _faq_separate_args: + +Why do my arguments need to be separate strings? +------------------------------------------------ + +This confuses many new sh users. They want to do something like this and expect +it to just work: + +.. code-block:: python + + from sh import tar + tar("cvf /tmp/test.tar /my/home/directory") + +But instead they'll get a confusing error message: + +.. code-block:: none + + RAN: '/bin/tar cvf /tmp/test.tar /my/home/directory' + + STDOUT: + + STDERR: + /bin/tar: Old option 'f' requires an argument. + Try '/bin/tar --help' or '/bin/tar --usage' for more information. + +The reason why they expect it to work is because shells, like Bash, automatically +parse your commandline and break up arguments for you, before sending them to +the binary. They have a complex set of rules (some of which are represented by +:mod:`shlex`) to take a single string of a command and arguments and separate +them. + +Even if we wanted to implement this in sh (which we don't), it would hurt the +ability for users to parameterize parts of their arguments. They would have to +use string interpolation, which would be ugly and error prone: + +.. code-block:: python + + from sh import tar + tar("cvf %s %s" % ("/tmp/tar1.tar", "/home/oh no a space") + +In the above example, ``"/home/oh"``, ``"no"``, ``"a"``, and ``"space"`` would +all be separate arguments to tar, causing the program to behave unexpectedly. +Basically every command with parameterized arguments would need to expect +characters that could break the parser. + +.. _faq_arg_ordering: + +How do I order keyword arguments? +--------------------------------- + +Typically this question gets asked when a user is trying to execute something +like the following commandline: + +.. code-block:: none + + my-command --arg1=val1 arg2 --arg3=val3 + +This is usually the first attempt that they make: + +.. code-block:: python + + sh.my_command(arg1="val1", "arg2", arg3="val3") + +This doesn't work because, in Python, position arguments, like ``arg2`` cannot +come after keyword arguments. + +Furthermore, it is entirely possible that ``--arg3=val3`` comes before +``--arg1=val1``. The reason for this is that a function's ``**kwargs`` is an +unordered mapping, and so key-value pairs are not guaranteed to resolve to a +specific order. + +So the solution here is to forego the usage of the keyword argument +*convenience*, and just use raw ordered arguments: + +.. code-block:: python + + sh.my_command("--arg1=val1", "arg2", "--arg3=val3") + +.. _faq_pylint: + +How to disable pylint E1101 no-member errors? +--------------------------------------------- + +Pylint complains with E1101 no-member to almost all ``sh.command`` invocations, +because it doesn't know, that these members are generated dynamically. +Starting with Pylint 1.6 these messages can be suppressed using `generated-members `_ option. + +Just add following lines to ``pylintrc``:: + + [TYPECHECK] + generated-members=sh + + +How do I patch sh in my tests? +------------------------------ + +sh can be patched in your tests the typical way, with +:func:`unittest.mock.patch`: + +.. code-block:: python + + from unittest.mock import patch + import sh + + def get_something(): + return sh.pwd() + + @patch("sh.pwd", create=True) + def test_something(pwd): + pwd.return_value = "/" + assert get_something() == "/" + +The important thing to note here is that ``create=True`` is set. This is +required because sh is a bit magical and ``patch`` will fail to find the ``pwd`` +command as an attribute on the sh module. + +You may also patch the :class:`Command` class: + +.. code-block:: python + + from unittest.mock import patch + import sh + + def get_something(): + pwd = sh.Command("pwd") + return pwd() + + @patch("sh.Command") + def test_something(Command): + Command().return_value = "/" + assert get_something() == "/" + +Notice here we do not need ``create=True``, because :class:`Command` is not an +automatically generated object on the sh module (it actually exists). + + +Why is sh just a single file? +----------------------------- + +When sh was first written, the design decision was made to make it a single-file +module. This has pros and cons: + +Cons: + +- Auditing the code is more challenging +- Without file-enforced structure, adding more features and abstractions makes + the code harder to follow +- Cognitively, it feels cluttered + +Pros: + +- Can be used easily on systems without Python package managers +- Can be embedded/bundled together with other software more easily +- Cognitively, it feels more self-contained + +In my mind, because the primary target audience of sh users is generally more +scrappy devops, systems people, or people just trying to stitch together some +clunky system programs, the listed pros weigh a little more heavily than the +cons. Sacrificing some development advantages to give those users a more +flexible tool is a win to me. + +Down the road, the development disadvantages of a single file can be solved with +additional development tools, for example, with a tool that compiles multiple +modules into the single sh.py file. Realistically, though, sh is pretty mature, +so I don't see it growing much more in complexity or code size. + +How do I see the commands sh is running? +---------------------------------------- + +Use logging: + +.. code-block:: python + + import logging + import sh + + logging.basicConfig(level=logging.INFO) + sh.ls() + +.. code-block:: none + + INFO:sh.command:: starting process + INFO:sh.command:: process started + INFO:sh.command:: process completed + ... diff --git a/docs/source/sections/passing_arguments.rst b/docs/source/sections/passing_arguments.rst new file mode 100644 index 00000000..94f04413 --- /dev/null +++ b/docs/source/sections/passing_arguments.rst @@ -0,0 +1,44 @@ +.. _passing_arguments: + +Passing Arguments +================= + +When passing multiple arguments to a command, each argument *must* be a separate +string: + +.. code-block:: python + + from sh import tar + tar("cvf", "/tmp/test.tar", "/my/home/directory/") + +This *will not work*: + +.. code-block:: python + + from sh import tar + tar("cvf /tmp/test.tar /my/home/directory") + +.. seealso:: :ref:`faq_separate_args` + + +Keyword Arguments +----------------- + +sh supports short-form ``-a`` and long-form ``--arg`` arguments as +keyword arguments: + +.. code-block:: python + + # resolves to "curl http://duckduckgo.com/ -o page.html --silent" + curl("http://duckduckgo.com/", o="page.html", silent=True) + + # or if you prefer not to use keyword arguments, this does the same thing: + curl("http://duckduckgo.com/", "-o", "page.html", "--silent") + + # resolves to "adduser amoffat --system --shell=/bin/bash --no-create-home" + adduser("amoffat", system=True, shell="/bin/bash", no_create_home=True) + + # or + adduser("amoffat", "--system", "--shell", "/bin/bash", "--no-create-home") + +.. seealso:: :ref:`faq_arg_ordering` diff --git a/docs/source/sections/piping.rst b/docs/source/sections/piping.rst new file mode 100644 index 00000000..902885fd --- /dev/null +++ b/docs/source/sections/piping.rst @@ -0,0 +1,64 @@ +.. _piping: + +Piping +====== + +Basic +----- + +Bash style piping is performed using function composition. Just pass +one command as the input to another, and sh will send the output of the inner +command to the input of the outer command: + +.. code-block:: python + + # sort this directory by biggest file + print(sort(du(glob("*"), "-sb"), "-rn")) + + # print(the number of folders and files in /etc + print(wc(ls("/etc", "-1"), "-l")) + +.. note:: + + This basic piping does not flow data through asynchronously; the inner + command blocks until it finishes, before sending its data to the outer + command. + +By default, any command that is piping another command in waits for it to +complete. This behavior can be changed with the :ref:`_piped ` special +kwarg on the command being piped, which tells it not to complete before sending +its data, but to send its data incrementally. Read ahead for examples of this. + +.. _advanced_piping: + +Advanced +-------- + +By default, all piped commands execute sequentially. What this means is that the +inner command executes first, then sends its data to the outer command: + +.. code-block:: python + + print(wc(ls("/etc", "-1"), "-l")) + +In the above example, ``ls`` executes, gathers its output, then sends that output +to ``wc``. This is fine for simple commands, but for commands where you need +parallelism, this isn't good enough. Take the following example: + +.. code-block:: python + + for line in tr(tail("-f", "test.log"), "[:upper:]", "[:lower:]", _iter=True): + print(line) + +**This won't work** because the ``tail -f`` command never finishes. What you +need is for ``tail`` to send its output to ``tr`` as it receives it. This is where +the :ref:`_piped ` special kwarg comes in handy: + +.. code-block:: python + + for line in tr(tail("-f", "test.log", _piped=True), "[:upper:]", "[:lower:]", _iter=True): + print(line) + +This works by telling ``tail -f`` that it is being used in a pipeline, and that +it should send its output line-by-line to ``tr``. By default, :ref:`piped` sends +STDOUT, but you can easily make it send STDERR instead by using ``_piped="err"`` diff --git a/docs/source/sections/redirection.rst b/docs/source/sections/redirection.rst new file mode 100644 index 00000000..33286a43 --- /dev/null +++ b/docs/source/sections/redirection.rst @@ -0,0 +1,65 @@ +.. _redirection: + +Redirection +=========== + +sh can redirect the STDOUT and STDERR of a process to many different types of +targets, using the :ref:`_out ` and :ref:`_err ` special kwargs. + +Filename +-------- + +If a string is used, it is assumed to be a filename. The filename is opened as +"wb", meaning truncate-write and binary mode. + +.. code-block:: python + + import sh + sh.ifconfig(_out="/tmp/interfaces") + +.. seealso:: :ref:`faq_append` + +File-like Object +---------------- + +You may also use any object that supports ``.write(data)``, like +:class:`io.StringIO`: + +.. code-block:: python + + import sh + from io import StringIO + + buf = StringIO() + sh.ifconfig(_out=buf) + print(buf.getvalue()) + +.. _red_func: + +Function Callback +----------------- + +A callback function may also be used as a target. The function must conform to +one of three signatures: + +.. py:function:: fn(data) + :noindex: + + The function takes just the chunk of data from the process. + +.. py:function:: fn(data, stdin_queue) + :noindex: + + In addition to the previous signature, the function also takes a + :class:`queue.Queue`, which may be used to communicate programmatically with + the process. + +.. py:function:: fn(data, stdin_queue, process) + :noindex: + + In addition to the previous signature, the function takes a + :class:`weakref.weakref` to the :ref:`OProc ` object. + +.. seealso:: :ref:`callbacks` + +.. seealso:: :ref:`tutorial2` diff --git a/docs/source/sections/special_arguments.rst b/docs/source/sections/special_arguments.rst new file mode 100644 index 00000000..01676066 --- /dev/null +++ b/docs/source/sections/special_arguments.rst @@ -0,0 +1,621 @@ +.. _special_arguments: + +.. |def| replace:: Default value: + +Special Kwargs +############## + +These arguments alter a command's behavior. They are not passed to the program. +You can use them on any command that you run, but some may not be used together. +sh will tell you if there are conflicts. + +To set default special keyword arguments on *every* command run, you may use +:ref:`default_arguments`. + +Controlling Output +================== + +.. _out: + +_out +---- +|def| ``None`` + +What to redirect STDOUT to. If this is a string, it will be treated as a file +name. You may also pass a file object (or file-like object), an int +(representing a file descriptor, like the result of :func:`os.pipe`), a +:class:`io.StringIO` object, or a callable. + +.. code-block:: python + + import sh + sh.ls(_out="/tmp/output") + +.. seealso:: + :ref:`redirection` + +.. _err: + +_err +---- +|def| ``None`` + +What to redirect STDERR to. See :ref:`_out`. + +_err_to_out +----------- +|def| ``False`` + +If ``True``, duplicate the file descriptor bound to the process's STDOUT also to +STDERR, effectively causing STDERR and STDOUT to go to the same place. + +_encoding +--------- +|def| ``sh.DEFAULT_ENCODING`` + +The character encoding of the process's STDOUT. By default, this is the +locale's default encoding. + +_decode_errors +-------------- +.. versionadded:: 1.07.0 + +|def| ``"strict"`` + +This is how Python should handle decoding errors of the process's output. +By default, this is ``"strict"``, but you can use any value that's valid +to :meth:`bytes.decode`, such as ``"ignore"``. + +_tee +---- +.. versionadded:: 1.07.0 + +|def| ``None`` + +As of 1.07.0, any time redirection is used, either for STDOUT or STDERR, the +respective internal buffers are not filled. For example, if you're downloading +a file and using a callback on STDOUT, the internal STDOUT buffer, nor the pipe +buffer be filled with data from STDOUT. This option forces one of stderr +(``_tee='err'``) or stdout (``_tee='out'`` or ``_tee=True``) to be filled +anyways, in effect "tee-ing" the output into two places (the callback/redirect +handler, and the internal buffers). + + +_truncate_exc +------------- +.. versionadded:: 1.12.0 + +|def| ``True`` + +Whether or not exception ouput should be truncated. + +Execution +========= + +.. _fg: + +_fg +--- +.. versionadded:: 1.12.0 + +|def| ``False`` + +Runs a command in the foreground, meaning it is spawned using :func:`os.spawnle()`. The current process's STDIN/OUT/ERR +is :func:`os.dup2`'d to the new process and so the new process becomes the *foreground* of the shell executing the +script. This is only really useful when you want to launch a lean, interactive process that sh is having trouble +running, for example, ssh. + +.. warning:: + + ``_fg=True`` side-steps a lot of sh's functionality. You will not be returned a process object and most (likely + all) other special kwargs will not work. + +If you are looking for similar functionality, but still retaining sh's features, use the following: + +.. code-block:: python + + import sh + import sys + sh.your_command(_in=sys.stdin, _out=sys.stdout, _err=sys.stderr) + + +.. _bg: + +_bg +--- +|def| ``False`` + +Runs a command in the background. The command will return immediately, and you +will have to run :meth:`RunningCommand.wait` on it to ensure it terminates. + +.. seealso:: :ref:`background`. + +.. _bg_exc: + +_bg_exc +------- +.. versionadded:: 1.12.9 + +|def| ``True`` + +Automatically report exceptions for the background command. If you set this to +``False`` you should make sure to call :meth:`RunningCommand.wait` or you may +swallow exceptions that happen in the background command. + +.. _env: + +_env +---- +|def| ``None`` + +A dictionary defining the only environment variables that will be made +accessible to the process. If not specified, the calling process's environment +variables are used. + +.. note:: + + This dictionary is the authoritative environment for the process. If you + wish to change a single variable in your current environement, you must pass + a copy of your current environment with the overriden variable to sh. + +.. seealso:: :ref:`environments` + +.. _timeout: + +_timeout +-------- +|def| ``None`` + +How much time, in seconds, we should give the process to complete. If the +process does not finish within the timeout, it will be sent the signal defined +by :ref:`timeout_signal`. + +.. _timeout_signal: + +_timeout_signal +--------------- +|def| ``signal.SIGKILL`` + +The signal to be sent to the process if :ref:`timeout` is not ``None``. + +_cwd +---- +|def| ``None`` + +A string that sets the current working directory of the process. + +.. _ok_code: + +_ok_code +-------- +|def| ``0`` + +Either an integer, a list, or a tuple containing the exit code(s) that are +considered "ok", or in other words: do not raise an exception. Some misbehaved +programs use exit codes other than 0 to indicate success. + +.. code-block:: python + + import sh + sh.weird_program(_ok_code=[0,3,5]) + +.. seealso:: :ref:`exit_codes` + +.. _new_session: + +_new_session +------------ +|def| ``True`` + +Determines if our forked process will be executed in its own session via +:func:`os.setsid`. + +.. note:: + + If ``_new_session`` is ``False``, the forked process will be put into its + own group via ``os.setpgrp()``. This way, the forked process, and all of + it's children, are always alone in their own group that may be signalled + directly, regardless of the value of ``_new_session``. + +.. seealso:: :ref:`architecture` + +.. _uid: + +_uid +---- +.. versionadded:: 1.12.0 + +|def| ``None`` + +The user id to assume before the child process calls :func:`os.execv`. + +_preexec_fn +----------- +.. versionadded:: 1.12.0 + +|def| ``None`` + +A function to be run directly before the child process calls :func:`os.execv`. +Typically not used by normal users. + +.. _pass_fds: + +_pass_fds +--------- +.. versionadded:: 1.13.0 + +|def| ``{}`` (empty set) + +A whitelist iterable of integer file descriptors to be inherited by the child. Passing anything in this argument causes :ref:`_close_fds ` to be ``True``. + +.. _close_fds: + +_close_fds +---------- +.. versionadded:: 1.13.0 + +|def| ``True`` + +Causes all inherited file descriptors besides stdin, stdout, and stderr to be automatically closed. This option is +automatically enabled when :ref:`_pass_fds ` is given a value. + +Communication +============= + +.. _in: + +_in +--- + +|def| ``None`` + +Specifies an argument for the process to use as its standard input. This may be +a string, a :class:`queue.Queue`, a file-like object, or any iterable. + +.. seealso:: :ref:`stdin` + +.. _piped: + +_piped +------ + +|def| ``None`` + +May be ``True``, ``"out"``, or ``"err"``. Signals a command that it is being +used as the input to another command, so it should return its output +incrementally as it receives it, instead of aggregating it all at once. + +.. seealso:: :ref:`Advanced Piping ` + +.. _iter: + +_iter +----- + +|def| ``None`` + +May be ``True``, ``"out"``, or ``"err"``. Puts a command in iterable mode. In +this mode, you can use a ``for`` or ``while`` loop to iterate over a command's +output in real-time. + +.. code-block:: python + + import sh + for line in sh.cat("/tmp/file", _iter=True): + print(line) + +.. seealso:: :ref:`iterable`. + +.. _iter_noblock: + +_iter_noblock +------------- +|def| ``None`` + +Same as :ref:`_iter `, except the loop will not block if there is no +output to iterate over. Instead, the output from the command will be +:attr:`errno.EWOULDBLOCK`. + +.. code-block:: python + + import sh + import errno + import time + + for line in sh.tail("-f", "stuff.log", _iter_noblock=True): + if line == errno.EWOULDBLOCK: + print("doing something else...") + time.sleep(0.5) + else: + print("processing line!") + + +.. seealso:: :ref:`iterable`. + +.. _with: + +_with +----- +|def| ``False`` + +Explicitly tells us that we're running a command in a ``with`` context. This is +only necessary if you're using a command in a ``with`` context **and** passing +parameters to it. + +.. code-block:: python + + import sh + with sh.contrib.sudo(password="abc123", _with=True): + print(sh.ls("/root")) + +.. seealso:: :ref:`with_contexts` + +.. _done: + +_done +----- +.. versionadded:: 1.11.0 + +|def| ``None`` + +A callback that is *always* called when the command completes, even if it +completes with an exit code that would raise an exception. After the callback +is run, any exception that would be raised is raised. + +The callback is passed the :class:`RunningCommand` instance, a boolean +indicating success, and the exit code. + +.. include:: /examples/done.rst + +TTYs +==== + +.. _tty_in: + +_tty_in +------- + +|def| ``False``, meaning a :func:`os.pipe` will be used. + +If ``True``, sh creates a TTY for STDIN, essentially emulating a terminal, as if +your command was entered from the commandline. This is necessary for commands +that require STDIN to be a TTY. + +.. _tty_out: + +_tty_out +-------- + +|def| ``True`` + +If ``True``, sh creates a TTY for STDOUT, otherwise use a :func:`os.pipe`. This +is necessary for commands that require STDOUT to be a TTY. + +.. seealso:: :ref:`faq_tty_out` + +.. _unify_ttys: + +_unify_ttys +----------- +.. versionadded:: 1.13.0 + +|def| ``False`` + +If ``True``, sh will combine the STDOUT and STDIN TTY into a single +pseudo-terminal. This is sometimes required by picky programs which expect to be +dealing with a single pseudo-terminal, like SSH. + +.. seealso:: :ref:`tutorial2` + +_tty_size +--------- + +|def| ``(20, 80)`` + +The (rows, columns) of stdout's TTY. Changing this may affect how much your +program prints per line, for example. + +Performance & Optimization +========================== + +_in_bufsize +----------- +|def| ``0`` + +The STDIN buffer size. 0 for unbuffered, 1 for line buffered, anything else for +a buffer of that amount. + +.. _out_bufsize: + +_out_bufsize +------------ +|def| ``1`` + +The STDOUT buffer size. 0 for unbuffered, 1 for line buffered, anything +else for a buffer of that amount. + +.. _err_bufsize: + +_err_bufsize +------------ +|def| ``1`` + +Same as :ref:`out_bufsize`, but with STDERR. + +.. _internal_bufsize: + +_internal_bufsize +----------------- +|def| ``3 * 1024**2`` chunks + +How much of STDOUT/ERR your command will store internally. This value +represents the *number of bufsize chunks* not the total number of bytes. For +example, if this value is 100, and STDOUT is line buffered, you will be able to +retrieve 100 lines from STDOUT. If STDOUT is unbuffered, you will be able to +retrieve only 100 characters. + +_no_out +------- +.. versionadded:: 1.07.0 + +|def| ``False`` + +Disables STDOUT being internally stored. This is useful for commands +that produce huge amounts of output that you don't need, that would +otherwise be hogging memory if stored internally by sh. + +_no_err +------- +.. versionadded:: 1.07.0 + +|def| ``False`` + +Disables STDERR being internally stored. This is useful for commands that +produce huge amounts of output that you don't need, that would otherwise be +hogging memory if stored internally by sh. + +_no_pipe +-------- +.. versionadded:: 1.07.0 + +|def| ``False`` + +Similar to ``_no_out``, this explicitly tells the sh command that it will never +be used for piping its output into another command, so it should not fill its +internal pipe buffer with the process's output. This is also useful for +conserving memory. + + +Program Arguments +================= + +These are options that affect how command options are fed into the program. + +_long_sep +--------- +.. versionadded:: 1.12.0 + +|def| ``"="`` + +This is the character(s) that separate a program's long argument's key from the +value, when using kwargs to specify your program's long arguments. For example, +if your program expects a long argument in the form ``--name value``, the way to +achieve this would be to set ``_long_sep=" "``. + +.. code-block:: python + + import sh + sh.your_program(key=value, _long_sep=" ") + +Would send the following list of arguments to your program: + +.. code-block:: python + + ["--key value"] + +If your program expects the long argument name to be separate from its value, +pass ``None`` into ``_long_sep`` instead: + +.. code-block:: python + + import sh + sh.your_program(key=value, _long_sep=None) + +Would send the following list of arguments to your program: + +.. code-block:: python + + ["--key", "value"] + +_long_prefix +------------ +.. versionadded:: 1.12.0 + +|def| ``"--"`` + +This is the character(s) that prefix a long argument for the program being run. +Some programs use single dashes, for example, and do not understand double +dashes. + +.. _preprocess: + +_arg_preprocess +--------------- +.. versionadded:: 1.12.0 + +|def| ``None`` + +This is an advanced option that allows you to rewrite a command's arguments on +the fly, based on other command arguments, or some other variable. It is really +only useful in conjunction with :ref:`baking `, and only currently used when +constructing :ref:`contrib ` wrappers. + +Example: + +.. code-block:: python + + import sh + + def processor(args, kwargs): + return args, kwargs + + my_ls = sh.bake.ls(_arg_preprocess=processor) + +.. warning:: + + The interface to the ``_arg_preprocess`` function may change without + warning. It is generally only for internal sh use, so don't use it unless + you absolutely have to. + +Misc +==== + +_log_msg +-------- + +|def| ``None`` + +.. versionadded:: 1.12.0 + +This allows for a custom logging header for :ref:`command_class` instances. For example, the default logging looks like this: + +.. code-block:: python + + import logging + import sh + + logging.basicConfig(level=logging.INFO) + + sh.ls("-l") + +.. code-block:: none + + INFO:sh.command:: starting process + INFO:sh.command:: process started + INFO:sh.command:: process completed + +People can find this ``` to make it well-behaved. In +particular, the contrib version allows you to specify your password at execution +time via terminal input, or as a string in your script. + +Terminal Input +^^^^^^^^^^^^^^ + +Via a :ref:`with context `: + +.. code-block:: python + + import sh + + with sh.contrib.sudo: + print(ls("/root")) + +Or alternatively via :ref:`subcommands `: + +.. code-block:: python + + import sh + print(sh.contrib.sudo.ls("/root")) + +Output: + +.. code-block:: none + + [sudo] password for youruser: ************* + your_root_files.txt + +In the above example, ``sh.contrib.sudo`` automatically asks you for a password +using :func:`getpass.getpass` under the hood. + +This method is the most secure, because it lowers the chances of doing something +insecure, like including your password in your python script, or by saying that +a particular user can execute anything inside of a particular script (the +NOPASSWD method). + +.. note:: + + ``sh.contrib.sudo`` does not do password caching like the sudo binary does. + Thie means that each time a sudo command is run in your script, you will be + asked to type in a password. + +String Input +^^^^^^^^^^^^ + +You may also specify your password to ``sh.contrib.sudo`` as a string: + +.. code-block:: python + + import sh + + password = get_your_password() + + with sh.contrib.sudo(password=password, _with=True): + print(ls("/root")) + +.. warning:: + + This method is less secure because it becomes tempting to hard-code your + password into the python script, and that's a bad idea. However, it is more + flexible, because it allows you to obtain your password from another source, + so long as the end result is a string. + +/etc/sudoers NOPASSWD +--------------------- + +With this method, you can use the raw ``sh.sudo`` command directly, because +you're being guaranteed that the system will not ask you for a password. It +first requires you set up your user to have root execution privileges + +Edit your sudoers file: + +.. code-block:: none + + $> sudo visudo + +Add or edit the line describing your user's permissions: + +.. code-block:: none + + yourusername ALL = (root) NOPASSWD: /path/to/your/program + +This says ``yourusername`` on ``ALL`` hosts will be able to run as root, but +only root ``(root)`` (no other users), and that no password ``NOPASSWD`` will be +asked of ``/path/to/your/program``. + +.. warning:: + + This method can be insecure if an unprivileged user can edit your script, + because the entire script will be exited as a privileged user. A malicious + user could put something bad in this script. + +.. _sudo_raw: + +sh.sudo +------- + +Using the raw command ``sh.sudo`` (which resolves directly to the system's +``sudo`` binary) without NOPASSWD is possible, provided you wire up the special +keyword arguments on your own to make it behave correctly. This method is +discussed generally for educational purposes; if you take the time to wire up +``sh.sudo`` on your own, then you have in essence just recreated +:ref:`contrib_sudo`. + +.. code-block:: python + + import sh + + # password must end in a newline + my_password = "password\n" + + # -S says "get the password from stdin" + my_sudo = sh.sudo.bake("-S", _in=my_password) + + print(my_sudo.ls("root")) + +_fg=True +-------- + +Another less-obvious way of using sudo is by executing the raw ``sh.sudo`` +command but also putting it in the foreground. This way, sudo will work +correctly automatically, by hooking up stdin/out/err automatically, and by +asking you for a password if it requires one. The downsides of using +:ref:`_fg=True `, however, are that you cannot capture its output -- everything is +just printed to your terminal as if you ran it from a shell. + +.. code-block:: python + + import sh + sh.sudo.ls("/root", _fg=True) diff --git a/docs/source/sections/with.rst b/docs/source/sections/with.rst new file mode 100644 index 00000000..bb4f8b78 --- /dev/null +++ b/docs/source/sections/with.rst @@ -0,0 +1,28 @@ +.. _with_contexts: + +'With' Contexts +=============== + +Commands can be run within a Python ``with`` context. Popular commands using +this might be ``sudo`` or ``fakeroot``: + +.. code-block:: python + + with sh.contrib.sudo: + print(ls("/root")) + +.. seealso:: + + :ref:`contrib_sudo` + +If you need to run a command in a with context and pass in arguments, for +example, specifying a -p prompt with sudo, you need to use the :ref:`_with=True +` This let's the command know that it's being run from a with context so +it can behave correctly: + +.. code-block:: python + + with sh.contrib.sudo(k=True, _with=True): + print(ls("/root")) + + diff --git a/docs/source/tutorials.rst b/docs/source/tutorials.rst new file mode 100644 index 00000000..87a60f8e --- /dev/null +++ b/docs/source/tutorials.rst @@ -0,0 +1,6 @@ +Tutorials +========= + +.. toctree:: + tutorials/real_time_output + tutorials/interacting_with_processes diff --git a/docs/source/tutorials/interacting_with_processes.rst b/docs/source/tutorials/interacting_with_processes.rst new file mode 100644 index 00000000..2b86114b --- /dev/null +++ b/docs/source/tutorials/interacting_with_processes.rst @@ -0,0 +1,243 @@ +.. _tutorial2: + +Entering an SSH password +======================== + +Here we will attempt to SSH into a server and enter a password programmatically. + +.. note:: + + It is recommended that you just ``ssh-copy-id`` to copy your public key to + the server so you don't need to enter your password, but for the purposes of + this demonstration, we try to enter a password. + +To interact with a process, we need to assign a callback to STDOUT. The +callback signature we'll use will take a :class:`queue.Queue` object for the +second argument, and we'll use that to send STDIN back to the process. + +.. seealso:: :ref:`red_func` + +Here's our first attempt: + +.. code-block:: python + + from sh import ssh + + def ssh_interact(line, stdin): + line = line.strip() + print(line) + if line.endswith("password:"): + stdin.put("correcthorsebatterystaple") + + ssh("10.10.10.100", _out=ssh_interact) + +If you run this (substituting an IP that you can SSH to), you'll notice that +nothing is printed from within the callback. The problem has to do with STDOUT +buffering. By default, sh line-buffers STDOUT, which means that +``ssh_interact`` will only receive output when sh encounters a newline in the +output. This is a problem because the password prompt has no newline: + +.. code-block:: none + + amoffat@10.10.10.100's password: + +Because a newline is never encountered, nothing is sent to the ``ssh_interact`` +callback. So we need to change the STDOUT buffering. We do this with the +:ref:`_out_bufsize ` special kwarg. We'll set +it to 0 for unbuffered output: + +.. code-block:: python + + from sh import ssh + + def ssh_interact(line, stdin): + line = line.strip() + print(line) + if line.endswith("password:"): + stdin.put("correcthorsebatterystaple") + + ssh("10.10.10.100", _out=ssh_interact, _out_bufsize=0) + +If you run this updated version, you'll notice a new problem. The output looks +like this: + +.. code-block:: none + + a + m + o + f + f + a + t + @ + 1 + 0 + . + 1 + 0 + . + 1 + 0 + . + 1 + 0 + 0 + ' + s + + p + a + s + s + w + o + r + d + : + +This is because the chunks of STDOUT our callback is receiving are unbuffered, +and are therefore individual characters, instead of entire lines. What we need +to do now is aggregate this character-by-character data into something more +meaningful for us to test if the pattern ``password:`` has been sent, signifying +that SSH is ready for input. + +It would make sense to encapsulate the variable we'll use for aggregating into +some kind of closure or class, but to keep it simple, we'll just use a global: + +.. code-block:: python + + from sh import ssh + import sys + + aggregated = "" + def ssh_interact(char, stdin): + global aggregated + sys.stdout.write(char.encode()) + sys.stdout.flush() + aggregated += char + if aggregated.endswith("password: "): + stdin.put("correcthorsebatterystaple") + + ssh("10.10.10.100", _out=ssh_interact, _out_bufsize=0) + +You'll also notice that the example still doesn't work. There are two problems: +The first is that your password must end with a newline, as if you had typed it +and hit the return key. This is because SSH has no idea how long your password +is, and is line-buffering STDIN. + +The second problem lies deeper in SSH. SSH needs a TTY attached to its STDIN in +order to work properly. This tricks SSH into believing that it is interacting +with a real user in a real terminal session. To enable TTY, we can add the +:ref:`_tty_in ` special kwarg. We also need to use :ref:`_unify_ttys ` special kwarg. +This tells sh to make STDOUT and STDIN come from a single pseudo-terminal, which is a requirement of SSH: + +.. code-block:: python + + from sh import ssh + import sys + + aggregated = "" + def ssh_interact(char, stdin): + global aggregated + sys.stdout.write(char.encode()) + sys.stdout.flush() + aggregated += char + if aggregated.endswith("password: "): + stdin.put("correcthorsebatterystaple\n") + + ssh("10.10.10.100", _out=ssh_interact, _out_bufsize=0, _tty_in=True, _unify_ttys=True) + +And now our remote login script works! + +.. code-block:: none + + amoffat@10.10.10.100's password: + Linux 10.10.10.100 testhost #1 SMP Tue Jun 21 10:29:24 EDT 2011 i686 GNU/Linux + Ubuntu 10.04.2 LTS + + Welcome to Ubuntu! + * Documentation: https://help.ubuntu.com/ + + 66 packages can be updated. + 53 updates are security updates. + + Ubuntu 10.04.2 LTS + + Welcome to Ubuntu! + * Documentation: https://help.ubuntu.com/ + You have new mail. + Last login: Thu Sep 13 03:53:00 2012 from some.ip.address + amoffat@10.10.10.100:~$ + +SSH Contrib command +------------------- + +The above process can be simplified by using a :ref:`contrib`. The :ref:`SSH contrib command ` does +all the ugly kwarg argument setup for you, and provides a simple but powerful interface for doing SSH password logins. +Please see the :ref:`SSH contrib command ` for more details about the exact api: + +.. code-block:: python + + from sh.contrib import ssh + + def ssh_interact(content, stdin): + sys.stdout.write(content.cur_char) + sys.stdout.flush() + + # automatically logs in with password and then presents subsequent content to + # the ssh_interact callback + ssh("10.10.10.100", password="correcthorsebatterystaple", interact=ssh_interact) + +How you should REALLY be using SSH +---------------------------------- + +Many people want to learn how to enter an SSH password by script because they +want to execute remote commands on a server. Instead of trying to log in +through SSH and then sending terminal input of the command to run, let's see how +we can do it another way. + +First, open a terminal and run ``ssh-copy-id yourservername``. You'll be asked +to enter your password for the server. After entering your password, you'll be +able to SSH into the server without needing a password again. This simplifies +things greatly for sh. + +The second thing we want to do is use SSH's ability to pass a command to run +to the server you're SSHing to. Here's how you can run ``ifconfig`` on a server +without having to use that server's shell directly: + +.. code-block:: none + + ssh amoffat@10.10.10.100 ifconfig + +Translating this to sh, it becomes: + +.. code-block:: python + + import sh + + print(sh.ssh("amoffat@10.10.10.100", "ifconfig")) + +We can make this even nicer by taking advantage of sh's :ref:`baking` to bind +our server username/ip to a command object: + +.. code-block:: python + + import sh + + my_server = sh.ssh.bake("amoffat@10.10.10.100") + print(my_server("ifconfig")) + print(my_server("whoami")) + +Now we have a reusable command object that we can use to call remote commands. +But there is room for one more improvement. We can also use sh's +:ref:`subcommands` feature which expands attribute access into command +arguments: + +.. code-block:: python + + import sh + + my_server = sh.ssh.bake("amoffat@10.10.10.100") + print(my_server.ifconfig()) + print(my_server.whoami()) diff --git a/docs/source/tutorials/real_time_output.rst b/docs/source/tutorials/real_time_output.rst new file mode 100644 index 00000000..8601cb2c --- /dev/null +++ b/docs/source/tutorials/real_time_output.rst @@ -0,0 +1,66 @@ +.. _tutorial1: + +Tailing a real-time log file +============================ + +sh has the ability to respond to subprocesses in an event-driven fashion. +A typical example of where this would be useful is tailing a log file for +a specific pattern, then responding to that value immediately:: + + from sh import tail + + for line in tail("-f", "info.log", _iter=True): + if "ERROR" in line: + send_an_email_to_support(line) + + +The :ref:`_iter ` special kwarg takes a command that would normally block +until completion, and turns its output into a real-time iterable. + +.. seealso:: :ref:`iterable` + +Of course, you can do more than just tail log files. Any program that +produces output can be iterated over. Say you wanted to send an email to a +coworker if their C code emits a warning: + +.. code-block:: python + + from sh import gcc, git + + for line in gcc("-o", "awesome_binary", "awesome_source.c", _iter=True): + if "warning" in line: + # parse out the relevant info + filename, line, char, message = line.split(":", 3) + + # find the commit using git + commit = git("blame", "-e", filename, L="%d,%d" % (line,line)) + + # send them an email + email_address = parse_email_from_commit_line(commit) + send_email(email_address, message) + +Using :ref:`_iter ` is a great way to respond to events from another +program, but your blocks while you're looping, making you unable to do anything +else. To be truly event-driven, sh provides callbacks: + +.. code-block:: python + + from sh import tail + + def process_log_line(line): + if "ERROR" in line: + send_an_email_to_support(line) + + process = tail("-f", "info.log", _out=process_log_line, _bg=True) + + # ... do other stuff here ... + + process.wait() + +The :ref:`_out ` special kwarg lets you to assign a callback to STDOUT. +This callback will receive each line of output from ``tail -f`` and allow you to +do the same processing that we did earlier. + +.. seealso:: :ref:`callbacks` + +.. seealso:: :ref:`redirection` diff --git a/docs/source/usage.rst b/docs/source/usage.rst new file mode 100644 index 00000000..2d9cdb29 --- /dev/null +++ b/docs/source/usage.rst @@ -0,0 +1,17 @@ +Usage +===== + +.. toctree:: + + sections/passing_arguments + sections/exit_codes + sections/redirection + sections/asynchronous_execution + sections/baking + sections/piping + sections/subcommands + sections/default_arguments + sections/envs + sections/stdin + sections/with + diff --git a/poetry.lock b/poetry.lock index b5cf2d25..ee7d2576 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,20 +6,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "attrs" -version = "21.2.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.extras] -dev = ["coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"] - [[package]] name = "babel" version = "2.9.1" @@ -33,11 +19,11 @@ pytz = ">=2015.7" [[package]] name = "black" -version = "23.1.0" +version = "23.7.0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" [package.dependencies] click = ">=8.0.0" @@ -56,11 +42,11 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "cachetools" -version = "5.3.0" +version = "5.3.1" description = "Extensible memoizing collections and decorators" category = "dev" optional = false -python-versions = "~=3.7" +python-versions = ">=3.7" [[package]] name = "certifi" @@ -87,7 +73,7 @@ optional = false python-versions = ">=3.5.0" [package.extras] -unicode_backport = ["unicodedata2"] +unicode-backport = ["unicodedata2"] [[package]] name = "click" @@ -121,47 +107,23 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "coverage" -version = "5.5" +version = "7.2.7" description = "Code coverage measurement for Python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" - -[package.extras] -toml = ["toml"] - -[[package]] -name = "coveralls" -version = "3.3.1" -description = "Show coverage stats online via coveralls.io" -category = "dev" -optional = false -python-versions = ">= 3.5" - -[package.dependencies] -coverage = ">=4.1,<6.0.0 || >6.1,<6.1.1 || >6.1.1,<7.0" -docopt = ">=0.6.1" -requests = ">=1.0.0" +python-versions = ">=3.7" [package.extras] -yaml = ["PyYAML (>=3.10)"] +toml = ["tomli"] [[package]] name = "distlib" -version = "0.3.6" +version = "0.3.7" description = "Distribution utilities" category = "dev" optional = false python-versions = "*" -[[package]] -name = "docopt" -version = "0.6.2" -description = "Pythonic argument parser, that will make you smile" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "docutils" version = "0.18.1" @@ -183,19 +145,19 @@ test = ["pytest (>=6)"] [[package]] name = "filelock" -version = "3.9.0" +version = "3.12.2" description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] [[package]] name = "flake8" -version = "6.0.0" +version = "6.1.0" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false @@ -203,8 +165,8 @@ python-versions = ">=3.8.1" [package.dependencies] mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.10.0,<2.11.0" -pyflakes = ">=3.0.0,<3.1.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.1.0,<3.2.0" [[package]] name = "idna" @@ -278,16 +240,16 @@ python-versions = ">=3.6" [[package]] name = "mypy" -version = "1.0.0" +version = "1.4.1" description = "Optional static typing for Python" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -mypy-extensions = ">=0.4.3" +mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=3.10" +typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] @@ -297,15 +259,15 @@ reports = ["lxml"] [[package]] name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.5" [[package]] name = "packaging" -version = "23.0" +version = "23.1" description = "Core utilities for Python packages" category = "dev" optional = false @@ -321,23 +283,23 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "platformdirs" -version = "3.0.0" +version = "3.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] [[package]] name = "pluggy" -version = "1.0.0" +version = "1.2.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] dev = ["pre-commit", "tox"] @@ -345,11 +307,11 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pycodestyle" -version = "2.10.0" +version = "2.11.0" description = "Python style guide checker" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" [[package]] name = "pydantic" @@ -368,11 +330,11 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pyflakes" -version = "3.0.1" +version = "3.1.0" description = "passive checker of Python programs" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" [[package]] name = "pygments" @@ -387,30 +349,29 @@ plugins = ["importlib-metadata"] [[package]] name = "pyproject-api" -version = "1.5.0" +version = "1.5.3" description = "API to interact with the python pyproject.toml based projects" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -packaging = ">=21.3" +packaging = ">=23.1" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} [package.extras] -docs = ["furo (>=2022.9.29)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -testing = ["covdefaults (>=2.2.2)", "importlib-metadata (>=5.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "virtualenv (>=20.17)", "wheel (>=0.38.4)"] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "importlib-metadata (>=6.6)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "setuptools (>=67.8)", "wheel (>=0.40)"] [[package]] name = "pytest" -version = "7.2.1" +version = "7.4.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" @@ -419,7 +380,7 @@ pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytz" @@ -445,7 +406,7 @@ urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "rich" @@ -464,7 +425,7 @@ jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] [[package]] name = "rstcheck" -version = "6.1.1" +version = "6.1.2" description = "Checks syntax of reStructuredText and code blocks nested within it" category = "dev" optional = false @@ -477,7 +438,7 @@ typer = {version = ">=0.4.1,<0.8", extras = ["all"]} [package.extras] docs = ["m2r2 (>=0.3.2)", "sphinx", "sphinx-autobuild (==2021.3.14)", "sphinx-click (>=4.0.3,<5.0.0)", "sphinx-rtd-dark-mode (>=1.2.4,<2.0.0)", "sphinx-rtd-theme (<1)", "sphinxcontrib-spelling (>=7.3)"] sphinx = ["sphinx"] -testing = ["coverage-conditional-plugin (>=0.5)", "coverage[toml] (>=6.0)", "pytest (>=6.0)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.0)", "pytest-sugar (>=0.9.5)"] +testing = ["coverage-conditional-plugin (>=0.5)", "coverage[toml] (>=6.0)", "pytest (>=7.2)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.0)", "pytest-sugar (>=0.9.5)"] toml = ["tomli"] [[package]] @@ -499,19 +460,6 @@ sphinx = ["sphinx (>=4.0,<6.0)"] testing = ["coverage-conditional-plugin (>=0.5)", "coverage[toml] (>=6.0)", "pytest (>=6.0)", "pytest-cov (>=3.0)", "pytest-mock (>=3.7)", "pytest-randomly (>=3.0)", "pytest-sugar (>=0.9.5)"] toml = ["tomli (>=2.0,<3.0)"] -[[package]] -name = "setuptools" -version = "67.2.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - [[package]] name = "shellingham" version = "1.5.0.post1" @@ -562,7 +510,7 @@ test = ["cython", "html5lib", "pytest (>=4.6)"] [[package]] name = "sphinx-rtd-theme" -version = "1.2.0" +version = "1.2.2" description = "Read the Docs theme for Sphinx" category = "dev" optional = false @@ -571,7 +519,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" [package.dependencies] docutils = "<0.19" sphinx = ">=1.6,<7" -sphinxcontrib-jquery = {version = ">=2.0.0,<3.0.0 || >3.0.0", markers = "python_version > \"3\""} +sphinxcontrib-jquery = ">=4,<5" [package.extras] dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] @@ -614,14 +562,14 @@ test = ["html5lib", "pytest"] [[package]] name = "sphinxcontrib-jquery" -version = "2.0.0" +version = "4.1" description = "Extension to include jQuery on newer Sphinx releases" category = "dev" optional = false python-versions = ">=2.7" [package.dependencies] -setuptools = "*" +Sphinx = ">=1.8" [[package]] name = "sphinxcontrib-jsmath" @@ -658,6 +606,14 @@ python-versions = ">=3.5" lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "tomli" version = "2.0.1" @@ -668,27 +624,27 @@ python-versions = ">=3.7" [[package]] name = "tox" -version = "4.4.5" +version = "4.6.4" description = "tox is a generic virtualenv management and test command line tool" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -cachetools = ">=5.3" +cachetools = ">=5.3.1" chardet = ">=5.1" colorama = ">=0.4.6" -filelock = ">=3.9" -packaging = ">=23" -platformdirs = ">=2.6.2" -pluggy = ">=1" -pyproject-api = ">=1.5" +filelock = ">=3.12.2" +packaging = ">=23.1" +platformdirs = ">=3.8" +pluggy = ">=1.2" +pyproject-api = ">=1.5.2" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} -virtualenv = ">=20.17.1" +virtualenv = ">=20.23.1" [package.extras] -docs = ["furo (>=2022.12.7)", "sphinx (>=6.1.3)", "sphinx-argparse-cli (>=1.11)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)", "sphinx-copybutton (>=0.5.1)", "sphinx-inline-tabs (>=2022.1.2b11)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] -testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.2.2)", "devpi-process (>=0.3)", "diff-cover (>=7.4)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.12.2)", "psutil (>=5.9.4)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-xdist (>=3.1)", "re-assert (>=1.1)", "time-machine (>=2.9)", "wheel (>=0.38.4)"] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.23.3,!=1.23.4)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=0.3.1)", "diff-cover (>=7.6)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.17.1)", "psutil (>=5.9.5)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "re-assert (>=1.1)", "time-machine (>=2.10)", "wheel (>=0.40)"] [[package]] name = "typer" @@ -720,11 +676,11 @@ python-versions = "*" [[package]] name = "typing-extensions" -version = "3.10.0.2" -description = "Backported and Experimental Type Hints for Python 3.5+" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" [[package]] name = "urllib3" @@ -741,20 +697,20 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.19.0" +version = "20.24.2" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -distlib = ">=0.3.6,<1" -filelock = ">=3.4.1,<4" -platformdirs = ">=2.4,<4" +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<4" [package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] -test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] name = "zipp" @@ -771,51 +727,44 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black ( [metadata] lock-version = "1.1" python-versions = ">=3.8.1,<4.0" -content-hash = "85385294eb45c64e75c642cb94b89a78ae2950ee138960f847624bb3361efc4d" +content-hash = "fd15344e892d371cda817467a113a25c5f87fcc78b17c527fee200b8e4262043" [metadata.files] alabaster = [ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] -attrs = [ - {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, - {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, -] babel = [ {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, ] black = [ - {file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"}, - {file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"}, - {file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"}, - {file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"}, - {file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"}, - {file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"}, - {file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"}, - {file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"}, - {file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"}, - {file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"}, - {file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"}, - {file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"}, - {file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"}, - {file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"}, - {file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"}, - {file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, + {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, + {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, + {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, + {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, + {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, + {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, + {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, + {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, + {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, + {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, ] cachetools = [ - {file = "cachetools-5.3.0-py3-none-any.whl", hash = "sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4"}, - {file = "cachetools-5.3.0.tar.gz", hash = "sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14"}, + {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, + {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, ] certifi = [ {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, @@ -842,69 +791,70 @@ commonmark = [ {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, ] coverage = [ - {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, - {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, - {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, - {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, - {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, - {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, - {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, - {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, - {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, - {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, - {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, - {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, - {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, - {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, - {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, - {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, - {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, - {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, - {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, - {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, - {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, - {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, - {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, - {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, - {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, - {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, - {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, - {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, - {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, - {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, - {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, - {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, - {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, - {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, - {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, - {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, - {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, - {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, - {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, - {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, - {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, - {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, - {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, - {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, - {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, - {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, - {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, - {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, -] -coveralls = [ - {file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"}, - {file = "coveralls-3.3.1.tar.gz", hash = "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, ] distlib = [ - {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, - {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, -] -docopt = [ - {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, ] docutils = [ {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, @@ -915,12 +865,12 @@ exceptiongroup = [ {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, ] filelock = [ - {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, - {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, ] flake8 = [ - {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"}, - {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"}, + {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, + {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, ] idna = [ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, @@ -1018,56 +968,56 @@ mccabe = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] mypy = [ - {file = "mypy-1.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0626db16705ab9f7fa6c249c017c887baf20738ce7f9129da162bb3075fc1af"}, - {file = "mypy-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ace23f6bb4aec4604b86c4843276e8fa548d667dbbd0cb83a3ae14b18b2db6c"}, - {file = "mypy-1.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87edfaf344c9401942883fad030909116aa77b0fa7e6e8e1c5407e14549afe9a"}, - {file = "mypy-1.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0ab090d9240d6b4e99e1fa998c2d0aa5b29fc0fb06bd30e7ad6183c95fa07593"}, - {file = "mypy-1.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:7cc2c01dfc5a3cbddfa6c13f530ef3b95292f926329929001d45e124342cd6b7"}, - {file = "mypy-1.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14d776869a3e6c89c17eb943100f7868f677703c8a4e00b3803918f86aafbc52"}, - {file = "mypy-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb2782a036d9eb6b5a6efcdda0986774bf798beef86a62da86cb73e2a10b423d"}, - {file = "mypy-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cfca124f0ac6707747544c127880893ad72a656e136adc935c8600740b21ff5"}, - {file = "mypy-1.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8845125d0b7c57838a10fd8925b0f5f709d0e08568ce587cc862aacce453e3dd"}, - {file = "mypy-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b1b9e1ed40544ef486fa8ac022232ccc57109f379611633ede8e71630d07d2"}, - {file = "mypy-1.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c7cf862aef988b5fbaa17764ad1d21b4831436701c7d2b653156a9497d92c83c"}, - {file = "mypy-1.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd187d92b6939617f1168a4fe68f68add749902c010e66fe574c165c742ed88"}, - {file = "mypy-1.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4e5175026618c178dfba6188228b845b64131034ab3ba52acaffa8f6c361f805"}, - {file = "mypy-1.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2f6ac8c87e046dc18c7d1d7f6653a66787a4555085b056fe2d599f1f1a2a2d21"}, - {file = "mypy-1.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7306edca1c6f1b5fa0bc9aa645e6ac8393014fa82d0fa180d0ebc990ebe15964"}, - {file = "mypy-1.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3cfad08f16a9c6611e6143485a93de0e1e13f48cfb90bcad7d5fde1c0cec3d36"}, - {file = "mypy-1.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67cced7f15654710386e5c10b96608f1ee3d5c94ca1da5a2aad5889793a824c1"}, - {file = "mypy-1.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a86b794e8a56ada65c573183756eac8ac5b8d3d59daf9d5ebd72ecdbb7867a43"}, - {file = "mypy-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:50979d5efff8d4135d9db293c6cb2c42260e70fb010cbc697b1311a4d7a39ddb"}, - {file = "mypy-1.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ae4c7a99e5153496243146a3baf33b9beff714464ca386b5f62daad601d87af"}, - {file = "mypy-1.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e398652d005a198a7f3c132426b33c6b85d98aa7dc852137a2a3be8890c4072"}, - {file = "mypy-1.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be78077064d016bc1b639c2cbcc5be945b47b4261a4f4b7d8923f6c69c5c9457"}, - {file = "mypy-1.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92024447a339400ea00ac228369cd242e988dd775640755fa4ac0c126e49bb74"}, - {file = "mypy-1.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:fe523fcbd52c05040c7bee370d66fee8373c5972171e4fbc323153433198592d"}, - {file = "mypy-1.0.0-py3-none-any.whl", hash = "sha256:2efa963bdddb27cb4a0d42545cd137a8d2b883bd181bbc4525b568ef6eca258f"}, - {file = "mypy-1.0.0.tar.gz", hash = "sha256:f34495079c8d9da05b183f9f7daec2878280c2ad7cc81da686ef0b484cea2ecf"}, + {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, + {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, + {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, + {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, + {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, + {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, + {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, + {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, + {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, + {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, + {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, + {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, + {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, + {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, + {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, + {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, + {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, + {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, + {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, + {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, ] mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] packaging = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] pathspec = [ {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] platformdirs = [ - {file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, - {file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, + {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, + {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, ] pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, ] pycodestyle = [ - {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, - {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, + {file = "pycodestyle-2.11.0-py2.py3-none-any.whl", hash = "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8"}, + {file = "pycodestyle-2.11.0.tar.gz", hash = "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0"}, ] pydantic = [ {file = "pydantic-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c9e04a6cdb7a363d7cb3ccf0efea51e0abb48e180c0d31dca8d247967d85c6e"}, @@ -1107,20 +1057,20 @@ pydantic = [ {file = "pydantic-1.9.2.tar.gz", hash = "sha256:8cb0bc509bfb71305d7a59d00163d5f9fc4530f0881ea32c74ff4f74c85f3d3d"}, ] pyflakes = [ - {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, - {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, + {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, + {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, ] pygments = [ {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, ] pyproject-api = [ - {file = "pyproject_api-1.5.0-py3-none-any.whl", hash = "sha256:4c111277dfb96bcd562c6245428f27250b794bfe3e210b8714c4f893952f2c17"}, - {file = "pyproject_api-1.5.0.tar.gz", hash = "sha256:0962df21f3e633b8ddb9567c011e6c1b3dcdfc31b7860c0ede7e24c5a1200fbe"}, + {file = "pyproject_api-1.5.3-py3-none-any.whl", hash = "sha256:14cf09828670c7b08842249c1f28c8ee6581b872e893f81b62d5465bec41502f"}, + {file = "pyproject_api-1.5.3.tar.gz", hash = "sha256:ffb5b2d7cad43f5b2688ab490de7c4d3f6f15e0b819cb588c4b771567c9729eb"}, ] pytest = [ - {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, - {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, ] pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, @@ -1135,17 +1085,13 @@ rich = [ {file = "rich-12.0.1.tar.gz", hash = "sha256:3fba9dd15ebe048e2795a02ac19baee79dc12cc50b074ef70f2958cd651b59a9"}, ] rstcheck = [ - {file = "rstcheck-6.1.1-py3-none-any.whl", hash = "sha256:edeff9ad0644d12bd250100b677887424193789254c90d95c13375062ee2cbac"}, - {file = "rstcheck-6.1.1.tar.gz", hash = "sha256:8e43485a644e794b8127f8c4868ef62c14ec7919bdda6cb16642157055d32e47"}, + {file = "rstcheck-6.1.2-py3-none-any.whl", hash = "sha256:4aaa46e0debc179f849807c453fa384fd2b75167faf5b1274115730805fab529"}, + {file = "rstcheck-6.1.2.tar.gz", hash = "sha256:f9cb07a72ef9a81d1e32187eae29b00a89421ccba1bde0b1652a08ed0923f61b"}, ] rstcheck-core = [ {file = "rstcheck_core-1.0.3-py3-none-any.whl", hash = "sha256:d75d7df8f15b58e8aafe322d6fb6ef1ac8d12bb563089b0696948a00ee7f601a"}, {file = "rstcheck_core-1.0.3.tar.gz", hash = "sha256:add19c9a1b97d9087f4b463b49c12cd8a9c03689a255e99089c70a2692f16369"}, ] -setuptools = [ - {file = "setuptools-67.2.0-py3-none-any.whl", hash = "sha256:16ccf598aab3b506593c17378473978908a2734d7336755a8769b480906bec1c"}, - {file = "setuptools-67.2.0.tar.gz", hash = "sha256:b440ee5f7e607bb8c9de15259dba2583dd41a38879a7abc1d43a71c59524da48"}, -] shellingham = [ {file = "shellingham-1.5.0.post1-py2.py3-none-any.whl", hash = "sha256:368bf8c00754fd4f55afb7bbb86e272df77e4dc76ac29dbcbb81a59e9fc15744"}, {file = "shellingham-1.5.0.post1.tar.gz", hash = "sha256:823bc5fb5c34d60f285b624e7264f4dda254bc803a3774a147bf99c0e3004a28"}, @@ -1159,8 +1105,8 @@ sphinx = [ {file = "sphinx-6.1.3-py3-none-any.whl", hash = "sha256:807d1cb3d6be87eb78a381c3e70ebd8d346b9a25f3753e9947e866b2786865fc"}, ] sphinx-rtd-theme = [ - {file = "sphinx_rtd_theme-1.2.0-py2.py3-none-any.whl", hash = "sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2"}, - {file = "sphinx_rtd_theme-1.2.0.tar.gz", hash = "sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8"}, + {file = "sphinx_rtd_theme-1.2.2-py2.py3-none-any.whl", hash = "sha256:6a7e7d8af34eb8fc57d52a09c6b6b9c46ff44aea5951bc831eeb9245378f3689"}, + {file = "sphinx_rtd_theme-1.2.2.tar.gz", hash = "sha256:01c5c5a72e2d025bd23d1f06c59a4831b06e6ce6c01fdd5ebfe9986c0a880fc7"}, ] sphinxcontrib-applehelp = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, @@ -1175,8 +1121,8 @@ sphinxcontrib-htmlhelp = [ {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, ] sphinxcontrib-jquery = [ - {file = "sphinxcontrib-jquery-2.0.0.tar.gz", hash = "sha256:8fb65f6dba84bf7bcd1aea1f02ab3955ac34611d838bcc95d4983b805b234daa"}, - {file = "sphinxcontrib_jquery-2.0.0-py3-none-any.whl", hash = "sha256:ed47fa425c338ffebe3c37e1cdb56e30eb806116b85f01055b158c7057fdb995"}, + {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, + {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, ] sphinxcontrib-jsmath = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, @@ -1190,13 +1136,17 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tox = [ - {file = "tox-4.4.5-py3-none-any.whl", hash = "sha256:1081864f1a1393ffa11ebe9beaa280349020579310d217a594a4e7b6124c5425"}, - {file = "tox-4.4.5.tar.gz", hash = "sha256:f9bc83c5da8666baa2a4d4e884bbbda124fe646e4b1c0e412949cecc2b6e8f90"}, + {file = "tox-4.6.4-py3-none-any.whl", hash = "sha256:1b8f8ae08d6a5475cad9d508236c51ea060620126fd7c3c513d0f5c7f29cc776"}, + {file = "tox-4.6.4.tar.gz", hash = "sha256:5e2ad8845764706170d3dcaac171704513cc8a725655219acb62fe4380bdadda"}, ] typer = [ {file = "typer-0.7.0-py3-none-any.whl", hash = "sha256:b5e704f4e48ec263de1c0b3a2387cd405a13767d2f907f44c1a08cbad96f606d"}, @@ -1207,17 +1157,16 @@ types-docutils = [ {file = "types_docutils-0.19.1.3-py3-none-any.whl", hash = "sha256:d608e6b91ccf0e8e01c586a0af5b0e0462382d3be65b734af82d40c9d010735d"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, - {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, - {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] urllib3 = [ {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, ] virtualenv = [ - {file = "virtualenv-20.19.0-py3-none-any.whl", hash = "sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1"}, - {file = "virtualenv-20.19.0.tar.gz", hash = "sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590"}, + {file = "virtualenv-20.24.2-py3-none-any.whl", hash = "sha256:43a3052be36080548bdee0b42919c88072037d50d56c28bd3f853cbe92b953ff"}, + {file = "virtualenv-20.24.2.tar.gz", hash = "sha256:fd8a78f46f6b99a67b7ec5cf73f92357891a7b3a40fd97637c27f854aae3b9e0"}, ] zipp = [ {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, diff --git a/pyproject.toml b/pyproject.toml index 67b34d75..ce093281 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,15 +41,16 @@ include = [ python = ">=3.8.1,<4.0" [tool.poetry.group.dev.dependencies] -tox = "^4.4.5" -black = "^23.1.0" -coveralls = "^3.3.1" -flake8 = "^6.0.0" -rstcheck = "^6.1.1" -sphinx = "^6.1.3" -sphinx-rtd-theme = "^1.2.0" -pytest = "^7.2.1" -mypy = "^1.0.0" +tox = "^4.6.4" +black = "^23.7.0" +coverage = "^7.2.7" +flake8 = "^6.1.0" +rstcheck = "^6.1.2" +sphinx = ">=1.6,<7" +sphinx-rtd-theme = "^1.2.2" +pytest = "^7.4.0" +mypy = "^1.4.1" +toml = "^0.10.2" [build-system] requires = ["poetry-core>=1.0.0a5"] From 27209dd64c446aee17c5dc68169f14834c15d4f8 Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Sun, 6 Aug 2023 23:45:53 -0700 Subject: [PATCH 09/21] update docs --- .gitignore | 3 ++- README.rst | 10 +-------- docs/source/index.rst | 9 +++----- docs/source/sections/architecture.rst | 2 +- .../sections/asynchronous_execution.rst | 4 ++-- docs/source/sections/command_class.rst | 14 ++++++------- docs/source/sections/contrib.rst | 6 +++--- docs/source/sections/default_arguments.rst | 21 ++----------------- docs/source/sections/piping.rst | 16 +++++++------- docs/source/sections/redirection.rst | 2 +- docs/source/sections/special_arguments.rst | 13 ++++++++---- 11 files changed, 39 insertions(+), 61 deletions(-) diff --git a/.gitignore b/.gitignore index 6a6c5488..41b053fa 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ __pycache__/ /.venv/ /build /dist -/docs/build \ No newline at end of file +/docs/build +/TODO.md \ No newline at end of file diff --git a/README.rst b/README.rst index b1452c78..f90efaf3 100644 --- a/README.rst +++ b/README.rst @@ -15,16 +15,13 @@ .. image:: https://img.shields.io/pypi/pyversions/sh.svg?style=flat-square :target: https://pypi.python.org/pypi/sh :alt: Python Versions -.. image:: https://img.shields.io/travis/amoffat/sh/master.svg?style=flat-square - :target: https://travis-ci.org/amoffat/sh - :alt: Build Status .. image:: https://img.shields.io/coveralls/amoffat/sh.svg?style=flat-square :target: https://coveralls.io/r/amoffat/sh?branch=master :alt: Coverage Status | -sh is a full-fledged subprocess replacement for Python 3.8 - 3.10, PyPy and PyPy3 +sh is a full-fledged subprocess replacement for Python 3.8 - 3.11, and PyPy that allows you to call *any* program as if it were a function: .. code:: python @@ -55,11 +52,6 @@ Support Developers ========== -Updating the docs ------------------ - -Check out the `gh-pages `_ branch and follow the ``README.rst`` there. - Testing ------- diff --git a/docs/source/index.rst b/docs/source/index.rst index dbbc2dff..fd2e550a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -26,9 +26,6 @@ sh .. image:: https://img.shields.io/pypi/pyversions/sh.svg?style=flat-square :target: https://pypi.python.org/pypi/sh :alt: Python Versions -.. image:: https://img.shields.io/travis/amoffat/sh/master.svg?style=flat-square - :target: https://travis-ci.org/amoffat/sh - :alt: Build Status .. image:: https://img.shields.io/coveralls/amoffat/sh.svg?style=flat-square :target: https://coveralls.io/r/amoffat/sh?branch=master :alt: Coverage Status @@ -36,8 +33,8 @@ sh :target: https://github.com/amoffat/sh :alt: Github -sh is a full-fledged subprocess replacement for Python 2.6 - 3.8, PyPy and PyPy3 -that allows you to call any program as if it were a function: +sh is a full-fledged subprocess replacement for Python 3.8 - 3.11, PyPy that +allows you to call any program as if it were a function: .. code-block:: python @@ -133,7 +130,7 @@ Piping .. code-block:: python - sh.wc(sh.ls("-1"), "-l") + sh.wc("-l", _in=sh.ls("-1")) :ref:`Read More ` diff --git a/docs/source/sections/architecture.rst b/docs/source/sections/architecture.rst index 267ed69d..42cdbafb 100644 --- a/docs/source/sections/architecture.rst +++ b/docs/source/sections/architecture.rst @@ -19,7 +19,7 @@ From here, we have two concurrent processes running: Child ----- -#. If :ref:`_bg=True ` is set, we ignore :attr:`signal.SIGHUP`. +#. If :ref:`_bg=True ` is set, we ignore :py:data:`signal.SIGHUP`. #. If :ref:`_new_session=True `, become a session leader with :func:`os.setsid`, else become a process group leader with :func:`os.setpgrp`. diff --git a/docs/source/sections/asynchronous_execution.rst b/docs/source/sections/asynchronous_execution.rst index d10b9d27..f7b1e2d4 100644 --- a/docs/source/sections/asynchronous_execution.rst +++ b/docs/source/sections/asynchronous_execution.rst @@ -32,7 +32,7 @@ changing the buffer size of the command's output with :ref:`out_bufsize`. .. note:: If you need a *fully* non-blocking iterator, use :ref:`iter_noblock`. If - the current iteration would block, :attr:`errno.EWOULDBLOCK` will be + the current iteration would block, :py:data:`errno.EWOULDBLOCK` will be returned, otherwise you'll receive a chunk of output, as normal. .. _background: @@ -98,7 +98,7 @@ To control whether the callback receives a line or a chunk, use tells the command not to call your callback anymore. The line or chunk received by the callback can either be of type ``str`` or -``bytes``. If the output could be decoded using the provided :ref:`encoding`, a +``bytes``. If the output could be decoded using the provided encoding, a ``str`` will be passed to the callback, otherwise it would be raw ``bytes``. .. note:: diff --git a/docs/source/sections/command_class.rst b/docs/source/sections/command_class.rst index 9c00b96e..a0e2521a 100644 --- a/docs/source/sections/command_class.rst +++ b/docs/source/sections/command_class.rst @@ -86,7 +86,7 @@ RunningCommand Class This represents a :ref:`Command ` instance that has been or is being executed. It exists as a wrapper around the low-level :ref:`OProc `. Most of your interaction with sh objects are with instances of -this class +this class. It is only returned if ``_return_cmd=True`` when you execute a command. .. warning:: @@ -162,12 +162,12 @@ this class .. py:method:: RunningCommand.signal(sig_num) Sends *sig_num* to the process. Typically used with a value from the - :mod:`signal` module, like :attr:`signal.SIGHUP` (see :manpage:`signal(7)`). + :mod:`signal` module, like :py:data:`signal.SIGHUP` (see :manpage:`signal(7)`). .. py:method:: RunningCommand.signal_group(sig_num) Sends *sig_num* to every process in the process group. Typically used with - a value from the :mod:`signal` module, like :attr:`signal.SIGHUP` (see + a value from the :mod:`signal` module, like :py:data:`signal.SIGHUP` (see :manpage:`signal(7)`). .. py:method:: RunningCommand.terminate() @@ -189,7 +189,7 @@ this class Returns whether or not the process is still alive. - :rtype: boolean + :rtype: bool .. _oproc_class: @@ -242,12 +242,12 @@ OProc Class .. py:method:: OProc.signal(sig_num) Sends *sig_num* to the process. Typically used with a value from the - :mod:`signal` module, like :attr:`signal.SIGHUP` (see :manpage:`signal(7)`). + :mod:`signal` module, like :py:data:`signal.SIGHUP` (see :manpage:`signal(7)`). .. py:method:: OProc.signal_group(sig_num) Sends *sig_num* to every process in the process group. Typically used with - a value from the :mod:`signal` module, like :attr:`signal.SIGHUP` (see + a value from the :mod:`signal` module, like :py:data:`signal.SIGHUP` (see :manpage:`signal(7)`). .. py:method:: OProc.terminate() @@ -274,7 +274,7 @@ ErrorReturnCode .. py:class:: ErrorReturnCode This is the base class for, as the name suggests, error return codes. It - subclasses :data:`exceptions.Exception`. + subclasses :py:class:`Exception`. .. py:attribute:: ErrorReturnCode.full_cmd diff --git a/docs/source/sections/contrib.rst b/docs/source/sections/contrib.rst index 1a5139c4..c18877ee 100644 --- a/docs/source/sections/contrib.rst +++ b/docs/source/sections/contrib.rst @@ -114,19 +114,19 @@ this. .. py:attribute:: SessionContent.cur_line - :type: string + :type: str A string of the line currently being aggregated. .. py:attribute:: SessionContent.last_line - :type: string + :type: str The previous line. .. py:attribute:: SessionContent.cur_char - :type: string + :type: str The currently written character. diff --git a/docs/source/sections/default_arguments.rst b/docs/source/sections/default_arguments.rst index d1eba3c5..565f79a5 100644 --- a/docs/source/sections/default_arguments.rst +++ b/docs/source/sections/default_arguments.rst @@ -28,28 +28,11 @@ that context: from io import StringIO buf = StringIO() - sh2 = sh(_out=buf) + sh2 = sh.bake(_out=buf) sh2.ls("/") sh2.whoami() sh2.ps("auxwf") Now, anything launched from ``sh2`` will send its output to the ``StringIO`` -instance ``buf``. - -Execution contexts may also be imported from, like it is the top-level sh -module: - -.. code-block:: python - - import sh - from io import StringIO - - buf = StringIO() - sh2 = sh(_out=buf) - - from sh2 import ls, whoami, ps - - ls("/") - whoami() - ps("auxwf") +instance ``buf``. \ No newline at end of file diff --git a/docs/source/sections/piping.rst b/docs/source/sections/piping.rst index 902885fd..aaa74249 100644 --- a/docs/source/sections/piping.rst +++ b/docs/source/sections/piping.rst @@ -6,17 +6,17 @@ Piping Basic ----- -Bash style piping is performed using function composition. Just pass -one command as the input to another, and sh will send the output of the inner -command to the input of the outer command: +Bash style piping is performed using function composition. Just pass one +command as the input to another's ``_in`` argument, and sh will send the output of +the inner command to the input of the outer command: .. code-block:: python # sort this directory by biggest file - print(sort(du(glob("*"), "-sb"), "-rn")) + print(sort("-rn", _in=du(glob("*"), "-sb"))) # print(the number of folders and files in /etc - print(wc(ls("/etc", "-1"), "-l")) + print(wc("-l", _in=ls("/etc", "-1"))) .. note:: @@ -39,7 +39,7 @@ inner command executes first, then sends its data to the outer command: .. code-block:: python - print(wc(ls("/etc", "-1"), "-l")) + print(wc("-l", _in=ls("/etc", "-1"))) In the above example, ``ls`` executes, gathers its output, then sends that output to ``wc``. This is fine for simple commands, but for commands where you need @@ -47,7 +47,7 @@ parallelism, this isn't good enough. Take the following example: .. code-block:: python - for line in tr(tail("-f", "test.log"), "[:upper:]", "[:lower:]", _iter=True): + for line in tr(_in=tail("-f", "test.log"), "[:upper:]", "[:lower:]", _iter=True): print(line) **This won't work** because the ``tail -f`` command never finishes. What you @@ -56,7 +56,7 @@ the :ref:`_piped ` special kwarg comes in handy: .. code-block:: python - for line in tr(tail("-f", "test.log", _piped=True), "[:upper:]", "[:lower:]", _iter=True): + for line in tr(_in=tail("-f", "test.log", _piped=True), "[:upper:]", "[:lower:]", _iter=True): print(line) This works by telling ``tail -f`` that it is being used in a pipeline, and that diff --git a/docs/source/sections/redirection.rst b/docs/source/sections/redirection.rst index 33286a43..14c91eb9 100644 --- a/docs/source/sections/redirection.rst +++ b/docs/source/sections/redirection.rst @@ -58,7 +58,7 @@ one of three signatures: :noindex: In addition to the previous signature, the function takes a - :class:`weakref.weakref` to the :ref:`OProc ` object. + :class:`weakref.ref` to the :ref:`OProc ` object. .. seealso:: :ref:`callbacks` diff --git a/docs/source/sections/special_arguments.rst b/docs/source/sections/special_arguments.rst index 01676066..038694fa 100644 --- a/docs/source/sections/special_arguments.rst +++ b/docs/source/sections/special_arguments.rst @@ -205,11 +205,16 @@ programs use exit codes other than 0 to indicate success. _new_session ------------ -|def| ``True`` +|def| ``False`` Determines if our forked process will be executed in its own session via :func:`os.setsid`. +.. versionchanged:: 2.0.0 + The default value of ``_new_session`` was changed from ``True`` to ``False`` + because it makes more sense for a launched process to default to being in + the process group of python script, so that it receives SIGINTs correctly. + .. note:: If ``_new_session`` is ``False``, the forked process will be put into its @@ -314,7 +319,7 @@ _iter_noblock Same as :ref:`_iter `, except the loop will not block if there is no output to iterate over. Instead, the output from the command will be -:attr:`errno.EWOULDBLOCK`. +:py:data:`errno.EWOULDBLOCK`. .. code-block:: python @@ -362,8 +367,8 @@ A callback that is *always* called when the command completes, even if it completes with an exit code that would raise an exception. After the callback is run, any exception that would be raised is raised. -The callback is passed the :class:`RunningCommand` instance, a boolean -indicating success, and the exit code. +The callback is passed the :ref:`RunningCommand ` instance, a +boolean indicating success, and the exit code. .. include:: /examples/done.rst From 1f09994aadf6e138398eabadc9f05c86698d72b3 Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Mon, 7 Aug 2023 00:00:46 -0700 Subject: [PATCH 10/21] use a portable echo --- tests/test.py | 46 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/tests/test.py b/tests/test.py index babaff92..953f540e 100644 --- a/tests/test.py +++ b/tests/test.py @@ -2213,14 +2213,21 @@ def test_tty_output(self): self.assertEqual(out, "no tty attached") def test_stringio_output(self): - from sh import echo + import sh + + py = create_tmp_test( + """ +import sys +sys.stdout.write(sys.argv[1]) +""" + ) out = StringIO() - echo("-n", "testing 123", _out=out) + sh.python(py.name, "testing 123", _out=out) self.assertEqual(out.getvalue(), "testing 123") out = BytesIO() - echo("-n", "testing 123", _out=out) + sh.python(py.name, "testing 123", _out=out) self.assertEqual(out.getvalue().decode(), "testing 123") def test_stringio_input(self): @@ -3419,9 +3426,16 @@ class ExecutionContextTests(unittest.TestCase): def test_basic(self): import sh + py = create_tmp_test( + """ +import sys +sys.stdout.write(sys.argv[1]) +""" + ) + out = StringIO() - _sh = sh.bake(_out=out) - _sh.echo("-n", "TEST") + sh2 = sh.bake(_out=out) + sh2.python(py.name, "TEST") self.assertEqual("TEST", out.getvalue()) def test_multiline_defaults(self): @@ -3443,17 +3457,24 @@ def test_multiline_defaults(self): def test_no_interfere1(self): import sh + py = create_tmp_test( + """ +import sys +sys.stdout.write(sys.argv[1]) +""" + ) + out = StringIO() _sh = sh.bake(_out=out) # noqa: F841 - _sh.echo("-n", "TEST") + _sh.python(py.name, "TEST") self.assertEqual("TEST", out.getvalue()) # Emptying the StringIO out.seek(0) out.truncate(0) - sh.echo("-n", "KO") + sh.python(py.name, "KO") self.assertEqual("", out.getvalue()) def test_no_interfere2(self): @@ -3469,16 +3490,23 @@ def test_no_interfere2(self): def test_set_in_parent_function(self): import sh + py = create_tmp_test( + """ +import sys +sys.stdout.write(sys.argv[1]) +""" + ) + out = StringIO() _sh = sh.bake(_out=out) def nested1(): - _sh.echo("-n", "TEST1") + _sh.python(py.name, "TEST1") def nested2(): import sh - sh.echo("-n", "TEST2") + sh.python(py.name, "TEST2") nested1() nested2() From c2db2f60db7bbcc84036d46f90465281f762972d Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Mon, 7 Aug 2023 00:00:59 -0700 Subject: [PATCH 11/21] bugfix when running dockerfile tests, fixes #670 --- sh.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sh.py b/sh.py index cab0cf9c..633a1bc6 100644 --- a/sh.py +++ b/sh.py @@ -67,7 +67,10 @@ from types import GeneratorType, ModuleType from typing import Any, Dict, Type, Union -__version__ = metadata.version("sh") +try: + __version__ = metadata.version("sh") +except metadata.PackageNotFoundError: + __version__ = "unknown" __project_url__ = "https://github.com/amoffat/sh" if "windows" in platform.system().lower(): # pragma: no cover From 0365c281892c3856c596e3720f8acdf23843ceef Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Mon, 7 Aug 2023 00:09:06 -0700 Subject: [PATCH 12/21] make timing of test less fiddly closes #684 --- tests/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test.py b/tests/test.py index 953f540e..196749d2 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1659,7 +1659,7 @@ def sig_handler(sig, frame): print(i) i += 1 sys.stdout.flush() - time.sleep(1) + time.sleep(2) """ ) From 89333ae48069a5b445b3535232195b2de6f4648f Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Mon, 7 Aug 2023 00:20:24 -0700 Subject: [PATCH 13/21] cleanup includes --- pyproject.toml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ce093281..5a2af9fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,12 +29,9 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] include = [ - { path = "CHANGELOG.md", format = "sdist" }, - { path = "MIGRATION.md", format = "sdist" }, - { path = "images", format = "sdist" }, - { path = "Makefile", format = "sdist" }, - { path = "tests", format = "sdist" }, - { path = "tox.ini", format = "sdist" }, + "CHANGELOG.md", + "MIGRATION.md", + "LICENSE.txt", ] [tool.poetry.dependencies] From 5f9f2ac88c9869ea9587df66492d2f970d617bd8 Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Mon, 7 Aug 2023 00:22:55 -0700 Subject: [PATCH 14/21] make tests friendlier to pytest --- tests/Dockerfile | 2 +- tests/{test.py => sh_test.py} | 0 tox.ini | 6 +++--- 3 files changed, 4 insertions(+), 4 deletions(-) rename tests/{test.py => sh_test.py} (100%) diff --git a/tests/Dockerfile b/tests/Dockerfile index 428dc538..a7301e74 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -44,4 +44,4 @@ WORKDIR /home/shtest/ ENV PATH="/home/shtest/.local/bin:$PATH" RUN pip install tox flake8 black rstcheck mypy -COPY README.rst sh.py .flake8 tox.ini tests/test.py /home/shtest/ +COPY README.rst sh.py .flake8 tox.ini tests/sh_test.py /home/shtest/ diff --git a/tests/test.py b/tests/sh_test.py similarity index 100% rename from tests/test.py rename to tests/sh_test.py diff --git a/tox.ini b/tox.ini index 0bfc16dc..a243f638 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ setenv = poller-poll: SH_TESTS_USE_SELECT=0 SH_TESTS_RUNNING=1 commands = - python test.py {posargs} + python sh_test.py {posargs} [testenv:lint] allowlist_externals = @@ -20,7 +20,7 @@ allowlist_externals = rstcheck mypy commands = - flake8 sh.py test.py - black --check --diff sh.py test.py + flake8 sh.py sh_test.py + black --check --diff sh.py sh_test.py rstcheck README.rst mypy sh.py \ No newline at end of file From 60445c749a7c862a7695e7a8cd18504109639495 Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Mon, 7 Aug 2023 00:34:30 -0700 Subject: [PATCH 15/21] call correct asyncio loop function, closes #683 --- CHANGELOG.md | 1 + sh.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index baf4a5d1..abbb327b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 2.0.5 - - Allow nested `with` contexts [#690](https://github.com/amoffat/sh/issues/690) +- Call correct asyncio function for getting event loop [#683](https://github.com/amoffat/sh/issues/683) ## 2.0.4 - 5/13/22 diff --git a/sh.py b/sh.py index 633a1bc6..955c94c5 100644 --- a/sh.py +++ b/sh.py @@ -649,7 +649,7 @@ def __init__(self, cmd, call_args, stdin, stdout, stderr): # this event is used when we want to `await` a RunningCommand. see how it gets # used in self.__await__ try: - asyncio.get_event_loop() + asyncio.get_running_loop() except RuntimeError: self.aio_output_complete = None else: @@ -2383,7 +2383,7 @@ def fn(exit_code): # if the `sh` command was launched from within a thread (so we're not in # the main thread), then we won't have an event loop. try: - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() except RuntimeError: def output_complete(): From 31a6ff45fb8ad8724df8529599d37dc47846c8bc Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Mon, 7 Aug 2023 00:52:26 -0700 Subject: [PATCH 16/21] async docs --- .../source/sections/asynchronous_execution.rst | 18 ++++++++++++++++++ docs/source/sections/special_arguments.rst | 11 +++++++++++ 2 files changed, 29 insertions(+) diff --git a/docs/source/sections/asynchronous_execution.rst b/docs/source/sections/asynchronous_execution.rst index f7b1e2d4..b410c53d 100644 --- a/docs/source/sections/asynchronous_execution.rst +++ b/docs/source/sections/asynchronous_execution.rst @@ -6,6 +6,24 @@ Asynchronous Execution sh provides a few methods for running commands and obtaining output in a non-blocking fashion. +AsyncIO +======= + +.. versionadded:: 2.0.0 + +Sh supports asyncio on commands with the :ref:`_async=True ` special +kwarg. This let's you incrementally ``await`` output produced from your command. + +.. code-block:: python + + import asyncio + import sh + + async def main(): + await sh.sleep(3, _async=True) + + asyncio.run(main()) + .. _iterable: Incremental Iteration diff --git a/docs/source/sections/special_arguments.rst b/docs/source/sections/special_arguments.rst index 038694fa..8f745f39 100644 --- a/docs/source/sections/special_arguments.rst +++ b/docs/source/sections/special_arguments.rst @@ -142,6 +142,17 @@ Automatically report exceptions for the background command. If you set this to ``False`` you should make sure to call :meth:`RunningCommand.wait` or you may swallow exceptions that happen in the background command. +.. _async_kw: + +_async +------ +.. versionadded:: 2.0.0 + +|def| ``False`` + +Allows your command to become awaitable. Use in combination with :ref:`_iter ` +and ``async for`` to incrementally await output as it is produced. + .. _env: _env From 0e82a7d462e2b1d65d30ac08d5533fbc74c2077e Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Mon, 7 Aug 2023 00:52:43 -0700 Subject: [PATCH 17/21] cleanup unsupported version --- sh.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sh.py b/sh.py index 955c94c5..dc993799 100644 --- a/sh.py +++ b/sh.py @@ -921,11 +921,7 @@ async def queue_connector(): finally: await self._aio_queue.put(None) - if sys.version_info < (3, 7, 0): - task = asyncio.ensure_future(queue_connector()) - else: - task = asyncio.create_task(queue_connector()) - + task = asyncio.create_task(queue_connector()) self._aio_task = task return self From 09caaf8680320a81220aaee01dba9cefd3311ee4 Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Mon, 7 Aug 2023 00:59:45 -0700 Subject: [PATCH 18/21] rtd config --- .readthedocs.yaml | 29 +++++++++++++++++++++++++++++ docs/requirements.txt | 1 + 2 files changed, 30 insertions(+) create mode 100644 .readthedocs.yaml create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..462bbf80 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,29 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.10" + jobs: + post_create_environment: + - pip install poetry + - poetry config virtualenvs.create false + post_install: + - poetry install + - pip install -e . + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/source/conf.py +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..e594d7ec --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +toml==0.10.2 \ No newline at end of file From 1062b9fc846bf13633d930847dfdd1aa38fec35c Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Mon, 7 Aug 2023 01:07:53 -0700 Subject: [PATCH 19/21] update docs links --- README.rst | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index f90efaf3..c33cce78 100644 --- a/README.rst +++ b/README.rst @@ -34,7 +34,7 @@ sh is *not* a collection of system commands implemented in Python. sh relies on various Unix system calls and only works on Unix-like operating systems - Linux, macOS, BSDs etc. Specifically, Windows is not supported. -`Complete documentation here `_ +`Complete documentation here `_ Installation ============ diff --git a/pyproject.toml b/pyproject.toml index 5a2af9fc..e47a4226 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,9 @@ maintainers = [ "Andrew Moffat ", "Erik Cederstrand " ] -homepage = "https://amoffat.github.io/sh/" +homepage = "https://sh.readthedocs.io/" repository = "https://github.com/amoffat/sh" -documentation = "https://amoffat.github.io/sh/" +documentation = "https://sh.readthedocs.io/" license = "MIT" classifiers = [ "Development Status :: 5 - Production/Stable", From 09c4ed9b4cfeb5d871b0e6605cef6aeef0890310 Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Mon, 7 Aug 2023 01:08:39 -0700 Subject: [PATCH 20/21] version 2.0.5 --- CHANGELOG.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abbb327b..853c080e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 2.0.5 - +## 2.0.5 - 8/7/23 - Allow nested `with` contexts [#690](https://github.com/amoffat/sh/issues/690) - Call correct asyncio function for getting event loop [#683](https://github.com/amoffat/sh/issues/683) diff --git a/pyproject.toml b/pyproject.toml index e47a4226..a8c9adaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "sh" -version = "2.0.4" +version = "2.0.5" description = "Python subprocess replacement" authors = ["Andrew Moffat "] readme = "README.rst" From e117fd897e4a84b8ce6c6393090e3292a3f72ccd Mon Sep 17 00:00:00 2001 From: Andrew Moffat Date: Mon, 7 Aug 2023 01:22:08 -0700 Subject: [PATCH 21/21] fix coverage --- .github/workflows/main.yml | 2 +- .gitignore | 3 ++- README.rst | 2 +- sh.py | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3c850f76..103b14fd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -92,7 +92,7 @@ jobs: - name: Run tests run: | - SH_TESTS_RUNNING=1 SH_TESTS_USE_SELECT=${{ matrix.use-select }} LANG=${{ matrix.lang }} poetry run coverage run -a -m unittest + SH_TESTS_RUNNING=1 SH_TESTS_USE_SELECT=${{ matrix.use-select }} LANG=${{ matrix.lang }} poetry run coverage run -a -m pytest - name: Store coverage uses: actions/upload-artifact@v2 diff --git a/.gitignore b/.gitignore index 41b053fa..246396b9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ __pycache__/ /build /dist /docs/build -/TODO.md \ No newline at end of file +/TODO.md +/htmlcov/ \ No newline at end of file diff --git a/README.rst b/README.rst index c33cce78..873f71f1 100644 --- a/README.rst +++ b/README.rst @@ -68,7 +68,7 @@ Coverage First run all of the tests:: - $> SH_TESTS_RUNNING=1 coverage run --source=sh -m unittest + $> SH_TESTS_RUNNING=1 coverage run --source=sh -m pytest This will aggregate a ``.coverage``. You may then visualize the report with:: diff --git a/sh.py b/sh.py index dc993799..28654e5a 100644 --- a/sh.py +++ b/sh.py @@ -27,7 +27,7 @@ try: from collections.abc import Mapping -except ImportError: +except ImportError: # pragma: no cover from collections import Mapping import errno @@ -69,7 +69,7 @@ try: __version__ = metadata.version("sh") -except metadata.PackageNotFoundError: +except metadata.PackageNotFoundError: # pragma: no cover __version__ = "unknown" __project_url__ = "https://github.com/amoffat/sh"