From 9fc0415ee517a55c8c587958dc9969365ec54d21 Mon Sep 17 00:00:00 2001 From: Abhiix0 <24r21a6778@mlrit.ac.in> Date: Fri, 21 Aug 2026 06:11:37 +0530 Subject: [PATCH 1/6] Fix the UTF-8 exit-code bug --- src/spawn/utils/console.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/spawn/utils/console.py b/src/spawn/utils/console.py index 5f8e6bd..b9ef233 100644 --- a/src/spawn/utils/console.py +++ b/src/spawn/utils/console.py @@ -1,3 +1,9 @@ +import sys from rich.console import Console -console = Console() \ No newline at end of file +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") +if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="replace") + +console = Console() From 9b05f693ec85276dd4aebc5f101186e3f67050b8 Mon Sep 17 00:00:00 2001 From: Abhiix0 <24r21a6778@mlrit.ac.in> Date: Fri, 21 Aug 2026 06:17:55 +0530 Subject: [PATCH 2/6] Fix Windows reserved device names --- src/spawn/utils/validators.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/spawn/utils/validators.py b/src/spawn/utils/validators.py index b143a21..17b7377 100644 --- a/src/spawn/utils/validators.py +++ b/src/spawn/utils/validators.py @@ -2,8 +2,22 @@ from spawn.core.exceptions import SpawnError +_WINDOWS_RESERVED = { + "CON", "PRN", "AUX", "NUL", + *(f"COM{i}" for i in range(1, 10)), + *(f"LPT{i}" for i in range(1, 10)), +} + def validate_project_name(name: str) -> None: + # Check for Windows reserved device names (case-insensitive, strip any + # extension so e.g. "NUL.txt" is caught too). + stem = name.split(".")[0].upper() + if stem in _WINDOWS_RESERVED: + raise SpawnError( + f"'{name}' is a reserved Windows device name and can't be used as a project name." + ) + if not re.search(r"[a-zA-Z0-9]", name): raise SpawnError( "Project name must contain at least one letter or number." @@ -12,4 +26,4 @@ def validate_project_name(name: str) -> None: if not re.match(r"^[a-zA-Z0-9_-]+$", name): raise SpawnError( "Project name can only contain letters, numbers, hyphens (-), and underscores (_)." - ) \ No newline at end of file + ) From f94c0538ca831783b3c8f1d5f9df2b676477558b Mon Sep 17 00:00:00 2001 From: Abhiix0 <24r21a6778@mlrit.ac.in> Date: Fri, 21 Aug 2026 06:19:02 +0530 Subject: [PATCH 3/6] Document the Python 3.12 requirement clearly --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 524c658..5f1a02c 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ It's repetitive. It's inconsistent. And you haven't written a single line of *re ## Prerequisites - **Python 3.12+** — [Download here](https://python.org/downloads) + > `pip install spawnio` will silently return "no matching distribution" on Python < 3.12. If you're using uv, run `uv python install 3.12` to get a compatible interpreter in seconds. - **uv** — [Install guide](https://github.com/astral-sh/uv) - **Git** — [Download here](https://git-scm.com/downloads) From 3ebdb7dd4bc3020003e59df1af6cb56cb2142e59 Mon Sep 17 00:00:00 2001 From: Abhiix0 <24r21a6778@mlrit.ac.in> Date: Fri, 21 Aug 2026 06:27:18 +0530 Subject: [PATCH 4/6] Add validator tests --- tests/test_console.py | 33 +++++++++++++++++++++++++++++++++ tests/test_validators.py | 22 ++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 tests/test_console.py diff --git a/tests/test_console.py b/tests/test_console.py new file mode 100644 index 0000000..cb1583c --- /dev/null +++ b/tests/test_console.py @@ -0,0 +1,33 @@ +"""Tests for spawn.utils.console — encoding safety.""" + +import io + +import pytest + +from rich.console import Console + + +def test_console_prints_unicode_glyphs_without_error(): + """Printing ✨ and 🏥 to a non-terminal byte stream must not raise UnicodeEncodeError. + + This simulates the Windows pipe condition (stdout redirected, encoding=cp1252) + by constructing a Console that writes to a BytesIO with latin-1 — characters + outside that range would crash a naive encoder. The fix in console.py uses + errors='replace', so the test confirms no exception is raised. + """ + # BytesIO with a wrapper that rejects non-latin-1 bytes unless replaced + buf = io.BytesIO() + wrapper = io.TextIOWrapper(buf, encoding="latin-1", errors="replace") + + # Build a Console pointing at this restricted stream — same parameters + # the real console uses after the reconfigure fix. + test_console = Console(file=wrapper, highlight=False) + + # Neither of these characters is representable in latin-1; they must + # degrade to '?' rather than raising UnicodeEncodeError. + try: + test_console.print("✨ Project Created Successfully 🏥") + except UnicodeEncodeError as exc: + pytest.fail(f"UnicodeEncodeError raised when printing glyphs: {exc}") + finally: + wrapper.flush() diff --git a/tests/test_validators.py b/tests/test_validators.py index 9f0698c..70080e9 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -91,3 +91,25 @@ def test_dot_gives_bad_chars_message(): def test_at_sign_gives_bad_chars_message(): with pytest.raises(SpawnError, match=_BAD_CHARS): validate_project_name("my@project") + + +# --------------------------------------------------------------------------- +# Windows reserved device names → "reserved Windows device name" +# --------------------------------------------------------------------------- + +_RESERVED = "reserved Windows device name" + + +@pytest.mark.parametrize("name", [ + "CON", "PRN", "AUX", "NUL", + "COM1", "LPT1", + "con", "prn", +]) +def test_reserved_windows_name_raises(name): + with pytest.raises(SpawnError, match=_RESERVED): + validate_project_name(name) + + +def test_valid_name_is_not_caught_as_reserved(): + """A normal name must never be mistaken for a reserved device name.""" + validate_project_name("my-project") From c927bf68206ae78ad3bdd907e54be90817e67689 Mon Sep 17 00:00:00 2001 From: Abhiix0 <24r21a6778@mlrit.ac.in> Date: Fri, 21 Aug 2026 06:32:36 +0530 Subject: [PATCH 5/6] Add publish.yml --- .github/workflows/publish.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..3089969 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,24 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +jobs: + build-and-publish: + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write # required for trusted publishing + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + + - name: Build package + run: uv build + + - name: Publish to PyPI + run: uv publish \ No newline at end of file From 51b89c77efe21d09e73d1c409cd69b3aba3613fe Mon Sep 17 00:00:00 2001 From: Abhiix0 <24r21a6778@mlrit.ac.in> Date: Fri, 21 Aug 2026 07:06:41 +0530 Subject: [PATCH 6/6] Version Bump --- README.md | 1 + docs/changelog.md | 19 +++++++++++++++++++ docs/getting-started.md | 4 +++- pyproject.toml | 2 +- src/spawn/__init__.py | 2 +- 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5f1a02c..6c7726b 100644 --- a/README.md +++ b/README.md @@ -766,6 +766,7 @@ All tests should pass. If they don't, please [open an issue](https://github.com/ **Recently shipped** +- Fixed UTF-8 console output crash on Windows (non-terminal stdout), added Windows reserved device name validation, clarified Python 3.12 prerequisite (v1.0.7) - Removed duplicate banner from `spawn create`; `spawn` alone now shows the banner and command overview (v1.0.6) - Published to PyPI as `spawnio` — install with `pip install spawnio` (v1.0.5) - Arrow-key menus, no-args banner, and consistent Ctrl+C handling (v1.0.5) diff --git a/docs/changelog.md b/docs/changelog.md index 543666e..bb6c0e7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,25 @@ All notable changes to Spawn are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## v1.0.7 — 2026 + +### Fixed + +- **UTF-8 console output crash on Windows** — `spawn create` and `spawn doctor` no + longer raise `UnicodeEncodeError` when stdout is redirected or piped (e.g. + `spawn create ... > out.txt`). stdout/stderr are now reconfigured to UTF-8 with + `errors="replace"` at startup, so glyphs like ✨ and 🏥 degrade to `?` instead + of crashing the process. +- **Windows reserved device name validation** — project names matching Windows + reserved device names (`CON`, `PRN`, `AUX`, `NUL`, `COM1`–`COM9`, `LPT1`–`LPT9`, + case-insensitive) are now rejected upfront by `validate_project_name()` with a + clear `SpawnError`. Previously `PRN` raised a raw `NotADirectoryError` traceback. + +### Documentation + +- Clarified that `pip install spawnio` fails silently with "no matching distribution" + on Python < 3.12, and that `uv python install 3.12` resolves it in seconds. + ## v1.0.6 — 2026 ### Fixed diff --git a/docs/getting-started.md b/docs/getting-started.md index 7399ac7..b1a3219 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -8,6 +8,8 @@ | uv | any | `pip install uv` or [github.com/astral-sh/uv](https://github.com/astral-sh/uv) | | Git | any | [git-scm.com/downloads](https://git-scm.com/downloads/) | +> **Python 3.12 is required.** `pip install spawnio` will silently return "no matching distribution" on Python < 3.12. If you're using uv, run `uv python install 3.12` to get a compatible interpreter in seconds. + --- ## 2. Installation @@ -47,7 +49,7 @@ After installation, `spawn` is available anywhere on your machine. ```bash spawn version -# Spawn v1.0.6 +# Spawn v1.0.7 ``` Or just run `spawn` with no arguments to see the command overview: diff --git a/pyproject.toml b/pyproject.toml index f529f59..d699f41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "spawnio" -version = "1.0.6" +version = "1.0.7" description = "Spawn a ready-to-use Python project in seconds" readme = "README.md" requires-python = ">=3.12" diff --git a/src/spawn/__init__.py b/src/spawn/__init__.py index eba0ba0..707c73f 100644 --- a/src/spawn/__init__.py +++ b/src/spawn/__init__.py @@ -3,4 +3,4 @@ try: __version__ = version("spawnio") except PackageNotFoundError: - __version__ = "1.0.6" + __version__ = "1.0.7"