From 71edea347482192cd19f81f619a793183e220e27 Mon Sep 17 00:00:00 2001 From: Vaughn Kottler Date: Sat, 2 Nov 2024 01:30:23 +0000 Subject: [PATCH] 5.7.10 - Prefer 'cat' for async reads --- .github/workflows/python-package.yml | 2 +- README.md | 4 ++-- local/variables/package.yaml | 2 +- pyproject.toml | 2 +- runtimepy/__init__.py | 4 ++-- runtimepy/net/server/__init__.py | 13 +++++-------- runtimepy/util.py | 27 ++++++++++++++++++++++++++- tests/test_util.py | 20 ++++++++++++++++++++ 8 files changed, 58 insertions(+), 16 deletions(-) create mode 100644 tests/test_util.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 13d98b0..e8e1a69 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -77,7 +77,7 @@ jobs: - run: | mk python-release owner=vkottler \ - repo=runtimepy version=5.7.9 + repo=runtimepy version=5.7.10 if: | matrix.python-version == '3.12' && matrix.system == 'ubuntu-latest' diff --git a/README.md b/README.md index 78841a8..c714b02 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ ===================================== generator=datazen version=3.1.4 - hash=9fbafffa1da655b349a05dfa48addf0b + hash=1e8298e9f6423ee6f9836f5ec04cc3ab ===================================== --> -# runtimepy ([5.7.9](https://pypi.org/project/runtimepy/)) +# runtimepy ([5.7.10](https://pypi.org/project/runtimepy/)) [![python](https://img.shields.io/pypi/pyversions/runtimepy.svg)](https://pypi.org/project/runtimepy/) ![Build Status](https://github.com/vkottler/runtimepy/workflows/Python%20Package/badge.svg) diff --git a/local/variables/package.yaml b/local/variables/package.yaml index cd9da9f..4d17adf 100644 --- a/local/variables/package.yaml +++ b/local/variables/package.yaml @@ -1,5 +1,5 @@ --- major: 5 minor: 7 -patch: 9 +patch: 10 entry: runtimepy diff --git a/pyproject.toml b/pyproject.toml index 52ea1d1..bcbe89f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta:__legacy__" [project] name = "runtimepy" -version = "5.7.9" +version = "5.7.10" description = "A framework for implementing Python services." readme = "README.md" requires-python = ">=3.12" diff --git a/runtimepy/__init__.py b/runtimepy/__init__.py index 54f6708..2db77a4 100644 --- a/runtimepy/__init__.py +++ b/runtimepy/__init__.py @@ -1,7 +1,7 @@ # ===================================== # generator=datazen # version=3.1.4 -# hash=5d30f53776c61076df3ffeca2fdb30a1 +# hash=01934a9c90fba4488654f035acc8b84e # ===================================== """ @@ -10,7 +10,7 @@ DESCRIPTION = "A framework for implementing Python services." PKG_NAME = "runtimepy" -VERSION = "5.7.9" +VERSION = "5.7.10" # runtimepy-specific content. METRICS_NAME = "metrics" diff --git a/runtimepy/net/server/__init__.py b/runtimepy/net/server/__init__.py index c927125..9142443 100644 --- a/runtimepy/net/server/__init__.py +++ b/runtimepy/net/server/__init__.py @@ -11,7 +11,6 @@ from typing import Any, Optional, TextIO, Union # third-party -import aiofiles from vcorelib import DEFAULT_ENCODING from vcorelib.io import IndentedFileWriter, JsonObject from vcorelib.paths import Pathlike, find_file, normalize @@ -26,7 +25,7 @@ from runtimepy.net.server.html import HtmlApp, HtmlApps, get_html, html_handler from runtimepy.net.server.json import encode_json, json_handler from runtimepy.net.tcp.http import HttpConnection -from runtimepy.util import normalize_root, path_has_part +from runtimepy.util import normalize_root, path_has_part, read_binary MIMETYPES_INIT = False @@ -158,10 +157,9 @@ async def render_markdown_file( ) -> bytes: """Render a markdown file as HTML and return the result.""" - async with aiofiles.open(path, mode="r") as path_fd: - return self.render_markdown( - await path_fd.read(), response, **kwargs - ) + return self.render_markdown( + (await read_binary(path)).decode(), response, **kwargs + ) async def try_file( self, path: PathMaybeQuery, response: ResponseHeader @@ -195,8 +193,7 @@ async def try_file( self.logger.info("Serving '%s' (MIME: %s)", candidate, mime) # Return the file data. - async with aiofiles.open(candidate, mode="rb") as path_fd: - result = await path_fd.read() + result = await read_binary(candidate) break diff --git a/runtimepy/util.py b/runtimepy/util.py index 73c5788..7a1b98b 100644 --- a/runtimepy/util.py +++ b/runtimepy/util.py @@ -3,14 +3,39 @@ """ # built-in +import asyncio from os import sep from pathlib import Path -from typing import Iterator, Union +from shutil import which +from typing import Iterator, Optional, Union # third-party +import aiofiles from vcorelib.paths import normalize ROOT_PATH = Path(sep) +USE_CAT: Optional[bool] = None + + +async def read_binary(path: Path, use_aiofiles: bool = False) -> bytes: + """An async wrapper for reading file contents.""" + + global USE_CAT # pylint: disable=global-statement + if USE_CAT is None: + USE_CAT = which("cat") is not None + + # Avoid ballooning a thread pool with one-off reads. + if USE_CAT and not use_aiofiles: + proc = await asyncio.create_subprocess_exec( + "cat", str(path), stdout=asyncio.subprocess.PIPE + ) + result, _ = await proc.communicate() + + else: + async with aiofiles.open(path, mode="rb") as path_fd: + result = await path_fd.read() + + return result def normalize_root(*src_parts: Union[str, Path]) -> Path: diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 0000000..8c5f2b6 --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,20 @@ +""" +Test the 'util' module. +""" + +# third-party +from pytest import mark + +# module under test +from runtimepy.util import read_binary + +# internal +from tests.resources import resource + + +@mark.asyncio +async def test_read_binary(): + """Test 'read_binary' invocations.""" + + assert await read_binary(resource("test.txt")) + assert await read_binary(resource("test.txt"), use_aiofiles=True)