ape-linux 0.5.2__tar.gz → 0.5.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -110,7 +110,19 @@ The flow in `ape_linux.py` is intentionally minimal:
110
110
  yields `None` so **no** `model_settings` are sent — deliberately, because a
111
111
  hard-coded temperature crashes models that reject sampling settings (some reasoning
112
112
  models, Claude Opus 4.7/4.8) — and an unparseable value exits `1` before any LLM
113
- call.
113
+ call. `call_llm()` also sets `pydantic_ai.BANNER_ENABLED = False` before building
114
+ the agent: recent Pydantic AI versions print a first-run banner (logo plus an
115
+ "observability: off" block) to **stderr** on the first agent run in a terminal,
116
+ which would sit next to Ape's one-line command output. That attribute is Pydantic
117
+ AI's documented switch for it. This is the **one** `ty: ignore` in the codebase
118
+ (`invalid-assignment`), and it is deliberate: Pydantic AI declares the flag as a
119
+ bare `BANNER_ENABLED = True` with no annotation, so ty infers `Literal[True]` and
120
+ rejects assigning `False` — the very thing the attribute exists for. No
121
+ `pydantic-ai-slim` floor bump is needed: the assignment is a harmless no-op on
122
+ versions that predate the banner. Note the banner is invisible to the test suite
123
+ regardless — Pydantic AI suppresses it whenever `PYTEST_VERSION`, `CI` or
124
+ `PYDANTIC_AI_NO_BANNER` is in the environment — so a regression here would only
125
+ show up when a user runs `ape` in a terminal.
114
126
  4. Errors are flattened to one-line stderr messages, raising `SystemExit(1)` —
115
127
  `ModelHTTPError` reports status/message; any other exception (bad credentials,
116
128
  unknown provider) prints `str(error)`. There is no CLI framework swallowing
@@ -133,6 +145,11 @@ stderr, exit `2`). The `mockenv` fixture sets a dummy `APE_API_KEY` and `APE_MOD
133
145
  blank/whitespace** `APE_MODEL` or `APE_API_KEY` is each checked to exit `1` before
134
146
  `call_llm` is ever reached.
135
147
 
148
+ `test_call_llm_disables_the_pydantic_ai_banner` runs the real `call_llm` (with
149
+ `TestModel` swapped in for `infer_model`) and asserts the banner flag ends up `False`.
150
+ It sets the flag back to `True` via `monkeypatch.setattr(..., raising=False)` first, so
151
+ the check is meaningful and monkeypatch restores the global afterwards.
152
+
136
153
  `build_provider()` is covered directly (not through `main()`): tests construct the
137
154
  `openai` and `anthropic` providers with the standard credential vars deleted and assert
138
155
  the injected key lands on `provider.client.api_key` (proving the standard var is neither
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ape-linux
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: AI for Linux commands
5
5
  Project-URL: Homepage, https://github.com/sbalian/ape
6
6
  Project-URL: Repository, https://github.com/sbalian/ape
@@ -7,6 +7,7 @@ import sys
7
7
  from functools import partial
8
8
  from typing import Any, Protocol, cast
9
9
 
10
+ import pydantic_ai
10
11
  from pydantic import BaseModel
11
12
  from pydantic_ai import Agent, ModelSettings
12
13
  from pydantic_ai.exceptions import ModelHTTPError
@@ -91,6 +92,18 @@ def call_llm(
91
92
  user_prompt: str,
92
93
  model_settings: ModelSettings | None,
93
94
  ) -> Command | CannotHelp:
95
+ # Pydantic AI shows a first-run banner (its logo plus an "observability: off" block)
96
+ # on stderr the first time an agent runs in a terminal. Ape prints a single command
97
+ # meant to be read or piped, so it owns its own output: turn the banner off before
98
+ # any run. This is Pydantic AI's documented switch for exactly that. Pydantic AI
99
+ # declares it as a bare `BANNER_ENABLED = True` with no annotation, so ty infers
100
+ # the narrowest type, `Literal[True]`, and reads any assignment of `False` as a
101
+ # type error; the assignment is precisely what the attribute is documented for, so
102
+ # the diagnostic is suppressed rather than worked around. Assigning it is also a
103
+ # harmless no-op on versions that predate the banner, so the pydantic-ai-slim floor
104
+ # stays where it is.
105
+ pydantic_ai.BANNER_ENABLED = False # ty: ignore[invalid-assignment]
106
+
94
107
  # Two spellings here are for the type checker, and neither changes behavior: the
95
108
  # generic parameters are explicit (`object` is the default deps type, as Ape uses
96
109
  # no deps), and `output_type` uses Pydantic AI's sequence form instead of
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ape-linux"
3
- version = "0.5.2"
3
+ version = "0.5.4"
4
4
  description = "AI for Linux commands"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT License"}
@@ -3,6 +3,7 @@ import os
3
3
  import platform
4
4
  import socket
5
5
 
6
+ import pydantic_ai
6
7
  import pytest
7
8
  from pydantic_ai.exceptions import ModelHTTPError
8
9
  from pydantic_ai.models.test import TestModel
@@ -258,6 +259,20 @@ def test_call_llm_returns_structured_output(monkeypatch):
258
259
  assert isinstance(result.command, str)
259
260
 
260
261
 
262
+ def test_call_llm_disables_the_pydantic_ai_banner(monkeypatch):
263
+ # Pydantic AI shows a first-run banner on stderr, which would land next to ape's
264
+ # one-line output. call_llm turns it off before the agent runs. Setting the flag
265
+ # back to True first makes the check meaningful, and hands monkeypatch the global
266
+ # to restore afterwards; raising=False because versions predating the banner have
267
+ # no such attribute.
268
+ monkeypatch.setattr(pydantic_ai, "BANNER_ENABLED", True, raising=False)
269
+ monkeypatch.setattr(
270
+ "ape_linux.infer_model", lambda model, provider_factory: TestModel()
271
+ )
272
+ ape_linux.call_llm("openai:gpt-4.1", "key", "system", "user", None)
273
+ assert pydantic_ai.BANNER_ENABLED is False
274
+
275
+
261
276
  def test_build_provider_injects_key_without_standard_env_var(monkeypatch):
262
277
  # build_provider constructs the inferred provider with our key injected directly,
263
278
  # so the provider's standard credential variable is neither needed nor read.