refactor: switch to basedpyright, remove pydantic-settings
- Replace pyright with basedpyright in devenv.nix (custom hook) - Add basedpyright to devenv packages - Fix all basedpyright warnings: add DiGraph[str] type args, annotate class attributes, narrow SyncResponse, handle unused call results, suppress unavoidable Any from yaml.safe_load and untyped blackforest - Replace pydantic-settings[yaml] with direct pyyaml dependency - Update CLAUDE.md to reflect basedpyright and dependency changes
This commit is contained in:
parent
f71af1cfaf
commit
7ab25d49cb
11 changed files with 58 additions and 59 deletions
13
CLAUDE.md
13
CLAUDE.md
|
|
@ -18,16 +18,16 @@ uv run pytest # run tests
|
|||
## Code quality
|
||||
|
||||
Pre-commit hooks run automatically on `git commit`:
|
||||
- **pyright** - static type checking (config: `pyrightconfig.json` points to `.devenv/state/venv`)
|
||||
- **basedpyright** - strict static type checking (config: `pyrightconfig.json` points to `.devenv/state/venv`)
|
||||
- **ruff check** - linting with auto-fix
|
||||
- **ruff format** - formatting
|
||||
- **commitizen** - enforces conventional commit messages (`feat:`, `fix:`, `chore:`, etc.)
|
||||
|
||||
Run manually:
|
||||
```bash
|
||||
/nix/store/h7f5vym2ykpl7ls8icw0wiqgmv9xiwnx-pyright-1.1.407/bin/pyright
|
||||
/nix/store/xmy9vff4zlbvkz3y830085dzgjpmaj8d-ruff-0.14.14/bin/ruff check
|
||||
/nix/store/xmy9vff4zlbvkz3y830085dzgjpmaj8d-ruff-0.14.14/bin/ruff format --check
|
||||
basedpyright
|
||||
ruff check
|
||||
ruff format --check
|
||||
```
|
||||
|
||||
## Code style conventions
|
||||
|
|
@ -110,9 +110,10 @@ The provider writes the result file to `project_dir / target_name`.
|
|||
## Dependencies
|
||||
|
||||
- `typer` - CLI framework
|
||||
- `pydantic` / `pydantic-settings[yaml]` - config parsing (pyyaml comes via the yaml extra)
|
||||
- `pydantic` - data validation and config models
|
||||
- `pyyaml` - YAML parsing
|
||||
- `networkx` - dependency graph
|
||||
- `blackforest` - BlackForestLabs API client (sync, uses `requests`)
|
||||
- `blackforest` - BlackForestLabs API client (sync, uses `requests`; no type stubs)
|
||||
- `mistralai` - Mistral API client (supports async)
|
||||
- `httpx` - async HTTP for downloading BFL result images (transitive via mistralai)
|
||||
- `hatchling` - build backend
|
||||
|
|
|
|||
|
|
@ -96,6 +96,6 @@ def graph() -> None:
|
|||
typer.echo(f"Stage {i} (targets): {', '.join(targets_in_gen)}")
|
||||
|
||||
for node in gen:
|
||||
predecessors = list(dep_graph.predecessors(node))
|
||||
if predecessors:
|
||||
typer.echo(f" {node} <- {', '.join(predecessors)}")
|
||||
preds: list[str] = list(dep_graph.predecessors(node))
|
||||
if preds:
|
||||
typer.echo(f" {node} <- {', '.join(preds)}")
|
||||
|
|
|
|||
|
|
@ -78,5 +78,5 @@ def resolve_model(target_name: str, target: TargetConfig, defaults: Defaults) ->
|
|||
def load_config(config_path: Path) -> ProjectConfig:
|
||||
"""Load and validate a ``.bulkgen.yaml`` file."""
|
||||
with config_path.open() as f:
|
||||
raw = yaml.safe_load(f)
|
||||
raw = yaml.safe_load(f) # pyright: ignore[reportAny]
|
||||
return ProjectConfig.model_validate(raw)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import networkx as nx
|
|||
from bulkgen.config import ProjectConfig
|
||||
|
||||
|
||||
def build_graph(config: ProjectConfig, project_dir: Path) -> nx.DiGraph:
|
||||
def build_graph(config: ProjectConfig, project_dir: Path) -> nx.DiGraph[str]:
|
||||
"""Build a dependency DAG from the project configuration.
|
||||
|
||||
Nodes are filenames: target names (keys in ``config.targets``) and
|
||||
|
|
@ -19,7 +19,7 @@ def build_graph(config: ProjectConfig, project_dir: Path) -> nx.DiGraph:
|
|||
Raises :class:`ValueError` if a dependency is neither a defined target
|
||||
nor an existing file, or if the graph contains a cycle.
|
||||
"""
|
||||
graph = nx.DiGraph()
|
||||
graph: nx.DiGraph[str] = nx.DiGraph()
|
||||
target_names = set(config.targets)
|
||||
|
||||
for target_name, target_cfg in config.targets.items():
|
||||
|
|
@ -37,7 +37,7 @@ def build_graph(config: ProjectConfig, project_dir: Path) -> nx.DiGraph:
|
|||
f"which is neither a defined target nor an existing file"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
graph.add_edge(dep, target_name)
|
||||
_ = graph.add_edge(dep, target_name)
|
||||
|
||||
if not nx.is_directed_acyclic_graph(graph):
|
||||
cycles = list(nx.simple_cycles(graph))
|
||||
|
|
@ -47,7 +47,7 @@ def build_graph(config: ProjectConfig, project_dir: Path) -> nx.DiGraph:
|
|||
return graph
|
||||
|
||||
|
||||
def get_build_order(graph: nx.DiGraph) -> list[list[str]]:
|
||||
def get_build_order(graph: nx.DiGraph[str]) -> list[list[str]]:
|
||||
"""Return targets grouped into generations for parallel execution.
|
||||
|
||||
Each inner list contains nodes with no inter-dependencies that can
|
||||
|
|
@ -56,9 +56,9 @@ def get_build_order(graph: nx.DiGraph) -> list[list[str]]:
|
|||
return [list(gen) for gen in nx.topological_generations(graph)]
|
||||
|
||||
|
||||
def get_subgraph_for_target(graph: nx.DiGraph, target: str) -> nx.DiGraph:
|
||||
def get_subgraph_for_target(graph: nx.DiGraph[str], target: str) -> nx.DiGraph[str]:
|
||||
"""Return the subgraph containing *target* and all its transitive dependencies."""
|
||||
ancestors = nx.ancestors(graph, target)
|
||||
ancestors: set[str] = nx.ancestors(graph, target) # pyright: ignore[reportUnknownMemberType]
|
||||
ancestors.add(target)
|
||||
subgraph = nx.DiGraph(graph.subgraph(ancestors))
|
||||
subgraph: nx.DiGraph[str] = nx.DiGraph(graph.subgraph(ancestors))
|
||||
return subgraph
|
||||
|
|
|
|||
|
|
@ -8,8 +8,13 @@ from pathlib import Path
|
|||
from typing import override
|
||||
|
||||
import httpx
|
||||
from blackforest import BFLClient
|
||||
from blackforest.types.general.client_config import ClientConfig
|
||||
from blackforest import BFLClient # pyright: ignore[reportMissingTypeStubs]
|
||||
from blackforest.types.general.client_config import ( # pyright: ignore[reportMissingTypeStubs]
|
||||
ClientConfig,
|
||||
)
|
||||
from blackforest.types.responses.responses import ( # pyright: ignore[reportMissingTypeStubs]
|
||||
SyncResponse,
|
||||
)
|
||||
|
||||
from bulkgen.config import TargetConfig
|
||||
from bulkgen.providers import Provider
|
||||
|
|
@ -25,6 +30,8 @@ def _encode_image_b64(path: Path) -> str:
|
|||
class ImageProvider(Provider):
|
||||
"""Generates images via the BlackForestLabs API."""
|
||||
|
||||
_client: BFLClient
|
||||
|
||||
def __init__(self, api_key: str) -> None:
|
||||
self._client = BFLClient(api_key=api_key)
|
||||
|
||||
|
|
@ -58,13 +65,20 @@ class ImageProvider(Provider):
|
|||
self._client.generate, resolved_model, inputs, _BFL_SYNC_CONFIG
|
||||
)
|
||||
|
||||
image_url: str | None = result.result.get("sample") # type: ignore[union-attr]
|
||||
if not isinstance(result, SyncResponse):
|
||||
msg = (
|
||||
f"BFL API returned unexpected response type for target '{target_name}'"
|
||||
)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
result_dict: dict[str, str] = result.result # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
|
||||
image_url = result_dict.get("sample")
|
||||
if not image_url:
|
||||
msg = f"BFL API did not return an image URL for target '{target_name}'"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
async with httpx.AsyncClient() as http:
|
||||
response = await http.get(image_url)
|
||||
response.raise_for_status()
|
||||
_ = response.raise_for_status()
|
||||
|
||||
output_path.write_bytes(response.content)
|
||||
_ = output_path.write_bytes(response.content)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ from bulkgen.providers import Provider
|
|||
class TextProvider(Provider):
|
||||
"""Generates text via the Mistral API."""
|
||||
|
||||
_api_key: str
|
||||
|
||||
def __init__(self, api_key: str) -> None:
|
||||
self._api_key = api_key
|
||||
|
||||
|
|
@ -50,8 +52,8 @@ class TextProvider(Provider):
|
|||
messages=[models.UserMessage(content=full_prompt)],
|
||||
)
|
||||
|
||||
if response is None or not response.choices:
|
||||
msg = f"Mistral API returned no response for target '{target_name}'"
|
||||
if not response.choices:
|
||||
msg = f"Mistral API returned no choices for target '{target_name}'"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
content = response.choices[0].message.content
|
||||
|
|
@ -60,4 +62,4 @@ class TextProvider(Provider):
|
|||
raise RuntimeError(msg)
|
||||
|
||||
text = content if isinstance(content, str) else str(content)
|
||||
output_path.write_text(text)
|
||||
_ = output_path.write_text(text)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ def load_state(project_dir: Path) -> BuildState:
|
|||
if not state_path.exists():
|
||||
return BuildState()
|
||||
with state_path.open() as f:
|
||||
raw = yaml.safe_load(f)
|
||||
raw = yaml.safe_load(f) # pyright: ignore[reportAny]
|
||||
if raw is None:
|
||||
return BuildState()
|
||||
return BuildState.model_validate(raw)
|
||||
|
|
|
|||
12
devenv.nix
12
devenv.nix
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
|
|
@ -9,8 +10,17 @@
|
|||
};
|
||||
};
|
||||
|
||||
packages = [
|
||||
pkgs.basedpyright
|
||||
];
|
||||
|
||||
git-hooks.hooks = {
|
||||
pyright.enable = true;
|
||||
basedpyright = {
|
||||
enable = true;
|
||||
entry = "${pkgs.basedpyright}/bin/basedpyright";
|
||||
files = "\\.py$";
|
||||
types = [ "file" ];
|
||||
};
|
||||
ruff.enable = true;
|
||||
ruff-format.enable = true;
|
||||
commitizen.enable = true;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ dependencies = [
|
|||
"mistralai>=1.0.0",
|
||||
"networkx>=3.6.1",
|
||||
"pydantic>=2.12.5",
|
||||
"pydantic-settings[yaml]>=2.12.0",
|
||||
"pyyaml>=6.0",
|
||||
"typer>=0.23.1",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"venvPath": ".",
|
||||
"venv": ".devenv/state/venv"
|
||||
"venv": ".devenv/state/venv",
|
||||
}
|
||||
|
|
|
|||
32
uv.lock
generated
32
uv.lock
generated
|
|
@ -55,7 +55,7 @@ dependencies = [
|
|||
{ name = "mistralai" },
|
||||
{ name = "networkx" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings", extra = ["yaml"] },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ requires-dist = [
|
|||
{ name = "mistralai", specifier = ">=1.0.0" },
|
||||
{ name = "networkx", specifier = ">=3.6.1" },
|
||||
{ name = "pydantic", specifier = ">=2.12.5" },
|
||||
{ name = "pydantic-settings", extras = ["yaml"], specifier = ">=2.12.0" },
|
||||
{ name = "pyyaml", specifier = ">=6.0" },
|
||||
{ name = "typer", specifier = ">=0.23.1" },
|
||||
]
|
||||
|
||||
|
|
@ -498,25 +498,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
yaml = [
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
|
|
@ -554,15 +535,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue