From 180379cddd0bef69ad852ca2db1badac691fafc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 21:06:28 +0000 Subject: [PATCH 1/2] build(deps): bump typer from 0.24.1 to 0.27.1 Bumps [typer](https://github.com/fastapi/typer) from 0.24.1 to 0.27.1. - [Release notes](https://github.com/fastapi/typer/releases) - [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md) - [Commits](https://github.com/fastapi/typer/compare/0.24.1...0.27.1) --- updated-dependencies: - dependency-name: typer dependency-version: 0.27.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- uv.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/uv.lock b/uv.lock index 352ec77..cdd6478 100644 --- a/uv.lock +++ b/uv.lock @@ -2945,17 +2945,17 @@ wheels = [ [[package]] name = "typer" -version = "0.24.1" +version = "0.27.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, - { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/40/4a3db7990d1f62a53182aa96eaef57aeb2886a27f90a195bc66713565d31/typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df", size = 203994, upload-time = "2026-08-03T14:41:03.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/43/89/9518bc0c3929bee36b3a4a8e3daddd6e03f92f9961c66d4983b837160543/typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56", size = 122874, upload-time = "2026-08-03T14:41:04.391Z" }, ] [[package]] From f09ac31a711774255f77592d2dc6fd4c00612dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20M=C3=BCller?= Date: Wed, 26 Aug 2026 23:12:28 +0200 Subject: [PATCH 2/2] fix: adapt to typer 0.27 context and testing changes typer 0.27 invokes command functions outside the Click context stack, so click.get_current_context() is empty inside a command and --quiet stopped taking effect. Record the flag in a ContextVar set by the root callback and read the root context off the injected ctx parameter instead. Click 8.5 also dropped CliRunner.isolated_filesystem; the init tests now use tmp_path with monkeypatch.chdir. typer 0.27 types prompt() as returning Any again, so init_cli narrows it back with a cast. --- src/scribae/cli_output.py | 21 ++++---- src/scribae/init_cli.py | 3 +- src/scribae/main.py | 8 ++- tests/unit/cli_output_test.py | 42 +++++++++++++++ tests/unit/init_cli_test.py | 99 ++++++++++++++++++----------------- 5 files changed, 110 insertions(+), 63 deletions(-) create mode 100644 tests/unit/cli_output_test.py diff --git a/src/scribae/cli_output.py b/src/scribae/cli_output.py index ee82ec2..3a554c2 100644 --- a/src/scribae/cli_output.py +++ b/src/scribae/cli_output.py @@ -1,21 +1,22 @@ from __future__ import annotations -from collections.abc import Mapping -from typing import Any, cast +from contextvars import ContextVar +from typing import Any -import click import typer +# typer 0.27 invokes command functions outside the Click context stack, so +# `click.get_current_context()` is empty inside a command. The root callback +# therefore records the flag here instead of on `ctx.obj`. +_quiet: ContextVar[bool] = ContextVar("scribae_quiet", default=False) -def _context_obj() -> Mapping[str, Any]: - context = click.get_current_context(silent=True) - if context is None or context.obj is None: - return {} - return cast(Mapping[str, Any], context.obj) + +def set_quiet(quiet: bool) -> None: + _quiet.set(quiet) def is_quiet() -> bool: - return bool(_context_obj().get("quiet", False)) + return _quiet.get() def echo_info(message: str, *, err: bool = False) -> None: @@ -30,4 +31,4 @@ def secho_info(message: str, **kwargs: Any) -> None: typer.secho(message, **kwargs) -__all__ = ["echo_info", "is_quiet", "secho_info"] +__all__ = ["echo_info", "is_quiet", "secho_info", "set_quiet"] diff --git a/src/scribae/init_cli.py b/src/scribae/init_cli.py index ddb692e..07b490a 100644 --- a/src/scribae/init_cli.py +++ b/src/scribae/init_cli.py @@ -47,7 +47,8 @@ def _prompt_text(label: str, description: str, example: str, *, default: str, sh typer.secho(label, fg=typer.colors.CYAN, bold=True) typer.echo(description) typer.secho(f"Example: {example}", fg=typer.colors.MAGENTA) - return typer.prompt("Value", default=default, show_default=show_default) + # typer 0.27 types `prompt` as returning Any, so narrow it back to the declared return type. + return cast(str, typer.prompt("Value", default=default, show_default=show_default)) def _split_list(value: str) -> list[str]: diff --git a/src/scribae/main.py b/src/scribae/main.py index 6b13944..19b24d9 100644 --- a/src/scribae/main.py +++ b/src/scribae/main.py @@ -2,10 +2,10 @@ import os -import click import typer from .brief_cli import brief_command +from .cli_output import set_quiet from .feedback_cli import feedback_command from .idea_cli import idea_command from .init_cli import init_command @@ -44,11 +44,9 @@ def app_callback( ) -> None: """Root Scribae CLI callback.""" setup_logging() - ctx.obj = {"quiet": quiet} + set_quiet(quiet) if no_color or "NO_COLOR" in os.environ: - context = click.get_current_context(silent=True) - if context is not None: - context.color = False + ctx.color = False app.command("idea", help="Brainstorm article ideas from a note with project-aware guidance.")(idea_command) diff --git a/tests/unit/cli_output_test.py b/tests/unit/cli_output_test.py new file mode 100644 index 0000000..5334bd8 --- /dev/null +++ b/tests/unit/cli_output_test.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import pytest + +from scribae.cli_output import echo_info, is_quiet, secho_info, set_quiet + + +@pytest.fixture(autouse=True) +def _reset_quiet() -> None: + set_quiet(False) + + +def test_is_quiet_defaults_to_false_outside_a_cli_run() -> None: + assert is_quiet() is False + + +def test_set_quiet_toggles_the_flag() -> None: + set_quiet(True) + + assert is_quiet() is True + + +def test_echo_info_is_suppressed_when_quiet(capsys: pytest.CaptureFixture[str]) -> None: + set_quiet(True) + + echo_info("hello") + + assert capsys.readouterr().out == "" + + +def test_echo_info_prints_when_not_quiet(capsys: pytest.CaptureFixture[str]) -> None: + echo_info("hello") + + assert capsys.readouterr().out == "hello\n" + + +def test_secho_info_is_suppressed_when_quiet(capsys: pytest.CaptureFixture[str]) -> None: + set_quiet(True) + + secho_info("hello") + + assert capsys.readouterr().out == "" diff --git a/tests/unit/init_cli_test.py b/tests/unit/init_cli_test.py index 906a0de..9c156e0 100644 --- a/tests/unit/init_cli_test.py +++ b/tests/unit/init_cli_test.py @@ -1,5 +1,6 @@ from pathlib import Path +import pytest import yaml from typer.testing import CliRunner @@ -9,6 +10,16 @@ runner = CliRunner() +@pytest.fixture(autouse=True) +def _isolated_cwd(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Run every test in an empty directory. + + Click 8.5 dropped ``CliRunner.isolated_filesystem``; ``tmp_path`` gives the same + isolation and pytest cleans it up. + """ + monkeypatch.chdir(tmp_path) + + def _questionnaire_input() -> str: return "\n".join( [ @@ -24,75 +35,69 @@ def _questionnaire_input() -> str: def test_init_writes_config_in_current_dir() -> None: - with runner.isolated_filesystem(): - result = runner.invoke(app, ["init"], input=_questionnaire_input()) - - assert result.exit_code == 0 - config_path = Path("scribae.yaml") - assert config_path.exists() - payload = yaml.safe_load(config_path.read_text(encoding="utf-8")) - assert payload["site_name"] == "Scribae Blog" - assert payload["domain"] == "https://example.com" - assert payload["audience"] == "developers and writers" - assert payload["tone"] == "friendly and practical" - assert payload["keywords"] == ["seo", "content strategy"] - assert payload["language"] == "en" - assert payload["allowed_tags"] == ["product-analytics", "case-study", "compliance"] + result = runner.invoke(app, ["init"], input=_questionnaire_input()) + + assert result.exit_code == 0 + config_path = Path("scribae.yaml") + assert config_path.exists() + payload = yaml.safe_load(config_path.read_text(encoding="utf-8")) + assert payload["site_name"] == "Scribae Blog" + assert payload["domain"] == "https://example.com" + assert payload["audience"] == "developers and writers" + assert payload["tone"] == "friendly and practical" + assert payload["keywords"] == ["seo", "content strategy"] + assert payload["language"] == "en" + assert payload["allowed_tags"] == ["product-analytics", "case-study", "compliance"] def test_init_prompts_allowed_tag_example() -> None: - with runner.isolated_filesystem(): - result = runner.invoke(app, ["init"], input=_questionnaire_input()) + result = runner.invoke(app, ["init"], input=_questionnaire_input()) - assert result.exit_code == 0 - output = strip_ansi(result.output) - assert "Allowed metadata tags" in output - assert "Example: product-analytics, case-study, compliance" in output + assert result.exit_code == 0 + output = strip_ansi(result.output) + assert "Allowed metadata tags" in output + assert "Example: product-analytics, case-study, compliance" in output def test_init_writes_config_in_project_dir() -> None: - with runner.isolated_filesystem(): - result = runner.invoke(app, ["init", "--project", "demo"], input=_questionnaire_input()) + result = runner.invoke(app, ["init", "--project", "demo"], input=_questionnaire_input()) - assert result.exit_code == 0 - config_path = Path("demo") / "scribae.yaml" - assert config_path.exists() + assert result.exit_code == 0 + config_path = Path("demo") / "scribae.yaml" + assert config_path.exists() def test_init_writes_config_to_custom_file() -> None: - with runner.isolated_filesystem(): - result = runner.invoke( - app, - ["init", "--file", "config/custom.yaml"], - input=_questionnaire_input(), - ) + result = runner.invoke( + app, + ["init", "--file", "config/custom.yaml"], + input=_questionnaire_input(), + ) - assert result.exit_code == 0 - config_path = Path("config") / "custom.yaml" - assert config_path.exists() + assert result.exit_code == 0 + config_path = Path("config") / "custom.yaml" + assert config_path.exists() def test_init_prompts_before_overwrite() -> None: - with runner.isolated_filesystem(): - config_path = Path("scribae.yaml") - config_path.write_text("site_name: old", encoding="utf-8") + config_path = Path("scribae.yaml") + config_path.write_text("site_name: old", encoding="utf-8") - result = runner.invoke(app, ["init"], input="n\n") + result = runner.invoke(app, ["init"], input="n\n") - assert result.exit_code != 0 - assert config_path.read_text(encoding="utf-8") == "site_name: old" + assert result.exit_code != 0 + assert config_path.read_text(encoding="utf-8") == "site_name: old" def test_init_force_overwrites_existing_file() -> None: - with runner.isolated_filesystem(): - config_path = Path("scribae.yaml") - config_path.write_text("site_name: old", encoding="utf-8") + config_path = Path("scribae.yaml") + config_path.write_text("site_name: old", encoding="utf-8") - result = runner.invoke(app, ["init", "--force"], input=_questionnaire_input()) + result = runner.invoke(app, ["init", "--force"], input=_questionnaire_input()) - assert result.exit_code == 0 - payload = yaml.safe_load(config_path.read_text(encoding="utf-8")) - assert payload["site_name"] == "Scribae Blog" + assert result.exit_code == 0 + payload = yaml.safe_load(config_path.read_text(encoding="utf-8")) + assert payload["site_name"] == "Scribae Blog" def test_init_rejects_project_and_file_options() -> None: