diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 888ced54..74b72729 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -55,7 +55,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] use-select: [0, 1] lang: [C, en_US.UTF-8] diff --git a/CHANGELOG.md b/CHANGELOG.md index dda831bd..f705b552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 2.2.0 - 1/9/25 + +- `return_cmd` with `await` now works correctly [#743](https://github.com/amoffat/sh/issues/743) +- Formal support for Python 3.12 + ## 2.1.0 - 10/8/24 - Add contrib command `sh.contrib.bash` [#736](https://github.com/amoffat/sh/pull/736) diff --git a/README.rst b/README.rst index 873f71f1..962e8dc8 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ | -sh is a full-fledged subprocess replacement for Python 3.8 - 3.11, and PyPy +sh is a full-fledged subprocess replacement for Python 3.8 - 3.12, and PyPy that allows you to call *any* program as if it were a function: .. code:: python diff --git a/pyproject.toml b/pyproject.toml index 9b8b0348..6621570b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [tool.poetry] name = "sh" -version = "2.1.0" +version = "2.2.0" description = "Python subprocess replacement" authors = ["Andrew Moffat "] readme = "README.rst" maintainers = [ "Andrew Moffat ", - "Erik Cederstrand " + "Erik Cederstrand ", ] homepage = "https://sh.readthedocs.io/" repository = "https://github.com/amoffat/sh" diff --git a/sh.py b/sh.py index 4a83a3ac..b02d6e16 100644 --- a/sh.py +++ b/sh.py @@ -27,6 +27,19 @@ from collections import deque from collections.abc import Mapping +import platform +from importlib import metadata + +try: + __version__ = metadata.version("sh") +except metadata.PackageNotFoundError: # pragma: no cover + __version__ = "unknown" + +if "windows" in platform.system().lower(): # pragma: no cover + raise ImportError( + f"sh {__version__} is currently only supported on Linux and macOS." + ) + import errno import fcntl import gc @@ -35,7 +48,6 @@ import inspect import logging import os -import platform import pty import pwd import re @@ -55,7 +67,6 @@ from asyncio import Queue as AQueue from contextlib import contextmanager from functools import partial -from importlib import metadata from io import BytesIO, StringIO, UnsupportedOperation from io import open as fdopen from locale import getpreferredencoding @@ -64,17 +75,8 @@ from types import GeneratorType, ModuleType from typing import Any, Dict, Type, Union -try: - __version__ = metadata.version("sh") -except metadata.PackageNotFoundError: # pragma: no cover - __version__ = "unknown" __project_url__ = "https://github.com/amoffat/sh" -if "windows" in platform.system().lower(): # pragma: no cover - raise ImportError( - f"sh {__version__} is currently only supported on Linux and macOS." - ) - TEE_STDOUT = {True, "out", 1} TEE_STDERR = {"err", 2} @@ -887,7 +889,10 @@ def __next__(self): def __await__(self): async def wait_for_completion(): await self.aio_output_complete.wait() - return str(self) + if self.call_args["return_cmd"]: + return self + else: + return str(self) return wait_for_completion().__await__() diff --git a/tests/sh_test.py b/tests/sh_test.py index d12c3dcf..0fe03111 100644 --- a/tests/sh_test.py +++ b/tests/sh_test.py @@ -1707,7 +1707,6 @@ def test_async(self): ) alternating = [] - q = AQueue() async def producer(q): alternating.append(1) @@ -1722,19 +1721,20 @@ async def consumer(q): self.assertEqual(msg, "hello") alternating.append(2) - loop = asyncio.get_event_loop() - fut = asyncio.gather(producer(q), consumer(q)) - loop.run_until_complete(fut) + async def main(): + q = AQueue() + await asyncio.gather(producer(q), consumer(q)) + + asyncio.run(main()) self.assertListEqual(alternating, [1, 2, 1, 2]) def test_async_exc(self): py = create_tmp_test("""exit(34)""") async def producer(): - await python(py.name, _async=True) + await python(py.name, _async=True, _return_cmd=False) - loop = asyncio.get_event_loop() - self.assertRaises(sh.ErrorReturnCode_34, loop.run_until_complete, producer()) + self.assertRaises(sh.ErrorReturnCode_34, asyncio.run, producer()) def test_async_iter(self): py = create_tmp_test( @@ -1743,7 +1743,6 @@ def test_async_iter(self): print(i) """ ) - q = AQueue() # this list will prove that our coroutines are yielding to eachother as each # line is produced @@ -1763,9 +1762,11 @@ async def consumer(q): return alternating.append(2) - loop = asyncio.get_event_loop() - res = asyncio.gather(producer(q), consumer(q)) - loop.run_until_complete(res) + async def main(): + q = AQueue() + await asyncio.gather(producer(q), consumer(q)) + + asyncio.run(main()) self.assertListEqual(alternating, [1, 2, 1, 2, 1, 2, 1, 2, 1, 2]) def test_async_iter_exc(self): @@ -1783,8 +1784,23 @@ async def producer(): async for line in python(py.name, _async=True): lines.append(int(line.strip())) - loop = asyncio.get_event_loop() - self.assertRaises(sh.ErrorReturnCode_34, loop.run_until_complete, producer()) + self.assertRaises(sh.ErrorReturnCode_34, asyncio.run, producer()) + + def test_async_return_cmd(self): + py = create_tmp_test( + """ +import sys +sys.exit(0) +""" + ) + + async def main(): + result = await python(py.name, _async=True, _return_cmd=True) + self.assertIsInstance(result, sh.RunningCommand) + result_str = await python(py.name, _async=True, _return_cmd=False) + self.assertIsInstance(result_str, str) + + asyncio.run(main()) def test_handle_both_out_and_err(self): py = create_tmp_test(