From d90141b4b3dcd63de2f3b75af9d3b58f1ae83760 Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Thu, 19 Mar 2026 23:36:11 +0100 Subject: [PATCH] Fix livereload when using `serve` from stdin We must not attempt to get the file name of stdin (the current error) and we also must not try to re-read it. Now that this case is handled specifically at `serve`, we don't need to generally expect closed files in `load_config` - it was a workaround and an insufficient one at that. --- properdocs/commands/serve.py | 21 ++++++++++++++++++--- properdocs/config/base.py | 3 --- properdocs/tests/config/base_tests.py | 16 +--------------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/properdocs/commands/serve.py b/properdocs/commands/serve.py index 2ddb9f53..afe3f847 100644 --- a/properdocs/commands/serve.py +++ b/properdocs/commands/serve.py @@ -1,10 +1,12 @@ from __future__ import annotations +import io import logging import shutil +import sys import tempfile from os.path import isdir, isfile, join -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, BinaryIO, Callable from urllib.parse import urlsplit from properdocs.commands.build import build @@ -18,7 +20,7 @@ def serve( - config_file: str | None = None, + config_file: str | BinaryIO | None = None, livereload: bool = True, build_type: str | None = None, watch_theme: bool = False, @@ -37,9 +39,22 @@ def serve( # Create a temporary build directory, and set some options to serve it site_dir = tempfile.mkdtemp(prefix='properdocs_') + get_config_file: Callable[[], str | BinaryIO | None] + if config_file is None or isinstance(config_file, str): + get_config_file = lambda: config_file + elif sys.stdin and config_file is sys.stdin.buffer: + # Stdin must be read only once, can't be reopened later. + config_file_content = sys.stdin.buffer.read() + get_config_file = lambda: io.BytesIO(config_file_content) + else: + # If closed file descriptor, reopen it through the file path instead. + get_config_file = lambda: ( + config_file.name if getattr(config_file, 'closed', False) else config_file + ) + def get_config(): config = load_config( - config_file=config_file, + config_file=get_config_file(), site_dir=site_dir, **kwargs, ) diff --git a/properdocs/config/base.py b/properdocs/config/base.py index bebb2168..5242c769 100644 --- a/properdocs/config/base.py +++ b/properdocs/config/base.py @@ -290,9 +290,6 @@ def _open_config_file(config_file: str | IO | None) -> Iterator[IO]: # If it is a string, we can assume it is a path and attempt to open it. elif isinstance(config_file, str): paths_to_try = [config_file] - # If closed file descriptor, get file path to reopen later. - elif getattr(config_file, 'closed', False): - paths_to_try = [config_file.name] else: result_config_file = config_file paths_to_try = None diff --git a/properdocs/tests/config/base_tests.py b/properdocs/tests/config/base_tests.py index 2463daaf..acecea22 100644 --- a/properdocs/tests/config/base_tests.py +++ b/properdocs/tests/config/base_tests.py @@ -118,20 +118,6 @@ def test_load_from_open_file(self, temp_path): # load_config will always close the file self.assertTrue(config_file.closed) - @tempdir() - def test_load_from_closed_file(self, temp_dir): - """ - The `serve` command with auto-reload may pass in a closed file descriptor. - Ensure `load_config` reloads the closed file. - """ - with open(os.path.join(temp_dir, 'properdocs.yml'), 'w') as config_file: - config_file.write("site_name: ProperDocs Test\ntheme: mkdocs\n") - os.mkdir(os.path.join(temp_dir, 'docs')) - - cfg = base.load_config(config_file=config_file) - self.assertTrue(isinstance(cfg, defaults.ProperDocsConfig)) - self.assertEqual(cfg.site_name, 'ProperDocs Test') - @tempdir() def test_load_missing_required(self, temp_dir): """`site_name` is a required setting.""" @@ -260,7 +246,7 @@ def test_load_from_file_with_relative_paths(self, config_dir): docs_dir = os.path.join(config_dir, 'src') os.mkdir(docs_dir) - cfg = base.load_config(config_file=config_file) + cfg = base.load_config(config_file=config_fname) self.assertTrue(isinstance(cfg, defaults.ProperDocsConfig)) self.assertEqual(cfg.site_name, 'ProperDocs Test') self.assertEqual(cfg.docs_dir, docs_dir)