diff --git a/pyproject.toml b/pyproject.toml
index 9ec0f3219d..0cdc6cf6bb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -82,9 +82,7 @@ dev = [
"jupyter-book>=2.0.0",
"jupytext>=1.17.1",
"matplotlib>=3.10.0",
- # ty 0.0.70 adds unsound-return-statement under `all = "error"`; adopting it
- # requires a repo-wide typing cleanup.
- "ty>=0.0.32,<0.0.70",
+ "ty>=0.0.32",
"pandas>=2.2.0",
"pre-commit>=4.2.0",
"pytest>=9.0.3",
diff --git a/pyrit/auth/copilot_authenticator.py b/pyrit/auth/copilot_authenticator.py
index d6894b868e..f6de84cd3a 100644
--- a/pyrit/auth/copilot_authenticator.py
+++ b/pyrit/auth/copilot_authenticator.py
@@ -359,7 +359,7 @@ async def _run_playwright_browser_automation_async(self) -> str | None:
"""
from playwright.async_api import async_playwright # type: ignore[ty:unresolved-import]
- bearer_token = None
+ bearer_token: str | None = None
token_expires_in = None
async with async_playwright() as playwright:
@@ -393,7 +393,10 @@ async def response_handler_async(response: Any) -> None:
try:
data = json.loads(text)
if "access_token" in data:
- bearer_token = data["access_token"]
+ token = data["access_token"]
+ if not isinstance(token, str):
+ raise TypeError("OAuth access_token must be a string")
+ bearer_token = token
token_expires_in = data.get("expires_in")
logger.info("Captured bearer token from JSON response.")
diff --git a/pyrit/backend/services/converter_service.py b/pyrit/backend/services/converter_service.py
index 7ec6aec799..3ed38e0400 100644
--- a/pyrit/backend/services/converter_service.py
+++ b/pyrit/backend/services/converter_service.py
@@ -17,7 +17,7 @@
import uuid
from functools import lru_cache
from pathlib import Path
-from typing import Any
+from typing import TYPE_CHECKING, Any
from urllib.parse import parse_qs, urlparse
from pyrit.backend.mappers.converter_mappers import converter_object_to_instance
@@ -37,6 +37,9 @@
from pyrit.models import PromptDataType
from pyrit.registry.components import ConverterRegistry
+if TYPE_CHECKING:
+ from pyrit.converter import ConverterResult
+
class ConverterService:
"""
@@ -336,13 +339,13 @@ async def _apply_converters_async(
Returns:
Tuple of (steps, final_value, final_type).
"""
- current_value = initial_value
- current_type = initial_type
+ current_value: str = initial_value
+ current_type: PromptDataType = initial_type
steps: list[PreviewStep] = []
for conv_id, conv_type, conv_obj in converters:
input_value, input_type = current_value, current_type
- result = await conv_obj.convert_async(prompt=current_value, input_type=current_type)
+ result: ConverterResult = await conv_obj.convert_async(prompt=current_value, input_type=current_type)
current_value, current_type = result.output_text, result.output_type
steps.append(
diff --git a/pyrit/cli/_cli_args.py b/pyrit/cli/_cli_args.py
index 3c7980ad06..af4435a716 100644
--- a/pyrit/cli/_cli_args.py
+++ b/pyrit/cli/_cli_args.py
@@ -285,12 +285,14 @@ def parse_memory_labels(json_string: str) -> dict[str, str]:
if not isinstance(labels, dict):
raise ValueError("Memory labels must be a JSON object (dictionary)")
- # Validate all keys and values are strings
+ # Validate all keys and values are strings and build a precisely typed result
+ validated_labels: dict[str, str] = {}
for key, value in labels.items():
if not isinstance(key, str) or not isinstance(value, str):
raise ValueError(f"All label keys and values must be strings. Got: {key}={value}")
+ validated_labels[key] = value
- return labels
+ return validated_labels
def parse_dataset_filter(arg: str) -> tuple[str, str]:
diff --git a/pyrit/cli/api_client.py b/pyrit/cli/api_client.py
index 47d587aa4a..4f43c57e1c 100644
--- a/pyrit/cli/api_client.py
+++ b/pyrit/cli/api_client.py
@@ -95,7 +95,7 @@ async def health_check_async(self) -> bool:
if resp.status_code != 200:
return False
payload = resp.json()
- return payload.get("status") == "healthy" and payload.get("service") == "pyrit-backend"
+ return bool(payload.get("status") == "healthy" and payload.get("service") == "pyrit-backend")
except httpx.ConnectError:
return False
except Exception:
@@ -405,9 +405,9 @@ def _response_detail(resp: Any) -> str | None:
if isinstance(text, bytes):
text = text.decode(errors="replace")
if isinstance(text, str):
- text = text.strip()
- if text:
- return text
+ stripped_text: str = text.strip()
+ if stripped_text:
+ return stripped_text
return None
@staticmethod
diff --git a/pyrit/cli/pyrit_scan.py b/pyrit/cli/pyrit_scan.py
index 0b8b383149..ee89d0bf7b 100644
--- a/pyrit/cli/pyrit_scan.py
+++ b/pyrit/cli/pyrit_scan.py
@@ -394,7 +394,7 @@ def _discover_verbs() -> frozenset[str]:
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):
return frozenset(action.choices)
- return frozenset()
+ return frozenset[str]()
#: Every valid subcommand verb (used by the legacy-argv shim to detect new-style calls).
@@ -634,12 +634,17 @@ async def _resolve_server_url_async(*, parsed_args: Namespace) -> str | None:
Returns:
str | None: The server base URL, or ``None`` if unreachable.
+
+ Raises:
+ TypeError: If the configured server URL is not a string.
"""
from pyrit.cli._config_reader import DEFAULT_SERVER_URL, read_server_settings
from pyrit.cli._server_launcher import ServerLauncher, parse_local_server_address
server_settings = read_server_settings(config_file=parsed_args.config_file)
base_url = parsed_args.server_url or server_settings.url or DEFAULT_SERVER_URL
+ if not isinstance(base_url, str):
+ raise TypeError(f"Configured server URL must be a string, got {type(base_url).__name__}")
startup_timeout = getattr(parsed_args, "startup_timeout", None) or server_settings.startup_timeout
# Probe existing server
@@ -679,10 +684,16 @@ def _resolve_configured_server_url(*, parsed_args: Namespace) -> str:
Returns:
str: The configured server URL, falling back to the built-in default.
+
+ Raises:
+ TypeError: If the configured server URL is not a string.
"""
from pyrit.cli._config_reader import DEFAULT_SERVER_URL, read_server_url
- return parsed_args.server_url or read_server_url(config_file=parsed_args.config_file) or DEFAULT_SERVER_URL
+ server_url = parsed_args.server_url or read_server_url(config_file=parsed_args.config_file) or DEFAULT_SERVER_URL
+ if not isinstance(server_url, str):
+ raise TypeError(f"Configured server URL must be a string, got {type(server_url).__name__}")
+ return server_url
async def _handle_stop_server_async(*, parsed_args: Namespace) -> int:
@@ -918,7 +929,7 @@ async def _poll_until_terminal_async(
seen_retry_attack_ids: set[str] = set()
while True:
- run = await client.get_scenario_run_async(scenario_result_id=scenario_result_id)
+ run: ScenarioRunSummary = await client.get_scenario_run_async(scenario_result_id=scenario_result_id)
_output.print_scenario_retry_warnings(run=run, seen_attack_ids=seen_retry_attack_ids)
_output.print_scenario_run_progress(run=run, total_techniques=total_techniques)
if run.status in terminal_states:
@@ -1034,9 +1045,17 @@ async def _dispatch_with_client_async(*, client: Any, parsed_args: Namespace) ->
Returns:
int: Exit code from the dispatched command.
+
+ Raises:
+ TypeError: If the dispatched handler returns a non-int exit code.
"""
handler = _CLIENT_HANDLERS[parsed_args.command]
- return await handler(client=client, parsed_args=parsed_args)
+ result = await handler(client=client, parsed_args=parsed_args)
+ if not isinstance(result, int):
+ raise TypeError(
+ f"Handler for '{parsed_args.command}' must return an int exit code, got {type(result).__name__}"
+ )
+ return result
async def _run_async(*, parsed_args: Namespace) -> int:
diff --git a/pyrit/common/yaml_loadable.py b/pyrit/common/yaml_loadable.py
index 2fb4422c73..0c32e169fc 100644
--- a/pyrit/common/yaml_loadable.py
+++ b/pyrit/common/yaml_loadable.py
@@ -44,5 +44,6 @@ def from_yaml_file(cls: type[T], file: Path | str) -> T:
# If this class provides a from_dict factory, use it;
# otherwise, just instantiate directly with **yaml_data
if hasattr(cls, "from_dict") and callable(getattr(cls, "from_dict")): # noqa: B009
- return cls.from_dict(yaml_data) # type: ignore[ty:call-non-callable]
+ result: T = cls.from_dict(yaml_data) # type: ignore[ty:call-non-callable]
+ return result
return cls(**yaml_data)
diff --git a/pyrit/converter/decomposition_converter.py b/pyrit/converter/decomposition_converter.py
index 936e3f8ad6..be8e8f807b 100644
--- a/pyrit/converter/decomposition_converter.py
+++ b/pyrit/converter/decomposition_converter.py
@@ -73,7 +73,8 @@ def _tokens(text: str) -> list[str]:
Returns:
list[str]: The lowercase word tokens.
"""
- return re.findall(r"\w+", text.lower(), flags=re.UNICODE)
+ tokens: list[str] = re.findall(r"\w+", text.lower(), flags=re.UNICODE)
+ return tokens
def _token_recall(source: list[str], got: list[str]) -> float:
@@ -291,6 +292,7 @@ def _parse_and_validate(self, *, objective: str, raw: str) -> tuple[list[str], l
if not isinstance(words, list) or not isinstance(types, list) or not words or len(words) != len(types):
raise InvalidJsonException(message="response must contain equal-length non-empty 'words' and 'types' lists")
words = [str(w) for w in words]
+ types = [str(t) for t in types]
if any(not w.strip() for w in words):
raise InvalidJsonException(message="every phrase must be non-empty")
if any(t not in _VALID_TAGS for t in types):
diff --git a/pyrit/datasets/seed_datasets/local/local_dataset_loader.py b/pyrit/datasets/seed_datasets/local/local_dataset_loader.py
index 92f85ccd12..fc26646c05 100644
--- a/pyrit/datasets/seed_datasets/local/local_dataset_loader.py
+++ b/pyrit/datasets/seed_datasets/local/local_dataset_loader.py
@@ -42,7 +42,7 @@ def __init__(self, *, file_path: Path) -> None:
try:
dataset = SeedDataset.from_yaml_file(file_path)
# Use the dataset_name from the YAML if available, otherwise use filename
- self._dataset_name = (
+ self._dataset_name: str = (
getattr(dataset, "dataset_name", None) or getattr(dataset, "name", None) or file_path.stem
)
except Exception as e:
diff --git a/pyrit/datasets/seed_datasets/remote/vlguard_dataset.py b/pyrit/datasets/seed_datasets/remote/vlguard_dataset.py
index 3d8671f7b0..316b67f8db 100644
--- a/pyrit/datasets/seed_datasets/remote/vlguard_dataset.py
+++ b/pyrit/datasets/seed_datasets/remote/vlguard_dataset.py
@@ -326,7 +326,7 @@ async def _download_dataset_files_async(self, *, cache: bool = True) -> tuple[li
if cache and json_path.exists() and image_dir.exists() and any(image_dir.iterdir()):
logger.info("Using cached VLGuard dataset")
with open(json_path, encoding="utf-8") as f:
- metadata = json.load(f)
+ metadata: list[dict[str, str]] = json.load(f)
return metadata, image_dir
logger.info("Downloading VLGuard dataset from HuggingFace...")
@@ -357,6 +357,6 @@ def _download_sync() -> tuple[str, str]:
await asyncio.to_thread(safe_extract_zip, source=zip_path, dest_dir=cache_dir)
with open(json_path, encoding="utf-8") as f:
- metadata = json.load(f)
+ metadata: list[dict[str, str]] = json.load(f)
return metadata, image_dir
diff --git a/pyrit/executor/attack/multi_turn/tree_of_attacks.py b/pyrit/executor/attack/multi_turn/tree_of_attacks.py
index 8e2f12aeb5..a7eaf72dbd 100644
--- a/pyrit/executor/attack/multi_turn/tree_of_attacks.py
+++ b/pyrit/executor/attack/multi_turn/tree_of_attacks.py
@@ -154,8 +154,14 @@ def threshold(self) -> float:
Returns:
float: The threshold value from the FloatScaleThresholdScorer.
+
+ Raises:
+ TypeError: If the configured objective scorer has an unexpected type.
"""
- return self.objective_scorer.threshold # type: ignore[ty:unresolved-attribute]
+ objective_scorer = self.objective_scorer
+ if not isinstance(objective_scorer, FloatScaleThresholdScorer):
+ raise TypeError("TAP objective scorer must be a FloatScaleThresholdScorer")
+ return objective_scorer.threshold
@dataclass(frozen=True, slots=True)
@@ -233,7 +239,8 @@ class TAPAttackResult(AttackResult):
@property
def tree_visualization(self) -> Tree | None:
"""The tree visualization from metadata."""
- return self.metadata.get("tree_visualization", None)
+ tree: Tree | None = self.metadata.get("tree_visualization")
+ return tree
@tree_visualization.setter
def tree_visualization(self, value: Tree) -> None:
diff --git a/pyrit/executor/promptgen/gcg/attack/base/attack_manager.py b/pyrit/executor/promptgen/gcg/attack/base/attack_manager.py
index 1d9cc7a69f..6a00c8b3c6 100644
--- a/pyrit/executor/promptgen/gcg/attack/base/attack_manager.py
+++ b/pyrit/executor/promptgen/gcg/attack/base/attack_manager.py
@@ -307,14 +307,14 @@ def _update_ids(self) -> None:
# token. Both are necessary for the slice arithmetic to remain valid
# across tokenizers/templates.
def end_tok(char_pos: int) -> int:
- tok = encoding.char_to_token(char_pos)
+ tok: int | None = encoding.char_to_token(char_pos)
return len(toks) if tok is None else tok
def start_tok(char_pos: int) -> int:
limit = len(prompt)
cur = char_pos
while cur < limit:
- tok = encoding.char_to_token(cur)
+ tok: int | None = encoding.char_to_token(cur)
if tok is not None:
return tok
cur += 1
@@ -355,11 +355,11 @@ def generate(self, model: Any, gen_config: Any = None) -> torch.Tensor:
logger.warning("max_new_tokens > 32 may cause testing to slow down.")
input_ids = self.input_ids[: self._assistant_role_slice.stop].to(model.device).unsqueeze(0)
attn_masks = torch.ones_like(input_ids).to(model.device)
- output_ids = model.generate(
+ output_ids: torch.Tensor = model.generate(
input_ids, attention_mask=attn_masks, generation_config=gen_config, pad_token_id=self.tokenizer.pad_token_id
)[0]
- return output_ids[self._assistant_role_slice.stop :] # type: ignore[no-any-return, unused-ignore]
+ return output_ids[self._assistant_role_slice.stop :]
def generate_str(self, model: Any, gen_config: Any = None) -> Any:
"""
@@ -564,11 +564,8 @@ def input_str(self) -> Any:
@property
def eval_str(self) -> str:
"""The decoded input used for evaluation."""
- return ( # type: ignore[no-any-return, unused-ignore]
- self.tokenizer.decode(self.input_ids[: self._assistant_role_slice.stop])
- .replace("", "")
- .replace("", "")
- )
+ decoded: str = self.tokenizer.decode(self.input_ids[: self._assistant_role_slice.stop])
+ return decoded.replace("", "").replace("", "")
class PromptManager:
@@ -754,7 +751,8 @@ def control_toks(self, input_control_toks: torch.Tensor) -> None:
@property
def control_str(self) -> str:
"""The shared decoded control suffix."""
- return self._prompts[0].control_str # type: ignore[no-any-return, unused-ignore]
+ control: str = self._prompts[0].control_str
+ return control
@control_str.setter
def control_str(self, control: str) -> None:
@@ -837,9 +835,10 @@ def __init__(
self.managers = managers
@property
- def control_str(self) -> Any:
+ def control_str(self) -> str:
"""The shared decoded control suffix."""
- return self.prompts[0].control_str
+ control: str = self.prompts[0].control_str
+ return control
@control_str.setter
def control_str(self, control: str) -> None:
@@ -871,7 +870,8 @@ def get_filtered_cands(
Returns:
list[str]: Decoded candidate controls.
"""
- cands, count = [], 0
+ cands: list[str] = []
+ count = 0
worker = self.workers[worker_index]
logger.info("Masking out of range token_id.")
@@ -1019,8 +1019,8 @@ def test(
for j, worker in enumerate(workers):
worker(prompts[j], ModelWorkerOperation.TEST)
model_tests = np.array([worker.results.get() for worker in workers])
- model_tests_jb = model_tests[..., 0].tolist()
- model_tests_mb = model_tests[..., 1].tolist()
+ model_tests_jb: list[list[bool]] = model_tests[..., 0].tolist()
+ model_tests_mb: list[list[int]] = model_tests[..., 1].tolist()
model_tests_loss: list[list[float]] = []
if include_loss:
for j, worker in enumerate(workers):
@@ -1334,7 +1334,7 @@ def run(
)
if num_goals == len(self.goals) and num_workers == len(self.workers):
stop_inner_on_success = False
- control, loss, inner_steps = attack.run(
+ inner_result: tuple[str, float, int] = attack.run(
n_steps=n_steps - step,
batch_size=batch_size,
topk=topk,
@@ -1350,6 +1350,7 @@ def run(
filter_cand=filter_cand,
verbose=verbose,
)
+ control, loss, inner_steps = inner_result
step += inner_steps
self.control = control
@@ -1683,8 +1684,12 @@ def run(
_update_attack_log_params(logfile=self.logfile, params={"num_tests": len(controls)})
- total_jb, total_em, total_outputs = [], [], []
- test_total_jb, test_total_em, test_total_outputs = [], [], []
+ total_jb: list[list[bool]] = []
+ total_em: list[list[bool]] = []
+ total_outputs: list[list[str]] = []
+ test_total_jb: list[list[bool]] = []
+ test_total_em: list[list[bool]] = []
+ test_total_outputs: list[list[str]] = []
curr_jb: list[bool] = []
curr_em: list[bool] = []
all_outputs: list[str] = []
@@ -1959,6 +1964,24 @@ def get_workers(params: Any, evaluation: bool = False) -> tuple[list[ModelWorker
return workers[:num_train_models], workers[num_train_models:]
+def _read_string_column(*, data: Any, column: str, start: int = 0, end: int | None = None) -> list[str]:
+ """
+ Read a slice of a dataframe column as strings.
+
+ Returns:
+ list[str]: The requested column values.
+
+ Raises:
+ ValueError: If the requested column contains a non-string value.
+ """
+ values: list[str] = []
+ for value in data[column].tolist()[start:end]:
+ if not isinstance(value, str):
+ raise ValueError(f"Column '{column}' must contain only strings, got {type(value).__name__}")
+ values.append(value)
+ return values
+
+
def get_goals_and_targets(params: Any) -> tuple[list[str], list[str], list[str], list[str]]:
"""
Load training and held-out goals and targets from parameters or CSV files.
@@ -1970,10 +1993,10 @@ def get_goals_and_targets(params: Any) -> tuple[list[str], list[str], list[str],
Raises:
ValueError: If a goal list and its corresponding target list differ in length.
"""
- train_goals = getattr(params, "goals", [])
- train_targets = getattr(params, "targets", [])
- test_goals = getattr(params, "test_goals", [])
- test_targets = getattr(params, "test_targets", [])
+ train_goals: list[str] = getattr(params, "goals", [])
+ train_targets: list[str] = getattr(params, "targets", [])
+ test_goals: list[str] = getattr(params, "test_goals", [])
+ test_targets: list[str] = getattr(params, "test_targets", [])
if params.train_data:
train_data = pd.read_csv(params.train_data)
@@ -1981,22 +2004,32 @@ def get_goals_and_targets(params: Any) -> tuple[list[str], list[str], list[str],
# this line shuffles the rows of train data randomly with a random seed
train_data = train_data.sample(frac=1, random_state=params.random_seed).reset_index(drop=True)
- train_targets = train_data["target"].tolist()[: params.n_train_data]
+ train_targets = _read_string_column(data=train_data, column="target", end=params.n_train_data)
if "goal" in train_data.columns:
- train_goals = train_data["goal"].tolist()[: params.n_train_data]
+ train_goals = _read_string_column(data=train_data, column="goal", end=params.n_train_data)
else:
train_goals = [""] * len(train_targets)
if params.test_data and params.n_test_data > 0:
test_data = pd.read_csv(params.test_data)
- test_targets = test_data["target"].tolist()[: params.n_test_data]
+ test_targets = _read_string_column(data=test_data, column="target", end=params.n_test_data)
if "goal" in test_data.columns:
- test_goals = test_data["goal"].tolist()[: params.n_test_data]
+ test_goals = _read_string_column(data=test_data, column="goal", end=params.n_test_data)
else:
test_goals = [""] * len(test_targets)
elif params.n_test_data > 0:
- test_targets = train_data["target"].tolist()[params.n_train_data : params.n_train_data + params.n_test_data]
+ test_targets = _read_string_column(
+ data=train_data,
+ column="target",
+ start=params.n_train_data,
+ end=params.n_train_data + params.n_test_data,
+ )
if "goal" in train_data.columns:
- test_goals = train_data["goal"].tolist()[params.n_train_data : params.n_train_data + params.n_test_data]
+ test_goals = _read_string_column(
+ data=train_data,
+ column="goal",
+ start=params.n_train_data,
+ end=params.n_train_data + params.n_test_data,
+ )
else:
test_goals = [""] * len(test_targets)
diff --git a/pyrit/executor/promptgen/gcg/attack/gcg/gcg_attack.py b/pyrit/executor/promptgen/gcg/attack/gcg/gcg_attack.py
index 916260e267..d9287b69cd 100644
--- a/pyrit/executor/promptgen/gcg/attack/gcg/gcg_attack.py
+++ b/pyrit/executor/promptgen/gcg/attack/gcg/gcg_attack.py
@@ -170,7 +170,7 @@ def __init__(
self._candidate_filter = candidate_filter
def _resolve_sampling(self) -> SamplingStrategy:
- sampling = getattr(self, "_sampling", None)
+ sampling: SamplingStrategy | None = getattr(self, "_sampling", None)
if sampling is not None:
return sampling
return StandardGCGSampling()
diff --git a/pyrit/executor/promptgen/gcg/default_implementations.py b/pyrit/executor/promptgen/gcg/default_implementations.py
index ee70a912df..ae22eb85f9 100644
--- a/pyrit/executor/promptgen/gcg/default_implementations.py
+++ b/pyrit/executor/promptgen/gcg/default_implementations.py
@@ -208,7 +208,8 @@ def compute_loss(
"CrossEntropyLoss.compute_loss produced no terms; "
"this indicates a corrupted instance with both weights at 0."
)
- return total
+ result: torch.Tensor = total
+ return result
class LengthPreservingFilter:
diff --git a/pyrit/memory/azure_sql_memory.py b/pyrit/memory/azure_sql_memory.py
index 102e8d3c2b..551d111c32 100644
--- a/pyrit/memory/azure_sql_memory.py
+++ b/pyrit/memory/azure_sql_memory.py
@@ -147,7 +147,12 @@ def _resolve_sas_token(env_var_name: str, passed_value: str | None = None) -> st
str | None: Resolved SAS token or None if not provided.
"""
try:
- return default_values.get_required_value(env_var_name=env_var_name, passed_value=passed_value)
+ value = default_values.get_required_value(env_var_name=env_var_name, passed_value=passed_value)
+ # get_required_value() is typed to return Any because it can also preserve callables
+ # (e.g. token providers). This call site only ever passes a str | None, so the
+ # runtime value is guaranteed to be a str.
+ resolved_value: str = value
+ return resolved_value
except ValueError:
return None
diff --git a/pyrit/memory/memory_interface.py b/pyrit/memory/memory_interface.py
index 4bbef89456..9a76171b72 100644
--- a/pyrit/memory/memory_interface.py
+++ b/pyrit/memory/memory_interface.py
@@ -854,7 +854,8 @@ def _get_identifiers(
filters: dict[str, Any],
) -> Sequence[IdentifierModel]:
if identifier_hashes is not None and not identifier_hashes:
- return []
+ empty_identifiers: list[IdentifierModel] = []
+ return empty_identifiers
list_filters = {name: value for name, value in filters.items() if isinstance(value, list)}
conditions = [
@@ -1405,11 +1406,15 @@ def _query_with_list_params(
batch_size=effective_batch_size,
)
+ if not other_large_params:
+ return results
+
+ filtered_results: list[Model] = list(results)
for _col, vals, attr_name in other_large_params:
vals_set = set(vals)
- results = [e for e in results if getattr(e, attr_name, None) in vals_set]
+ filtered_results = [e for e in filtered_results if getattr(e, attr_name, None) in vals_set]
- return results
+ return filtered_results
def _insert_entry(self, entry: Base) -> None:
"""
@@ -1666,7 +1671,8 @@ def get_scores(
Sequence[Score]: A list of Score objects that match the specified filters.
"""
if score_ids is not None and len(score_ids) == 0:
- return []
+ empty_scores: list[Score] = []
+ return empty_scores
conditions: list[Any] = []
@@ -1699,7 +1705,8 @@ def get_scores(
# No score_ids specified - use regular query
if not conditions:
- return []
+ no_condition_scores: list[Score] = []
+ return no_condition_scores
score_entries: Sequence[ScoreEntry] = self._query_entries(ScoreEntry, conditions=and_(*conditions))
return [entry.get_score() for entry in score_entries]
@@ -1762,7 +1769,8 @@ def get_prompt_scores(
# with their originals.
original_ids = {piece.original_prompt_id for piece in message_pieces if piece.original_prompt_id is not None}
if not original_ids:
- return []
+ no_original_ids_scores: list[Score] = []
+ return no_original_ids_scores
score_entries = self._execute_batched_query(
ScoreEntry,
@@ -1929,7 +1937,8 @@ def get_message_pieces(
an exception is logged and an empty list is returned.
"""
if prompt_ids is not None and len(prompt_ids) == 0:
- return []
+ empty_message_pieces: list[MessagePiece] = []
+ return empty_message_pieces
try:
conditions: list[Any] = []
@@ -2464,7 +2473,7 @@ def get_seed_dataset_names(self) -> Sequence[str]:
distinct=True,
)
# Extract unique dataset names from the entries
- dataset_names = set()
+ dataset_names: set[str] = set()
for entry in entries:
if entry.dataset_name:
dataset_names.add(entry.dataset_name)
@@ -2895,7 +2904,8 @@ def _query_attack_results(self, *, query: _AttackResultQuery) -> Sequence[Attack
with an ID-batched lookup.
"""
if self._attack_result_query_has_empty_lookup(query=query):
- return []
+ empty_attack_results: list[AttackResult] = []
+ return empty_attack_results
conditions = self._build_attack_result_conditions(query=query)
paginating = query.limit is not None or query.after is not None
@@ -3400,7 +3410,8 @@ def get_scenario_results(
ordered by completion_time descending.
"""
if scenario_result_ids is not None and len(scenario_result_ids) == 0:
- return []
+ empty_scenario_results: list[ScenarioResult] = []
+ return empty_scenario_results
conditions = self._build_scenario_result_query_conditions(
scenario_name=scenario_name,
diff --git a/pyrit/memory/memory_models.py b/pyrit/memory/memory_models.py
index 653abf8b48..2cc9ef0dc7 100644
--- a/pyrit/memory/memory_models.py
+++ b/pyrit/memory/memory_models.py
@@ -1396,7 +1396,8 @@ def _pack_seed_metadata(entry: Seed) -> dict[str, str | int] | None:
schema = getattr(entry, "response_json_schema", None)
if not raw and schema is None:
- return raw
+ no_schema_metadata: dict[str, str | int] | None = None if raw is None else {}
+ return no_schema_metadata
packed: dict[str, str | int] = dict(raw) if raw else {}
# Defensive strip — the reserved key is owned by this class.
diff --git a/pyrit/models/identifiers/component_identifier.py b/pyrit/models/identifiers/component_identifier.py
index 45753b771f..ae8dce3bce 100644
--- a/pyrit/models/identifiers/component_identifier.py
+++ b/pyrit/models/identifiers/component_identifier.py
@@ -323,7 +323,8 @@ def _promoted_fields(cls) -> tuple[str, ...]:
``ComponentIdentifier``, in field-definition order.
"""
base_fields = set(ComponentIdentifier.model_fields)
- return tuple(name for name in cls.model_fields if name not in base_fields)
+ promoted_fields: tuple[str, ...] = tuple(name for name in cls.model_fields if name not in base_fields)
+ return promoted_fields
@classmethod
def _promoted_param_fields(cls) -> tuple[str, ...]:
diff --git a/pyrit/models/parameter.py b/pyrit/models/parameter.py
index e7e692d178..ae2da9cd6b 100644
--- a/pyrit/models/parameter.py
+++ b/pyrit/models/parameter.py
@@ -535,7 +535,8 @@ def _render_type_name(param_type: Any) -> str:
param_type = _unwrap_optional(param_type)
if get_origin(param_type) is Literal:
args = get_args(param_type)
- return type(args[0]).__name__ if args else "str"
+ literal_type_name: str = type(args[0]).__name__ if args else "str"
+ return literal_type_name
if get_origin(param_type) is list:
type_args = get_args(param_type)
element_type = _unwrap_optional(type_args[0]) if type_args else str
diff --git a/pyrit/prompt_normalizer/json_retry.py b/pyrit/prompt_normalizer/json_retry.py
index e63fd662e4..2f1d71dce4 100644
--- a/pyrit/prompt_normalizer/json_retry.py
+++ b/pyrit/prompt_normalizer/json_retry.py
@@ -75,4 +75,5 @@ async def _attempt_async() -> T:
raise ValueError(f"No response received for conversation ID: {conversation_id}")
return parse(response)
- return await _attempt_async()
+ result: T = await _attempt_async()
+ return result
diff --git a/pyrit/prompt_normalizer/prompt_normalizer.py b/pyrit/prompt_normalizer/prompt_normalizer.py
index cf7b458f94..412c71fc71 100644
--- a/pyrit/prompt_normalizer/prompt_normalizer.py
+++ b/pyrit/prompt_normalizer/prompt_normalizer.py
@@ -216,7 +216,7 @@ async def send_prompt_batch_to_target_async(
"conversation_id",
]
- return await batch_task_async(
+ results: list[Message] = await batch_task_async(
prompt_target=target,
batch_size=batch_size,
items_to_batch=batch_items,
@@ -224,6 +224,7 @@ async def send_prompt_batch_to_target_async(
task_arguments=batch_item_keys,
target=target,
)
+ return results
async def convert_values_async(
self,
diff --git a/pyrit/prompt_target/common/chat_completions_response_parser.py b/pyrit/prompt_target/common/chat_completions_response_parser.py
index b1a28a0f18..a65acc5db8 100644
--- a/pyrit/prompt_target/common/chat_completions_response_parser.py
+++ b/pyrit/prompt_target/common/chat_completions_response_parser.py
@@ -413,7 +413,7 @@ def is_content_filter_response(response: Any) -> bool:
bool: True if ``finish_reason == "content_filter"``, False otherwise.
"""
try:
- return bool(response.choices) and response.choices[0].finish_reason == "content_filter"
+ return bool(response.choices) and bool(response.choices[0].finish_reason == "content_filter")
except (AttributeError, IndexError):
return False
@@ -429,9 +429,9 @@ def extract_partial_content(response: Any) -> str | None:
str | None: The partial text, or None if none was generated.
"""
try:
- choice = response.choices[0]
- if choice.message and choice.message.content:
- return choice.message.content
+ content = response.choices[0].message.content
+ if isinstance(content, str) and content:
+ return content
except (AttributeError, IndexError):
pass
return None
diff --git a/pyrit/prompt_target/common/discover_target_capabilities.py b/pyrit/prompt_target/common/discover_target_capabilities.py
index 882db920b8..ff9ad42f98 100644
--- a/pyrit/prompt_target/common/discover_target_capabilities.py
+++ b/pyrit/prompt_target/common/discover_target_capabilities.py
@@ -285,7 +285,8 @@ async def _send_and_check_async(
def _retry_backoff_seconds(*, attempt: int) -> float:
"""Return the exponential backoff delay for a retry attempt."""
- return min(DEFAULT_PROBE_RETRY_BACKOFF_SECONDS * (2**attempt), MAX_PROBE_RETRY_BACKOFF_SECONDS)
+ backoff_seconds: float = DEFAULT_PROBE_RETRY_BACKOFF_SECONDS * (2**attempt)
+ return min(backoff_seconds, MAX_PROBE_RETRY_BACKOFF_SECONDS)
async def _sleep_before_retry_async(*, attempt: int) -> None:
diff --git a/pyrit/prompt_target/openai/_response_adapter.py b/pyrit/prompt_target/openai/_response_adapter.py
index 8b8d629fc8..aa3d914011 100644
--- a/pyrit/prompt_target/openai/_response_adapter.py
+++ b/pyrit/prompt_target/openai/_response_adapter.py
@@ -119,7 +119,7 @@ def is_content_filtered(self, *, response: Response) -> bool:
if getattr(response, "status", None) != "incomplete":
return False
incomplete_details = getattr(response, "incomplete_details", None)
- return incomplete_details is not None and incomplete_details.reason == "content_filter"
+ return incomplete_details is not None and bool(incomplete_details.reason == "content_filter")
def extract_partial_content(self, *, response: Response) -> str | None:
"""
@@ -132,13 +132,17 @@ def extract_partial_content(self, *, response: Response) -> str | None:
str | None: Partial text, if present.
"""
try:
- parts = [
- content_item.text
- for section in response.output or []
- if getattr(section, "type", None) == "message" and getattr(section, "status", None) == "completed"
- for content_item in getattr(section, "content", None) or []
- if isinstance(content_item, ResponseOutputText) and content_item.text
- ]
+ parts: list[str] = []
+ for section in response.output or []:
+ if getattr(section, "type", None) != "message" or getattr(section, "status", None) != "completed":
+ continue
+ parts.extend(
+ content_item.text
+ for content_item in getattr(section, "content", None) or []
+ if isinstance(content_item, ResponseOutputText)
+ and isinstance(content_item.text, str)
+ and content_item.text
+ )
except (AttributeError, IndexError, TypeError):
return None
return "\n".join(parts) if parts else None
diff --git a/pyrit/prompt_target/openai/openai_error_handling.py b/pyrit/prompt_target/openai/openai_error_handling.py
index 5c67f2402f..851cb7937e 100644
--- a/pyrit/prompt_target/openai/openai_error_handling.py
+++ b/pyrit/prompt_target/openai/openai_error_handling.py
@@ -136,7 +136,8 @@ def _extract_error_payload(exc: Exception) -> tuple[dict[str, object] | str, boo
data = resp.json()
# Validate that we got actual data, not a mock
if isinstance(data, dict):
- return data, _is_content_filter_error(data)
+ json_payload: dict[str, object] = data
+ return json_payload, _is_content_filter_error(json_payload)
except Exception:
pass
# Try text fallback from response
@@ -151,13 +152,17 @@ def _extract_error_payload(exc: Exception) -> tuple[dict[str, object] | str, boo
body = getattr(exc, "body", None)
if body is not None:
if isinstance(body, dict):
- return body, _is_content_filter_error(body)
+ body_payload: dict[str, object] = body
+ return body_payload, _is_content_filter_error(body_payload)
if isinstance(body, str):
try:
data = json.loads(body)
- return data, _is_content_filter_error(data)
except json.JSONDecodeError:
return body, _is_content_filter_error(body)
+ if isinstance(data, dict):
+ parsed_payload: dict[str, object] = data
+ return parsed_payload, _is_content_filter_error(parsed_payload)
+ return body, _is_content_filter_error(body)
# Strategy 3: Fall back to str(e)
text = str(exc)
diff --git a/pyrit/prompt_target/openai/openai_target.py b/pyrit/prompt_target/openai/openai_target.py
index dc3f57e4b0..4e9f6391b2 100644
--- a/pyrit/prompt_target/openai/openai_target.py
+++ b/pyrit/prompt_target/openai/openai_target.py
@@ -180,8 +180,9 @@ def _extract_deployment_from_azure_url(self, url: str) -> str:
match = re.search(r"/deployments/([^/]+)/", url)
if match:
deployment = match.group(1)
- logger.info(f"Extracted deployment name from URL: {deployment}")
- return deployment
+ if isinstance(deployment, str):
+ logger.info(f"Extracted deployment name from URL: {deployment}")
+ return deployment
return ""
diff --git a/pyrit/prompt_target/playwright_copilot_target.py b/pyrit/prompt_target/playwright_copilot_target.py
index fe5d0e3e62..a662e7f1a1 100644
--- a/pyrit/prompt_target/playwright_copilot_target.py
+++ b/pyrit/prompt_target/playwright_copilot_target.py
@@ -377,7 +377,7 @@ async def _extract_text_from_message_groups_async(
Returns:
List of extracted text strings (may include placeholders)
"""
- all_text_parts = []
+ all_text_parts: list[str] = []
for group_idx, msg_group in enumerate(ai_message_groups):
text_elements = await msg_group.query_selector_all(text_selector)
@@ -385,7 +385,7 @@ async def _extract_text_from_message_groups_async(
for text_elem in text_elements:
text = await text_elem.text_content()
- if text:
+ if isinstance(text, str) and text:
all_text_parts.append(text.strip())
return all_text_parts
@@ -712,10 +712,10 @@ async def _extract_fallback_text_async(self, *, ai_message_groups: list[Any]) ->
Returns:
Combined text content from all groups
"""
- fallback_parts = []
+ fallback_parts: list[str] = []
for msg_group in ai_message_groups:
fallback_text = await msg_group.text_content()
- if fallback_text:
+ if isinstance(fallback_text, str) and fallback_text:
fallback_parts.append(fallback_text.strip())
fallback_result = "\n".join(fallback_parts).strip()
logger.debug(f"Using fallback text: '{fallback_result}'")
diff --git a/pyrit/registry/components/target_registry.py b/pyrit/registry/components/target_registry.py
index eff8338121..4cd941f5f6 100644
--- a/pyrit/registry/components/target_registry.py
+++ b/pyrit/registry/components/target_registry.py
@@ -53,7 +53,8 @@ class TargetMetadata(RegistryMetadata):
@property
def supported_auth_modes(self) -> tuple[str, ...]:
"""Auth modes this target type accepts (e.g. ``"api_key"``, ``"identity"``)."""
- return tuple(self.class_attributes.get("supported_auth_modes") or ())
+ auth_modes: tuple[str, ...] = tuple(self.class_attributes.get("supported_auth_modes") or ())
+ return auth_modes
class TargetRegistry(Registry["PromptTarget", TargetMetadata]):
diff --git a/pyrit/scenario/core/atomic_attack.py b/pyrit/scenario/core/atomic_attack.py
index 427b14384e..0ad434542a 100644
--- a/pyrit/scenario/core/atomic_attack.py
+++ b/pyrit/scenario/core/atomic_attack.py
@@ -19,13 +19,12 @@
from typing import TYPE_CHECKING, Any
from pyrit.common.utils import to_sha256
-from pyrit.executor.attack import AttackExecutor
+from pyrit.executor.attack import AttackExecutor, AttackExecutorResult
from pyrit.executor.attack.core.attack_result_attribution import AttackResultAttribution
from pyrit.memory import CentralMemory
from pyrit.models import AtomicAttackEvaluationIdentifier, AtomicAttackIdentifier, AttackResult, AttackSeedGroup
if TYPE_CHECKING:
- from pyrit.executor.attack.core.attack_executor import AttackExecutorResult
from pyrit.prompt_target import PromptTarget
from pyrit.scenario.core.attack_technique import AttackTechnique
from pyrit.score import TrueFalseScorer
@@ -333,7 +332,7 @@ async def run_async(
parent_eval_hash=self.technique_eval_hash,
)
- results = await executor.execute_attack_from_seed_groups_async(
+ untyped_results = await executor.execute_attack_from_seed_groups_async(
attack=technique.attack,
seed_groups=execution_seed_groups,
adversarial_chat=self._adversarial_chat,
@@ -343,6 +342,16 @@ async def run_async(
attribution=attribution,
**self._attack_execute_params,
)
+ completed_results: list[AttackResult] = []
+ for result in untyped_results.completed_results:
+ if not isinstance(result, AttackResult):
+ raise ValueError(f"Attack returned unsupported result type: {type(result).__name__}")
+ completed_results.append(result)
+ results = AttackExecutorResult[AttackResult](
+ completed_results=completed_results,
+ incomplete_objectives=untyped_results.incomplete_objectives,
+ input_indices=untyped_results.input_indices,
+ )
# Enrich atomic_attack_identifier with seed identifiers
self._enrich_atomic_attack_identifiers(results=results)
diff --git a/pyrit/scenario/core/attack_technique_factory.py b/pyrit/scenario/core/attack_technique_factory.py
index bf415e4e22..990957386c 100644
--- a/pyrit/scenario/core/attack_technique_factory.py
+++ b/pyrit/scenario/core/attack_technique_factory.py
@@ -749,6 +749,8 @@ def _get_scoring_config_type(self) -> type | None:
if inner is None or inner is AttackScoringConfig:
# Base type or unresolvable — any config is accepted
return None
+ if not issubclass(inner, AttackScoringConfig):
+ return None
return inner
@staticmethod
@@ -764,13 +766,15 @@ def _unwrap_optional(annotation: Any) -> type | None:
if origin is Union or (hasattr(annotation, "__args__") and origin is None and hasattr(annotation, "__or__")):
args = typing.get_args(annotation)
non_none = [a for a in args if a is not type(None)]
- return non_none[0] if len(non_none) == 1 else None
+ candidate = non_none[0] if len(non_none) == 1 else None
+ return candidate if isinstance(candidate, type) else None
# types.UnionType from PEP 604 at runtime (3.10+)
if hasattr(annotation, "__args__") and type(annotation).__name__ == "UnionType":
args = annotation.__args__
non_none = [a for a in args if a is not type(None)]
- return non_none[0] if len(non_none) == 1 else None
+ candidate = non_none[0] if len(non_none) == 1 else None
+ return candidate if isinstance(candidate, type) else None
# Plain type (not Optional)
if isinstance(annotation, type):
diff --git a/pyrit/scenario/core/scenario.py b/pyrit/scenario/core/scenario.py
index 5ca3c2247c..9385c8c97b 100644
--- a/pyrit/scenario/core/scenario.py
+++ b/pyrit/scenario/core/scenario.py
@@ -145,7 +145,7 @@ def _get_additional_scoring_questions(cls) -> Sequence[Path]:
Returns:
Sequence[Path]: Paths to true/false question prompts, or an empty sequence to use the default scorer.
"""
- return []
+ return ()
def __init__(
self,
@@ -484,20 +484,28 @@ def _resolve_objective_target(self, *, value: Any) -> PromptTarget | None:
nor available as a global default.
Raises:
+ TypeError: If a configured default or resolved target is not a ``PromptTarget``.
ValueError: If a target name is supplied that is not registered in ``TargetRegistry``.
"""
if value is None:
found, default = get_global_default_values().get_default_value(
class_type=type(self), parameter_name="objective_target"
)
- return default if found else None
+ if not found or default is None:
+ return None
+ if not isinstance(default, PromptTarget):
+ raise TypeError(f"Default objective_target must be a PromptTarget, got {type(default).__name__}")
+ return default
- return resolve_reference_value(
+ resolved = resolve_reference_value(
component_type=ComponentType.TARGET,
value=value,
owner=type(self).__name__,
name="objective_target",
)
+ if not isinstance(resolved, PromptTarget):
+ raise TypeError(f"Resolved objective_target must be a PromptTarget, got {type(resolved).__name__}")
+ return resolved
def _resolve_scenario_techniques(self, *, scenario_techniques: Any) -> list[ScenarioTechnique]:
"""
diff --git a/pyrit/scenario/scenarios/adaptive/adaptive_scenario.py b/pyrit/scenario/scenarios/adaptive/adaptive_scenario.py
index d1289df6cf..fec03778d2 100644
--- a/pyrit/scenario/scenarios/adaptive/adaptive_scenario.py
+++ b/pyrit/scenario/scenarios/adaptive/adaptive_scenario.py
@@ -309,14 +309,20 @@ def _build_scoring_config_for_factory(self, *, factory: AttackTechniqueFactory)
AttackScoringConfig | None: The most specific config that could
be built, or ``None`` if the technique is incompatible with
the scenario scorer.
+
+ Raises:
+ TypeError: If a factory returns a non-``AttackScoringConfig`` instance.
"""
required = factory.scoring_config_type
if required is None or required is AttackScoringConfig:
return AttackScoringConfig(objective_scorer=self._objective_scorer)
try:
- return required(objective_scorer=self._objective_scorer)
+ config = required(objective_scorer=self._objective_scorer)
except (TypeError, ValueError):
return None
+ if not isinstance(config, AttackScoringConfig):
+ raise TypeError(f"Scoring config factory returned unsupported type: {type(config).__name__}")
+ return config
async def _build_atomics_for_dataset_async(
self,
diff --git a/pyrit/scenario/scenarios/foundry/red_team_agent.py b/pyrit/scenario/scenarios/foundry/red_team_agent.py
index 874ca6d752..5d0cc4f235 100644
--- a/pyrit/scenario/scenarios/foundry/red_team_agent.py
+++ b/pyrit/scenario/scenarios/foundry/red_team_agent.py
@@ -108,7 +108,7 @@ def __post_init__(self) -> None:
def name(self) -> str:
"""A human-readable name for this composite."""
if not self.converters:
- return self.attack.value if self.attack else "baseline"
+ return str(self.attack.value) if self.attack else "baseline"
if self.attack is None and len(self.converters) == 1:
return str(self.converters[0].value)
attack_name = self.attack.value if self.attack else "baseline"
diff --git a/pyrit/score/conversation_scorer.py b/pyrit/score/conversation_scorer.py
index 5ddf784947..bf48e04004 100644
--- a/pyrit/score/conversation_scorer.py
+++ b/pyrit/score/conversation_scorer.py
@@ -212,6 +212,7 @@ def create_conversation_scorer(
Scorer: A ConversationScorer instance that is also an instance of the wrapped scorer's type.
Raises:
+ TypeError: If the dynamic scorer does not inherit from ``Scorer``.
ValueError: If the scorer is not an instance of FloatScaleScorer or TrueFalseScorer.
Example:
@@ -243,11 +244,22 @@ class DynamicConversationScorer(ConversationScorer, scorer_base_class): # type:
def __init__(self) -> None:
# Initialize with the validator and wrapped scorer
MessageScorer.__init__(self, validator=validator or ConversationScorer._DEFAULT_VALIDATOR)
- self._wrapped_scorer = wrapped_scorer
+ self._wrapped_scorer: MessageScorer = wrapped_scorer
def _get_wrapped_scorer(self) -> MessageScorer:
- """Return the wrapped scorer."""
- return self._wrapped_scorer
+ """
+ Return the wrapped scorer.
+
+ Returns:
+ MessageScorer: The scorer used for conversation-level evaluation.
+
+ Raises:
+ TypeError: If the stored wrapped scorer is not a ``MessageScorer``.
+ """
+ wrapped_scorer = self._wrapped_scorer
+ if not isinstance(wrapped_scorer, MessageScorer):
+ raise TypeError("Wrapped conversation scorer must inherit from MessageScorer")
+ return wrapped_scorer
def _build_identifier(self) -> ComponentIdentifier:
"""
@@ -255,9 +267,18 @@ def _build_identifier(self) -> ComponentIdentifier:
Returns:
ComponentIdentifier: The identifier for this scorer.
+
+ Raises:
+ TypeError: If identifier construction returns an unexpected type.
"""
- return self._create_identifier(
+ identifier = self._create_identifier(
sub_scorers=[self._wrapped_scorer.get_identifier()],
)
-
- return DynamicConversationScorer()
+ if not isinstance(identifier, ComponentIdentifier):
+ raise TypeError("Conversation scorer identifier must be a ComponentIdentifier")
+ return identifier
+
+ conversation_scorer = DynamicConversationScorer()
+ if not isinstance(conversation_scorer, Scorer):
+ raise TypeError("Dynamic conversation scorer must inherit from Scorer")
+ return conversation_scorer
diff --git a/pyrit/score/float_scale/video_float_scale_scorer.py b/pyrit/score/float_scale/video_float_scale_scorer.py
index 7ae07fe118..84b7345bb2 100644
--- a/pyrit/score/float_scale/video_float_scale_scorer.py
+++ b/pyrit/score/float_scale/video_float_scale_scorer.py
@@ -124,7 +124,10 @@ def matched_conditions(self) -> frozenset[type[Condition]]:
scorers = [self._video_helper.image_scorer]
if self.audio_scorer:
scorers.append(self.audio_scorer)
- return frozenset().union(*(scorer.matched_conditions() for scorer in scorers))
+ conditions: set[type[Condition]] = set()
+ for scorer in scorers:
+ conditions.update(scorer.matched_conditions())
+ return frozenset(conditions)
def required_conditions(self) -> frozenset[type[Condition]]:
"""
@@ -136,7 +139,10 @@ def required_conditions(self) -> frozenset[type[Condition]]:
scorers = [self._video_helper.image_scorer]
if self.audio_scorer:
scorers.append(self.audio_scorer)
- return frozenset().union(*(scorer.required_conditions() for scorer in scorers))
+ conditions: set[type[Condition]] = set()
+ for scorer in scorers:
+ conditions.update(scorer.required_conditions())
+ return frozenset(conditions)
async def _score_piece_async(self, message_piece: MessagePiece, *, objective: str | None = None) -> list[Score]:
"""
diff --git a/pyrit/score/score_utils.py b/pyrit/score/score_utils.py
index 4429b34e67..93baea6e87 100644
--- a/pyrit/score/score_utils.py
+++ b/pyrit/score/score_utils.py
@@ -2,7 +2,6 @@
# Licensed under the MIT license.
-from pyrit.common.utils import combine_dict
from pyrit.models import Score
# Key used by FloatScaleThresholdScorer to store the original float value
@@ -24,8 +23,9 @@ def combine_metadata_and_categories(scores: list[Score]) -> tuple[dict[str, str
category_set: set[str] = set()
for s in scores:
- metadata = combine_dict(metadata, getattr(s, "score_metadata", None))
- score_categories = getattr(s, "score_category", None) or []
+ if s.score_metadata:
+ metadata.update(s.score_metadata)
+ score_categories = s.score_category or []
category_set.update([c for c in score_categories if c])
category = sorted(category_set)
diff --git a/pyrit/score/scorer.py b/pyrit/score/scorer.py
index 086d4a019c..fc1749b2ca 100644
--- a/pyrit/score/scorer.py
+++ b/pyrit/score/scorer.py
@@ -66,7 +66,8 @@ async def _legacy_score_scorable_async(
resolver = getattr(self, "_message_resolver", None) or MessageScorableResolver()
message = resolver.resolve(scorable=scorable, memory=self._memory)
legacy_score_async = self._score_async # type: ignore[ty:unresolved-attribute]
- return await legacy_score_async(message, objective=expectation.objective if expectation else None)
+ scores: list[Score] = await legacy_score_async(message, objective=expectation.objective if expectation else None)
+ return scores
def _adapt_legacy_message_scorer(cls: type) -> None:
@@ -187,7 +188,8 @@ def get_chat_target(self) -> PromptTarget | None:
Returns:
PromptTarget | None: The chat target, or None if not applicable.
"""
- return getattr(self, "_prompt_target", None)
+ prompt_target: PromptTarget | None = getattr(self, "_prompt_target", None)
+ return prompt_target
def get_identifier(self) -> ComponentIdentifier:
"""
diff --git a/pyrit/score/true_false/true_false_composite_scorer.py b/pyrit/score/true_false/true_false_composite_scorer.py
index fd3cf2a2e1..679afb3210 100644
--- a/pyrit/score/true_false/true_false_composite_scorer.py
+++ b/pyrit/score/true_false/true_false_composite_scorer.py
@@ -89,7 +89,10 @@ def matched_conditions(self) -> frozenset[type[Condition]]:
Returns:
frozenset[type[Condition]]: The condition types this composite routes.
"""
- return frozenset().union(*(scorer.matched_conditions() for scorer in self._scorers))
+ conditions: set[type[Condition]] = set()
+ for scorer in self._scorers:
+ conditions.update(scorer.matched_conditions())
+ return frozenset(conditions)
def required_conditions(self) -> frozenset[type[Condition]]:
"""
@@ -98,7 +101,10 @@ def required_conditions(self) -> frozenset[type[Condition]]:
Returns:
frozenset[type[Condition]]: The required condition types.
"""
- return frozenset().union(*(scorer.required_conditions() for scorer in self._scorers))
+ conditions: set[type[Condition]] = set()
+ for scorer in self._scorers:
+ conditions.update(scorer.required_conditions())
+ return frozenset(conditions)
async def _score_prepared_message_async(
self,
diff --git a/pyrit/score/true_false/video_true_false_scorer.py b/pyrit/score/true_false/video_true_false_scorer.py
index 2370de1841..9e390e9fdb 100644
--- a/pyrit/score/true_false/video_true_false_scorer.py
+++ b/pyrit/score/true_false/video_true_false_scorer.py
@@ -101,7 +101,10 @@ def matched_conditions(self) -> frozenset[type[Condition]]:
scorers = [self._video_helper.image_scorer]
if self.audio_scorer:
scorers.append(self.audio_scorer)
- return frozenset().union(*(scorer.matched_conditions() for scorer in scorers))
+ conditions: set[type[Condition]] = set()
+ for scorer in scorers:
+ conditions.update(scorer.matched_conditions())
+ return frozenset(conditions)
def required_conditions(self) -> frozenset[type[Condition]]:
"""
@@ -113,7 +116,10 @@ def required_conditions(self) -> frozenset[type[Condition]]:
scorers = [self._video_helper.image_scorer]
if self.audio_scorer:
scorers.append(self.audio_scorer)
- return frozenset().union(*(scorer.required_conditions() for scorer in scorers))
+ conditions: set[type[Condition]] = set()
+ for scorer in scorers:
+ conditions.update(scorer.required_conditions())
+ return frozenset(conditions)
async def _score_piece_async(self, message_piece: MessagePiece, *, objective: str | None = None) -> list[Score]:
"""
diff --git a/pyrit/setup/configuration_loader.py b/pyrit/setup/configuration_loader.py
index 26a29c45b9..86f6b402e2 100644
--- a/pyrit/setup/configuration_loader.py
+++ b/pyrit/setup/configuration_loader.py
@@ -509,11 +509,11 @@ def resolve_initializers(self) -> Sequence["PyRITInitializer"]:
from pyrit.registry import InitializerRegistry
configs = self._initializer_configs
+ resolved: list[PyRITInitializer] = []
if not configs:
- return []
+ return resolved
registry = InitializerRegistry()
- resolved: list[PyRITInitializer] = []
logging.getLogger(__name__).info("Running %d initializer(s)...", len(configs))
@@ -544,7 +544,7 @@ def resolve_initialization_scripts(self) -> Sequence[pathlib.Path] | None:
# Empty list means "load nothing" - return empty list
if len(self.initialization_scripts) == 0:
- return []
+ return list[pathlib.Path]()
resolved: list[pathlib.Path] = []
for script_str in self.initialization_scripts:
@@ -569,7 +569,7 @@ def resolve_env_files(self) -> Sequence[pathlib.Path] | None:
# Empty list means "load nothing" - return empty list
if len(self.env_files) == 0:
- return []
+ return list[pathlib.Path]()
resolved: list[pathlib.Path] = []
for env_str in self.env_files:
diff --git a/tests/unit/cli/test_pyrit_scan.py b/tests/unit/cli/test_pyrit_scan.py
index 0d52bc9ef4..db6fb98da7 100644
--- a/tests/unit/cli/test_pyrit_scan.py
+++ b/tests/unit/cli/test_pyrit_scan.py
@@ -1024,6 +1024,43 @@ async def test_resolution_order_cli_beats_config_beats_default(self):
):
assert await pyrit_scan._resolve_server_url_async(parsed_args=parsed) == DEFAULT_SERVER_URL
+ async def test_raises_type_error_when_configured_url_is_not_a_string(self):
+ """A non-string configured URL (e.g. malformed config) raises TypeError."""
+ parsed = Namespace(server_url=None, start_server=False, config_file=None)
+ with patch(
+ "pyrit.cli._config_reader.read_server_settings",
+ return_value=pyrit_scan_config_reader.ServerSettings(url=12345), # type: ignore[arg-type]
+ ):
+ with pytest.raises(TypeError, match="Configured server URL must be a string"):
+ await pyrit_scan._resolve_server_url_async(parsed_args=parsed)
+
+
+class TestResolveConfiguredServerUrl:
+ """Tests for _resolve_configured_server_url."""
+
+ def test_uses_cli_flag_when_provided(self):
+ parsed = Namespace(server_url="http://cli:1111", config_file=None)
+ assert pyrit_scan._resolve_configured_server_url(parsed_args=parsed) == "http://cli:1111"
+
+ def test_falls_back_to_config_file(self):
+ parsed = Namespace(server_url=None, config_file=None)
+ with patch("pyrit.cli._config_reader.read_server_url", return_value="http://cfg:2222"):
+ assert pyrit_scan._resolve_configured_server_url(parsed_args=parsed) == "http://cfg:2222"
+
+ def test_falls_back_to_default(self):
+ from pyrit.cli._config_reader import DEFAULT_SERVER_URL
+
+ parsed = Namespace(server_url=None, config_file=None)
+ with patch("pyrit.cli._config_reader.read_server_url", return_value=None):
+ assert pyrit_scan._resolve_configured_server_url(parsed_args=parsed) == DEFAULT_SERVER_URL
+
+ def test_raises_type_error_when_configured_url_is_not_a_string(self):
+ """A non-string configured URL (e.g. malformed config) raises TypeError."""
+ parsed = Namespace(server_url=None, config_file=None)
+ with patch("pyrit.cli._config_reader.read_server_url", return_value=12345):
+ with pytest.raises(TypeError, match="Configured server URL must be a string"):
+ pyrit_scan._resolve_configured_server_url(parsed_args=parsed)
+
class TestScenarioParamCoercion:
"""Regression tests for client-side coercion of typed scenario-declared params."""
diff --git a/tests/unit/executor/attack/multi_turn/test_tree_of_attacks.py b/tests/unit/executor/attack/multi_turn/test_tree_of_attacks.py
index 6e9431ce19..d984134357 100644
--- a/tests/unit/executor/attack/multi_turn/test_tree_of_attacks.py
+++ b/tests/unit/executor/attack/multi_turn/test_tree_of_attacks.py
@@ -2472,6 +2472,22 @@ def test_tap_init_raises_when_objective_scorer_is_none():
)
+def test_tap_scoring_config_threshold_raises_for_reassigned_invalid_scorer():
+ """threshold re-validates objective_scorer at access time, not just at construction.
+
+ __init__ already rejects a non-FloatScaleThresholdScorer objective_scorer, so this
+ exercises the defensive re-check by mutating the attribute after construction.
+ """
+ mock_threshold_scorer = MagicMock(spec=FloatScaleThresholdScorer)
+ mock_threshold_scorer.threshold = 0.8
+ scoring_config = TAPAttackScoringConfig(objective_scorer=mock_threshold_scorer)
+
+ scoring_config.objective_scorer = MagicMock(spec=Scorer)
+
+ with pytest.raises(TypeError, match="TAP objective scorer must be a FloatScaleThresholdScorer"):
+ _ = scoring_config.threshold
+
+
def test_tap_attack_result_tree_visualization_getter_returns_value():
"""Test that TAPAttackResult.tree_visualization returns the stored tree."""
tree = Tree()
diff --git a/tests/unit/prompt_target/target/test_openai_error_handling.py b/tests/unit/prompt_target/target/test_openai_error_handling.py
index cfbea439c0..d91df315f6 100644
--- a/tests/unit/prompt_target/target/test_openai_error_handling.py
+++ b/tests/unit/prompt_target/target/test_openai_error_handling.py
@@ -6,9 +6,19 @@
from pyrit.exceptions import CONTENT_FILTER_MARKERS
from pyrit.prompt_target.openai.openai_error_handling import (
SAFETY_MESSAGE_MARKERS,
+ _extract_error_payload,
_is_content_filter_error,
)
+
+class _FakeBodyError(Exception):
+ """A minimal exception stand-in exposing a ``body`` attribute like the OpenAI SDK's errors."""
+
+ def __init__(self, body: object) -> None:
+ super().__init__("fake error")
+ self.body = body
+
+
# Tests for _is_content_filter_error helper
@@ -109,3 +119,47 @@ def test_is_content_filter_error_no_filter():
def test_is_content_filter_error_string_no_filter():
"""String input without any marker returns False."""
assert _is_content_filter_error("connection timed out") is False
+
+
+# Tests for _extract_error_payload helper
+
+
+def test_extract_error_payload_body_dict():
+ """When exc.body is a dict, it's returned as-is with content-filter detection applied."""
+ exc = _FakeBodyError({"error": {"code": "content_filter"}})
+ payload, is_filter = _extract_error_payload(exc)
+ assert payload == {"error": {"code": "content_filter"}}
+ assert is_filter is True
+
+
+def test_extract_error_payload_body_json_string_decodes_to_dict():
+ """When exc.body is a JSON string that decodes to a dict, the parsed dict is returned."""
+ exc = _FakeBodyError('{"error": {"code": "content_filter"}}')
+ payload, is_filter = _extract_error_payload(exc)
+ assert payload == {"error": {"code": "content_filter"}}
+ assert is_filter is True
+
+
+def test_extract_error_payload_body_json_string_decodes_to_non_dict():
+ """When exc.body is a JSON string that decodes to a non-dict (e.g. a list), the original
+ string body is returned rather than the parsed value."""
+ exc = _FakeBodyError('["not", "a", "dict"]')
+ payload, is_filter = _extract_error_payload(exc)
+ assert payload == '["not", "a", "dict"]'
+ assert is_filter is False
+
+
+def test_extract_error_payload_body_non_json_string():
+ """When exc.body is a non-JSON string, the original string is returned unparsed."""
+ exc = _FakeBodyError("not json at all")
+ payload, is_filter = _extract_error_payload(exc)
+ assert payload == "not json at all"
+ assert is_filter is False
+
+
+def test_extract_error_payload_falls_back_to_str_of_exception():
+ """When exc has neither a response nor a body, the payload falls back to str(exc)."""
+ exc = Exception("plain failure")
+ payload, is_filter = _extract_error_payload(exc)
+ assert payload == "plain failure"
+ assert is_filter is False
diff --git a/tests/unit/scenario/core/test_atomic_attack.py b/tests/unit/scenario/core/test_atomic_attack.py
index 28fd9d8004..8a04f20457 100644
--- a/tests/unit/scenario/core/test_atomic_attack.py
+++ b/tests/unit/scenario/core/test_atomic_attack.py
@@ -314,6 +314,20 @@ async def test_run_async_passes_seed_groups(self, mock_attack, sample_seed_group
assert "seed_groups" in call_kwargs
assert call_kwargs["seed_groups"] == sample_seed_groups
+ async def test_run_async_raises_when_executor_returns_non_attack_result(self, mock_attack, sample_seed_groups):
+ """Test that a non-AttackResult item from the executor raises ValueError."""
+ atomic_attack = AtomicAttack(
+ attack_technique=AttackTechnique(attack=mock_attack),
+ seed_groups=sample_seed_groups,
+ atomic_attack_name="Test Attack Run",
+ )
+
+ with patch.object(AttackExecutor, "execute_attack_from_seed_groups_async", new_callable=AsyncMock) as mock_exec:
+ mock_exec.return_value = wrap_results(["not-an-attack-result"])
+
+ with pytest.raises(ValueError, match="unsupported result type"):
+ await atomic_attack.run_async()
+
async def test_run_async_passes_attack_execute_params(self, mock_attack, sample_seed_groups, sample_attack_results):
"""Test that attack execute parameters are passed to the executor."""
atomic_attack = AtomicAttack(
diff --git a/tests/unit/scenario/core/test_attack_technique_factory.py b/tests/unit/scenario/core/test_attack_technique_factory.py
index 1124cd4df2..5ea41cd64b 100644
--- a/tests/unit/scenario/core/test_attack_technique_factory.py
+++ b/tests/unit/scenario/core/test_attack_technique_factory.py
@@ -3,6 +3,7 @@
"""Tests for the AttackTechniqueFactory class."""
+import typing
from unittest.mock import MagicMock, patch
import pytest
@@ -974,3 +975,39 @@ def test_unwrap_non_type_annotation_returns_none(self):
"""A non-type annotation (e.g., string forward ref) returns None."""
result = AttackTechniqueFactory._unwrap_optional("SomeForwardRef")
assert result is None
+
+ def test_unwrap_typing_optional_with_none(self):
+ """typing.Optional[X] (legacy typing.Union syntax) should unwrap to X."""
+ # Intentionally uses the legacy typing.Optional/Union construct (rather than `X | None`)
+ # to exercise _unwrap_optional's typing.Union-origin branch specifically.
+ result = AttackTechniqueFactory._unwrap_optional(typing.Optional[AttackScoringConfig]) # noqa: UP045
+ assert result is AttackScoringConfig
+
+ def test_unwrap_typing_union_multi_returns_none(self):
+ """typing.Union of more than one non-None type returns None (ambiguous)."""
+ # Intentionally uses typing.Union (rather than `X | Y`) to exercise _unwrap_optional's
+ # typing.Union-origin branch specifically.
+ result = AttackTechniqueFactory._unwrap_optional(typing.Union[int, str, None]) # noqa: UP007
+ assert result is None
+
+
+class TestGetScoringConfigType:
+ """Tests for AttackTechniqueFactory._get_scoring_config_type."""
+
+ def test_returns_none_when_annotation_is_not_attack_scoring_config_subclass(self):
+ """A resolved, narrowed annotation that isn't an AttackScoringConfig subclass yields None."""
+
+ class _WrongAnnotationAttack:
+ def __init__(self, *, objective_target, attack_scoring_config: int | None = None):
+ pass
+
+ def get_identifier(self):
+ return ComponentIdentifier(class_name="_WrongAnnotationAttack", class_module="test")
+
+ factory = AttackTechniqueFactory(
+ name="test",
+ attack_class=_WrongAnnotationAttack,
+ scorer_override_policy=ScorerOverridePolicy.SKIP,
+ )
+
+ assert factory._get_scoring_config_type() is None
diff --git a/tests/unit/scenario/core/test_scenario.py b/tests/unit/scenario/core/test_scenario.py
index 73dbc5a048..27c8cae746 100644
--- a/tests/unit/scenario/core/test_scenario.py
+++ b/tests/unit/scenario/core/test_scenario.py
@@ -17,6 +17,7 @@
from pyrit.executor.attack.core import AttackExecutorResult
from pyrit.memory import CentralMemory
from pyrit.models import AttackOutcome, AttackResult, ComponentIdentifier, ScenarioRunState
+from pyrit.prompt_target import PromptTarget
from pyrit.scenario import (
DatasetAttackConfiguration,
DatasetConfiguration,
@@ -116,7 +117,7 @@ def mock_atomic_attacks():
@pytest.fixture
def mock_objective_target():
"""Create a mock objective target for testing."""
- target = MagicMock()
+ target = MagicMock(spec=PromptTarget)
target.get_identifier.return_value = ComponentIdentifier(
class_name="MockTarget",
class_module="test",
diff --git a/tests/unit/scenario/core/test_scenario_parameters.py b/tests/unit/scenario/core/test_scenario_parameters.py
index 98fe9e28cb..9dffbdd92a 100644
--- a/tests/unit/scenario/core/test_scenario_parameters.py
+++ b/tests/unit/scenario/core/test_scenario_parameters.py
@@ -9,6 +9,7 @@
import pytest
from pyrit.models import ComponentIdentifier, Parameter
+from pyrit.prompt_target import PromptTarget
from pyrit.scenario import DatasetConfiguration
from pyrit.scenario.core import BaselineAttackPolicy, Scenario, ScenarioTechnique
from pyrit.score import Scorer
@@ -558,7 +559,7 @@ class TestParamPersistenceJsonSafety:
@staticmethod
def _mock_target() -> MagicMock:
- target = MagicMock()
+ target = MagicMock(spec=PromptTarget)
target.get_identifier.return_value = ComponentIdentifier(class_name="MockTarget", class_module="test")
return target
@@ -592,7 +593,7 @@ class _NotJsonable:
def _mock_objective_target() -> MagicMock:
- target = MagicMock()
+ target = MagicMock(spec=PromptTarget)
target.get_identifier.return_value = ComponentIdentifier(class_name="MockTarget", class_module="test")
return target
@@ -667,6 +668,40 @@ async def test_unregistered_name_raises(self) -> None:
finally:
TargetRegistry.reset_registry_singleton()
+ async def test_resolved_non_target_value_raises_type_error(self) -> None:
+ """A supplied value that isn't a string name and isn't a PromptTarget raises TypeError."""
+ scenario = _make_scenario(declared_params=[], include_common_params=True)
+ scenario.set_params_from_args(args={"objective_target": object()})
+ with pytest.raises(TypeError, match="Resolved objective_target must be a PromptTarget"):
+ await scenario.initialize_async()
+
+ async def test_default_non_target_value_raises_type_error(self) -> None:
+ """A registered default that isn't a PromptTarget raises TypeError."""
+ from pyrit.common import reset_default_values, set_default_value
+
+ scenario = _make_scenario(declared_params=[], include_common_params=True)
+ set_default_value(class_type=type(scenario), parameter_name="objective_target", value=object())
+ try:
+ scenario.set_params_from_args(args={})
+ with pytest.raises(TypeError, match="Default objective_target must be a PromptTarget"):
+ await scenario.initialize_async()
+ finally:
+ reset_default_values()
+
+ async def test_valid_default_target_is_used_when_no_value_supplied(self) -> None:
+ """A registered default that is a valid PromptTarget is used when no value is supplied."""
+ from pyrit.common import reset_default_values, set_default_value
+
+ default_target = _mock_objective_target()
+ scenario = _make_scenario(declared_params=[], include_common_params=True)
+ set_default_value(class_type=type(scenario), parameter_name="objective_target", value=default_target)
+ try:
+ scenario.set_params_from_args(args={})
+ await scenario.initialize_async()
+ assert scenario._objective_target is default_target
+ finally:
+ reset_default_values()
+
@pytest.mark.usefixtures("patch_central_database")
class TestOpaquePassthrough:
diff --git a/tests/unit/scenario/core/test_scenario_partial_results.py b/tests/unit/scenario/core/test_scenario_partial_results.py
index dd8fa97b06..d6f58e902b 100644
--- a/tests/unit/scenario/core/test_scenario_partial_results.py
+++ b/tests/unit/scenario/core/test_scenario_partial_results.py
@@ -18,6 +18,7 @@
from pyrit.executor.attack.core import AttackExecutorResult
from pyrit.memory import CentralMemory
from pyrit.models import AttackOutcome, AttackResult, ComponentIdentifier, ScenarioRunState
+from pyrit.prompt_target import PromptTarget
from pyrit.scenario import DatasetConfiguration, ScenarioResult
from pyrit.scenario.core import AtomicAttack, BaselineAttackPolicy, Scenario, ScenarioTechnique
@@ -33,7 +34,7 @@ def _mock_scorer_id(name: str = "MockScorer") -> ComponentIdentifier:
@pytest.fixture
def mock_objective_target():
"""Create a mock objective target for testing."""
- target = MagicMock()
+ target = MagicMock(spec=PromptTarget)
target.get_identifier.return_value = ComponentIdentifier(
class_name="MockTarget",
class_module="test",
diff --git a/tests/unit/scenario/core/test_scenario_retry.py b/tests/unit/scenario/core/test_scenario_retry.py
index f1ed5a1572..af4a222d0b 100644
--- a/tests/unit/scenario/core/test_scenario_retry.py
+++ b/tests/unit/scenario/core/test_scenario_retry.py
@@ -240,7 +240,7 @@ def mock_atomic_attacks():
@pytest.fixture
def mock_objective_target():
"""Create a mock objective target for testing."""
- target = MagicMock()
+ target = MagicMock(spec=PromptTarget)
target.get_identifier.return_value = ComponentIdentifier(
class_name="MockTarget",
class_module=TEST_MODULE,
diff --git a/tests/unit/score/test_conversation_history_scorer.py b/tests/unit/score/test_conversation_history_scorer.py
index d9f10ec488..ff893ee984 100644
--- a/tests/unit/score/test_conversation_history_scorer.py
+++ b/tests/unit/score/test_conversation_history_scorer.py
@@ -2,7 +2,7 @@
# Licensed under the MIT license.
import uuid
-from unittest.mock import AsyncMock, MagicMock
+from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from unit.mocks import store_message
@@ -434,6 +434,25 @@ def test_factory_creates_unique_instances():
assert isinstance(conv_scorer2, ConversationScorer)
+def test_get_wrapped_scorer_raises_when_wrapped_scorer_reassigned_to_non_scorer():
+ """_get_wrapped_scorer re-validates the stored scorer, not just at construction time."""
+ conv_scorer = create_conversation_scorer(scorer=MockFloatScaleScorer())
+
+ conv_scorer._wrapped_scorer = "not-a-scorer" # type: ignore[assignment]
+
+ with pytest.raises(TypeError, match="Wrapped conversation scorer must inherit from MessageScorer"):
+ conv_scorer._get_wrapped_scorer()
+
+
+def test_build_identifier_raises_when_create_identifier_returns_non_component_identifier():
+ """_build_identifier re-validates the identifier produced by _create_identifier."""
+ conv_scorer = create_conversation_scorer(scorer=MockFloatScaleScorer())
+
+ with patch.object(type(conv_scorer), "_create_identifier", return_value="not-an-identifier"):
+ with pytest.raises(TypeError, match="Conversation scorer identifier must be a ComponentIdentifier"):
+ conv_scorer._build_identifier()
+
+
def test_conversation_scorer_validates_float_scale_scores():
"""Test that ConversationScorer delegates float scale score validation to wrapped scorer."""
scorer = MockFloatScaleScorer()
diff --git a/uv.lock b/uv.lock
index 8fa68fe46c..26f78f6208 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1095,7 +1095,7 @@ resolution-markers = [
"(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
]
dependencies = [
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
wheels = [
@@ -1173,7 +1173,7 @@ resolution-markers = [
"(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')",
]
dependencies = [
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
@@ -1417,7 +1417,7 @@ name = "cuda-bindings"
version = "13.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "cuda-pathfinder" },
+ { name = "cuda-pathfinder", marker = "sys_platform != 'darwin'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/21/8464d133752951c154feafb3b65c297e7d80f301183d220bec4c830f1441/cuda_bindings-13.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120fcc53d57903df529c3486962c56528cba5b7d6c57c99537320ed9922c8b86", size = 6073403, upload-time = "2026-05-29T23:11:36.22Z" },
@@ -1452,43 +1452,43 @@ wheels = [
[package.optional-dependencies]
cublas = [
- { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
- { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
+ { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
cudart = [
- { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
cufft = [
- { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
- { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
+ { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
cufile = [
- { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
cupti = [
- { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
curand = [
- { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
cusolver = [
- { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
- { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
- { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
- { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
+ { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
+ { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
+ { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
cusparse = [
- { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
- { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
+ { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
nvjitlink = [
- { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
nvrtc = [
- { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
nvtx = [
- { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" },
]
[[package]]
@@ -1699,7 +1699,7 @@ name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
@@ -2420,17 +2420,17 @@ resolution-markers = [
"(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
]
dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "decorator" },
- { name = "exceptiongroup" },
- { name = "jedi" },
- { name = "matplotlib-inline" },
- { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
- { name = "prompt-toolkit" },
- { name = "pygments" },
- { name = "stack-data" },
- { name = "traitlets" },
- { name = "typing-extensions" },
+ { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
+ { name = "decorator", marker = "python_full_version < '3.11'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "jedi", marker = "python_full_version < '3.11'" },
+ { name = "matplotlib-inline", marker = "python_full_version < '3.11'" },
+ { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit", marker = "python_full_version < '3.11'" },
+ { name = "pygments", marker = "python_full_version < '3.11'" },
+ { name = "stack-data", marker = "python_full_version < '3.11'" },
+ { name = "traitlets", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" }
wheels = [
@@ -2453,17 +2453,17 @@ resolution-markers = [
"(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')",
]
dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "decorator" },
- { name = "ipython-pygments-lexers" },
- { name = "jedi" },
- { name = "matplotlib-inline" },
- { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
- { name = "prompt-toolkit" },
- { name = "pygments" },
- { name = "stack-data" },
- { name = "traitlets" },
- { name = "typing-extensions", marker = "python_full_version < '3.12'" },
+ { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" },
+ { name = "decorator", marker = "python_full_version >= '3.11'" },
+ { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" },
+ { name = "jedi", marker = "python_full_version >= '3.11'" },
+ { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" },
+ { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" },
+ { name = "pygments", marker = "python_full_version >= '3.11'" },
+ { name = "stack-data", marker = "python_full_version >= '3.11'" },
+ { name = "traitlets", marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version == '3.11.*'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/dd/fb08d22ec0c27e73c8bc8f71810709870d51cadaf27b7ddd3f011236c100/ipython-9.9.0.tar.gz", hash = "sha256:48fbed1b2de5e2c7177eefa144aba7fcb82dac514f09b57e2ac9da34ddb54220", size = 4425043, upload-time = "2026-01-05T12:36:46.233Z" }
wheels = [
@@ -2475,7 +2475,7 @@ name = "ipython-pygments-lexers"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "pygments" },
+ { name = "pygments", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
wheels = [
@@ -4019,7 +4019,7 @@ name = "nvidia-cublas"
version = "13.1.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "nvidia-cuda-nvrtc" },
+ { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'darwin'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" },
@@ -4058,7 +4058,7 @@ name = "nvidia-cudnn-cu13"
version = "9.20.0.48"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "nvidia-cublas" },
+ { name = "nvidia-cublas", marker = "sys_platform != 'darwin'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" },
@@ -4070,7 +4070,7 @@ name = "nvidia-cufft"
version = "12.0.0.61"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "nvidia-nvjitlink" },
+ { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
@@ -4100,9 +4100,9 @@ name = "nvidia-cusolver"
version = "12.0.4.66"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "nvidia-cublas" },
- { name = "nvidia-cusparse" },
- { name = "nvidia-nvjitlink" },
+ { name = "nvidia-cublas", marker = "sys_platform != 'darwin'" },
+ { name = "nvidia-cusparse", marker = "sys_platform != 'darwin'" },
+ { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
@@ -4114,7 +4114,7 @@ name = "nvidia-cusparse"
version = "12.6.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "nvidia-nvjitlink" },
+ { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
@@ -5487,7 +5487,7 @@ dev = [
{ name = "pytest-xdist", specifier = ">=3.6.1" },
{ name = "respx", specifier = ">=0.22.0" },
{ name = "ruff", specifier = ">=0.14.4" },
- { name = "ty", specifier = ">=0.0.32,<0.0.70" },
+ { name = "ty", specifier = ">=0.0.32" },
{ name = "types-aiofiles", specifier = ">=24.1.0" },
{ name = "types-pyyaml", specifier = ">=6.0.12.20250516" },
{ name = "types-requests", specifier = ">=2.31.0.20250515" },
@@ -5794,9 +5794,9 @@ resolution-markers = [
"(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
]
dependencies = [
- { name = "attrs" },
- { name = "rpds-py" },
- { name = "typing-extensions" },
+ { name = "attrs", marker = "python_full_version < '3.12'" },
+ { name = "rpds-py", marker = "python_full_version < '3.12'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.12'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" }
wheels = [
@@ -5816,9 +5816,9 @@ resolution-markers = [
"(python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'darwin' and sys_platform != 'linux')",
]
dependencies = [
- { name = "attrs" },
- { name = "rpds-py" },
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+ { name = "attrs", marker = "python_full_version >= '3.12'" },
+ { name = "rpds-py", marker = "python_full_version >= '3.12'" },
+ { name = "typing-extensions", marker = "python_full_version == '3.12.*'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
wheels = [
@@ -6212,7 +6212,7 @@ resolution-markers = [
"(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
]
dependencies = [
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
wheels = [
@@ -6279,7 +6279,7 @@ resolution-markers = [
"(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')",
]
dependencies = [
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" }
@@ -7128,27 +7128,27 @@ wheels = [
[[package]]
name = "ty"
-version = "0.0.69"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/8e/5b/7a618632dfe9373b7df572ecd7a08c8f799d772fbc317da82dd3aa363207/ty-0.0.69.tar.gz", hash = "sha256:b65106e9ff24fa76e25e1142fb09c85244e815c40450e3021d2bf652c231bb43", size = 6565094, upload-time = "2026-08-06T10:04:25.667Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/06/60/6534092f4d2c15e2491807edd609c2e50d527c1fed957acf40b9f110b64a/ty-0.0.69-py3-none-linux_armv6l.whl", hash = "sha256:98bfd383b273540829af673e7f98b9c1c4bcc8547d12a1a3806cd0bec7f0e087", size = 12364185, upload-time = "2026-08-06T10:03:47.137Z" },
- { url = "https://files.pythonhosted.org/packages/34/2b/5c29689bd4f74c2e3394d983d85e4011b629f2ce3730c9442553b8554bf8/ty-0.0.69-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:964621ddd05771660017c51b4e74078d861d9fc863c21ef2a500db1ab62c9ccf", size = 12042510, upload-time = "2026-08-06T10:03:49.481Z" },
- { url = "https://files.pythonhosted.org/packages/09/46/fa085bde4d23516d7ef14b24736fc5dd7dc498f60f52b3d077e59ffdea20/ty-0.0.69-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3ffea4048dd0da4c9c97393b4be0901098a9065b06fa81be2477cbde65d8a151", size = 11549397, upload-time = "2026-08-06T10:03:51.747Z" },
- { url = "https://files.pythonhosted.org/packages/25/cc/97b9efb2061dcab6fef1e94a4ad99df0bb45bd2cc15d4f5794c787ee0552/ty-0.0.69-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8684d4a70aadd1eab0f41bdba835e3288ef49db8402a8e6ca81bab52ed5d610", size = 12115567, upload-time = "2026-08-06T10:03:53.79Z" },
- { url = "https://files.pythonhosted.org/packages/e0/c1/a5e0404965093835f3e62544e661784ec0aa8ef0b006ed50af50b19c107e/ty-0.0.69-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:afaaba240ab4122e2069a796836d10be81b4ddb053ae268b3dff962a0b4ca5c7", size = 12149770, upload-time = "2026-08-06T10:03:55.993Z" },
- { url = "https://files.pythonhosted.org/packages/e2/39/8cad6b205a4abe8a044ca0c84aea71e8ccda29b07a75a5f090e310605580/ty-0.0.69-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11ea63ef07d4e33aeb1a775cf5f2c736b3ed22fa6f8b1b608591612c36795044", size = 12941278, upload-time = "2026-08-06T10:03:58.324Z" },
- { url = "https://files.pythonhosted.org/packages/d6/8b/8766d96b732c2a060d70dc8ccafcc4d6a54109a2a95f1deb0705de88892b/ty-0.0.69-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb3730b1268e92a2907d7aea3afe8dd1b360ae65862f0557080cf479d481b424", size = 13426509, upload-time = "2026-08-06T10:04:00.621Z" },
- { url = "https://files.pythonhosted.org/packages/02/1f/e991b2cde953ea5b94d6a9a4c45c87937bd916bc09235f764407bf471c0a/ty-0.0.69-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a544ff57a752ef186ed40b5a2f44c17402af4cdefeb74a311ca02ebd57c4fca0", size = 13106582, upload-time = "2026-08-06T10:04:02.818Z" },
- { url = "https://files.pythonhosted.org/packages/ea/bb/73538f1b99e3558fd9db87b98698426f0f60fc8666da0b1efd0e70e275eb/ty-0.0.69-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87ed2cbca20caddfdf8e3e14d213ce91b67e75feed78900f4aaf3ef884954028", size = 12708931, upload-time = "2026-08-06T10:04:05.233Z" },
- { url = "https://files.pythonhosted.org/packages/87/cd/484a5208d74c4ad1155933906295ccdce9aa81a257d8df2ab9e41bd60133/ty-0.0.69-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2684efcbce5b6fe45045faf610b377b50781b6d2aa7e61ea23ecf5b3d2bce421", size = 12985322, upload-time = "2026-08-06T10:04:07.587Z" },
- { url = "https://files.pythonhosted.org/packages/6e/81/b75003f0d4da9ab3bc8fd4f4802f836cb9921ff7e70f460604f7b769a0b5/ty-0.0.69-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:da9aeb26fdac1d2214937542b59e0d4d1ba94ec7a3f45444f33c846de1eb1d63", size = 12063910, upload-time = "2026-08-06T10:04:09.835Z" },
- { url = "https://files.pythonhosted.org/packages/8a/76/088469f547ef63dceefc4a75826aedee5014f9371dc5171cde931896a82c/ty-0.0.69-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:00e7677cd14ede381f705f71104ea7b8ea0ce217a8634e19a89781953de0e9ad", size = 12166823, upload-time = "2026-08-06T10:04:12.114Z" },
- { url = "https://files.pythonhosted.org/packages/0a/c9/ce88a0bec0d46d8ae180b99c6ec014866fecc4cba1727b5feec8877b2765/ty-0.0.69-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d91965eb799649833d0d6042db09cd03d15289125245337cc46a2606effb7bda", size = 12483136, upload-time = "2026-08-06T10:04:14.33Z" },
- { url = "https://files.pythonhosted.org/packages/63/9e/6fae0ff225a0012642cf72c077e20f8f448c0a80771bc3360e8178fe2f32/ty-0.0.69-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1f03359cd8e5c412aa0c181118fa9b9061a4dddaedbb61bac0a424fb0814d402", size = 12799025, upload-time = "2026-08-06T10:04:16.445Z" },
- { url = "https://files.pythonhosted.org/packages/e4/43/78a658d18b2a4ccf35b053392f2213bf12e3c63b2abea512d3b6751d1f4c/ty-0.0.69-py3-none-win32.whl", hash = "sha256:ec460e01586b1eb91894c4a8403bee3e045a47e7a4ada943cc27ce8e348e88cf", size = 11787774, upload-time = "2026-08-06T10:04:18.622Z" },
- { url = "https://files.pythonhosted.org/packages/3a/5e/88db1f674403f2b81316a853a44a81ed220621fa96f8f7ae586fb6ca7513/ty-0.0.69-py3-none-win_amd64.whl", hash = "sha256:18976ca26a4e28fc3249477f79a695d5502e670803f2e080d89ac905baef3c6e", size = 12864038, upload-time = "2026-08-06T10:04:20.748Z" },
- { url = "https://files.pythonhosted.org/packages/4d/7b/6fc6efd00c69103d70f2bdbe824343089cd70b17b3079170057d3e5a3ac0/ty-0.0.69-py3-none-win_arm64.whl", hash = "sha256:7d4ca3bb74d91cb9947ba3f3b4cb131ad6a2b3ecc76d34040c4ec6092d2e411d", size = 12196693, upload-time = "2026-08-06T10:04:22.902Z" },
+version = "0.0.73"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e5/90/c4e1bb4cead3b644c3e258a27f9b05c7dc5eb0ec96a4f5282194edae9e0d/ty-0.0.73.tar.gz", hash = "sha256:823d4ce0d237bfc7eb6bcee70842f2c0706113813a16951077840743712f4b74", size = 6712739, upload-time = "2026-08-19T03:12:43.381Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e4/0f/f5e1801e55cc631f2db193276675b30561b963a2403da832bffb5d100267/ty-0.0.73-py3-none-linux_armv6l.whl", hash = "sha256:90a946082bf9bc446b5e72973d9f4ff1222a240b2ca4c9e6eed61eb913e30810", size = 12715452, upload-time = "2026-08-19T03:12:06.673Z" },
+ { url = "https://files.pythonhosted.org/packages/54/32/515dd05074c213b433524ab97eb003b0132ae7e358e0d75633ba7a314ed8/ty-0.0.73-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b7d6b5c6a6db7ea95fbbc16af514ef44a27a29a2fe1dc798900790364d170209", size = 12301870, upload-time = "2026-08-19T03:12:08.924Z" },
+ { url = "https://files.pythonhosted.org/packages/50/4d/085b4889f0d4bbe4af8b96242d4a1cb209fff95967cfa239ea141983719b/ty-0.0.73-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dd6f657f463e01372d8688f235be164750c8db722c97da27fa4903aa8d40b203", size = 12111741, upload-time = "2026-08-19T03:12:11.067Z" },
+ { url = "https://files.pythonhosted.org/packages/95/f6/d6ec277cadfecf03ad4c18551b67c4c6eb7807a0560d801db14be99d7a89/ty-0.0.73-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2de468e33fd44c9ff1c43473a7316f4289480f5cba8995a67b6d22aee39ca9", size = 12196124, upload-time = "2026-08-19T03:12:13.14Z" },
+ { url = "https://files.pythonhosted.org/packages/75/b7/ce78d8707563af9cae9bbd25328bfbc4931035085bd20089adf0c418f70e/ty-0.0.73-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2942fa0ef795a66034cdc8d75a72f453442f3b58ff2f69b4da05b7b954765b55", size = 12488557, upload-time = "2026-08-19T03:12:15.252Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/e8/329b9851b23502758c5c98e8cc875ea2a1b4c9674b4ca3a86da56a5063d3/ty-0.0.73-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e0f1ef14f642e18ac4e7a616a2796dcf7a5d82e28cd17f9796494acc7c4aabb", size = 13215606, upload-time = "2026-08-19T03:12:17.225Z" },
+ { url = "https://files.pythonhosted.org/packages/36/38/67fedfd2cb77516ef0066b1642f487dba0eb3006493cf3475b15f5b8b228/ty-0.0.73-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16981e15fdceedb37d0aff76c5ac25914595dfee2675af95335550064251ad22", size = 13665497, upload-time = "2026-08-19T03:12:19.286Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/b3/154f4dd48ec5eebc186ab4b822c6e62f982fc5ddfd262d6e3903c2acba44/ty-0.0.73-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644b2bec8a2e2e4957a942ae81d6cff5571c489bb5a8675e4d3886de537a694d", size = 13351231, upload-time = "2026-08-19T03:12:21.353Z" },
+ { url = "https://files.pythonhosted.org/packages/35/5f/d462496903fbe453fb76363f8478be929c8e6ff21e6928c57dcd7e5fa21f/ty-0.0.73-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338d565be3186f50ff8e9d10483685549c2d23f0754485d5ede3b54f4319188a", size = 12782586, upload-time = "2026-08-19T03:12:23.667Z" },
+ { url = "https://files.pythonhosted.org/packages/87/52/ec6d24b74abe3ec324204c1c71e6d0c6c76a17ffc15fd51d603b0a302abe/ty-0.0.73-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:11c7b6d839309d2c102cb3a4c03d817176bbfab5b2fccc95a75ec5c9597421c9", size = 13247134, upload-time = "2026-08-19T03:12:25.956Z" },
+ { url = "https://files.pythonhosted.org/packages/26/20/cc74650fec56a54786c6d7c89e09576fcad3092be34cf21715d39a406a9b/ty-0.0.73-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:488572db7ff97fb50ea36a76250f2d617c9727d143da6c7bf0623276eb0fc507", size = 12309344, upload-time = "2026-08-19T03:12:28.122Z" },
+ { url = "https://files.pythonhosted.org/packages/89/bd/4b0a9087f4315d7fbadf77a3ce44c816cc9ffabed1ced06cc5be81fbc414/ty-0.0.73-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1b958ebceefbbf594e59eb8d3d55bbd033ce634026fcba3e4bc3179e78e45bb7", size = 12502319, upload-time = "2026-08-19T03:12:30.128Z" },
+ { url = "https://files.pythonhosted.org/packages/11/80/0a925074911fe111912ea29d9eed309bcc183f43d2fb3eef07db056a0beb/ty-0.0.73-py3-none-musllinux_1_2_i686.whl", hash = "sha256:91a32993b3c34e42c3f323ad6c0399cb596bd1c27e9b7f20db7cd64c1067b68e", size = 12753688, upload-time = "2026-08-19T03:12:32.433Z" },
+ { url = "https://files.pythonhosted.org/packages/24/6b/aeccaf89efbc2e112bd415340a22e2669ec998aa397242503e747b712ca4/ty-0.0.73-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bab8a19fbf51f479bddb2a12c5fabfe52f918a5590362321ed5d89b44eb62c15", size = 13069050, upload-time = "2026-08-19T03:12:35.398Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/3e/eae485fd86c1585943fd4e1746b0757b2da01e2c43136ebe8c686fe1c7f1/ty-0.0.73-py3-none-win32.whl", hash = "sha256:03347a612f0fa020b19bfd8dbd521db6ecc75d377a3e4d4f6e6c2e62871da4cc", size = 12053187, upload-time = "2026-08-19T03:12:37.565Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/01/9b8b983786e3ce34924e372e8b76b92b508273ab65c589fc7e88cc03ee17/ty-0.0.73-py3-none-win_amd64.whl", hash = "sha256:cedd05122ded0b5dcc55431a370e974b747f99c41c290a3d2ab8c1867f197519", size = 12693838, upload-time = "2026-08-19T03:12:39.483Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/88/25333bbfea6a5dc064371d2002d3d4807db90b84d5448f9106b2712b0fbc/ty-0.0.73-py3-none-win_arm64.whl", hash = "sha256:e47068f8369dea5d641a26a2ad0a947a320b02ff87099b07e95de0323245a4dc", size = 12443573, upload-time = "2026-08-19T03:12:41.449Z" },
]
[[package]]