refactor: replace get_message_template match with dict lookup (#1165)

## ℹ️ Description

Replace the ~115-arm match/case in `__get_message_template` with a
module-level `_MESSAGE_TEMPLATES` dict of raw English strings plus a
3-line lazy `_(template)` lookup function.

- **Motivation**: Eliminate 5 complexity `noqa` annotations (C901,
PLR0911, PLR0912, PLR0915, E701) by removing the real architectural
complexity — a pure flat error-code → message mapping that has zero
branching or logic.
- **Approach**: A `dict[str, str]` of raw English strings (no `_()`
calls at import time) with lazy translation inside
`__get_message_template`. `"custom_error"` intentionally omitted —
handled by `.get()` default.
- **Translation**: Caller function name unchanged → translation keys
under `__get_message_template` preserved. AST-based translation test
extractor updated to recognize `_MESSAGE_TEMPLATES` dict values.

## 📋 Changes Summary

- `src/kleinanzeigen_bot/utils/pydantics.py`: Replaced ~115 match arms
with `_MESSAGE_TEMPLATES` dict + 3-line function; removed all 5 noqa
annotations
- `tests/unit/test_translations.py`: Added AST extraction for
`_MESSAGE_TEMPLATES` dict values to maintain translation coverage checks

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ]  New feature (adds new functionality without breaking existing
usage)
- [ ] 💥 Breaking change (changes that might break existing user setups,
scripts, or configurations)

##  Checklist
Before requesting a review, confirm the following:
- [x] I have reviewed my changes to ensure they meet the project's
standards.
- [x] I have tested my changes and ensured that all tests pass (`pdm run
test`).
- [x] I have formatted the code (`pdm run format`).
- [x] I have verified that linting passes (`pdm run lint`).
- [x] I have updated documentation where necessary.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Improved error message handling system to support better
internationalization and translation workflows

* **Tests**
* Updated test suite to extract and verify translatable message strings

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Jens
2026-06-22 07:12:23 +02:00
committed by GitHub
parent 32c6d3985f
commit eaa646874d
2 changed files with 138 additions and 120 deletions

View File

@@ -89,123 +89,123 @@ def format_validation_error(ex:ValidationError) -> str:
return "\n".join(lines) return "\n".join(lines)
def __get_message_template(error_code:str) -> str | None: # noqa: C901 # generated/static pydantic error-code mapping # Mapping of pydantic error codes -> raw English message templates.
# https://github.com/pydantic/pydantic-core/blob/d03bf4a01ca3b378cc8590bd481f307e82115bc6/src/errors/types.rs#L477 # "custom_error" intentionally omitted; caller falls back to Pydantic's message.
# ruff: noqa: PLR0911 Too many return statements # Strings are NOT wrapped with _() here — translation happens lazily in
# ruff: noqa: PLR0912 Too many branches # __get_message_template to avoid freezing the locale at import time.
# ruff: noqa: PLR0915 Too many statements # See https://github.com/pydantic/pydantic-core/blob/main/src/errors/types.rs
# ruff: noqa: E701 Multiple statements on one line (colon) _MESSAGE_TEMPLATES:dict[str, str] = {
match error_code: "no_such_attribute": "Object has no attribute '{attribute}'",
case "no_such_attribute": return _("Object has no attribute '{attribute}'") "json_invalid": "Invalid JSON: {error}",
case "json_invalid": return _("Invalid JSON: {error}") "json_type": "JSON input should be string, bytes or bytearray",
case "json_type": return _("JSON input should be string, bytes or bytearray") "needs_python_object": "Cannot check `{method_name}` when validating from json, use a JsonOrPython validator instead",
case "needs_python_object": return _("Cannot check `{method_name}` when validating from json, use a JsonOrPython validator instead") "recursion_loop": "Recursion error - cyclic reference detected",
case "recursion_loop": return _("Recursion error - cyclic reference detected") "missing": "Field required",
case "missing": return _("Field required") "frozen_field": "Field is frozen",
case "frozen_field": return _("Field is frozen") "frozen_instance": "Instance is frozen",
case "frozen_instance": return _("Instance is frozen") "extra_forbidden": "Extra inputs are not permitted",
case "extra_forbidden": return _("Extra inputs are not permitted") "invalid_key": "Keys should be strings",
case "invalid_key": return _("Keys should be strings") "get_attribute_error": "Error extracting attribute: {error}",
case "get_attribute_error": return _("Error extracting attribute: {error}") "model_type": "Input should be a valid dictionary or instance of {class_name}",
case "model_type": return _("Input should be a valid dictionary or instance of {class_name}") "model_attributes_type": "Input should be a valid dictionary or object to extract fields from",
case "model_attributes_type": return _("Input should be a valid dictionary or object to extract fields from") "dataclass_type": "Input should be a dictionary or an instance of {class_name}",
case "dataclass_type": return _("Input should be a dictionary or an instance of {class_name}") "dataclass_exact_type": "Input should be an instance of {class_name}",
case "dataclass_exact_type": return _("Input should be an instance of {class_name}") "none_required": "Input should be None",
case "none_required": return _("Input should be None") "greater_than": "Input should be greater than {gt}",
case "greater_than": return _("Input should be greater than {gt}") "greater_than_equal": "Input should be greater than or equal to {ge}",
case "greater_than_equal": return _("Input should be greater than or equal to {ge}") "less_than": "Input should be less than {lt}",
case "less_than": return _("Input should be less than {lt}") "less_than_equal": "Input should be less than or equal to {le}",
case "less_than_equal": return _("Input should be less than or equal to {le}") "multiple_of": "Input should be a multiple of {multiple_of}",
case "multiple_of": return _("Input should be a multiple of {multiple_of}") "finite_number": "Input should be a finite number",
case "finite_number": return _("Input should be a finite number") "too_short": "{field_type} should have at least {min_length} item{expected_plural} after validation, not {actual_length}",
case "too_short": return _("{field_type} should have at least {min_length} item{expected_plural} after validation, not {actual_length}") "too_long": "{field_type} should have at most {max_length} item{expected_plural} after validation, not {actual_length}",
case "too_long": return _("{field_type} should have at most {max_length} item{expected_plural} after validation, not {actual_length}") "iterable_type": "Input should be iterable",
case "iterable_type": return _("Input should be iterable") "iteration_error": "Error iterating over object, error: {error}",
case "iteration_error": return _("Error iterating over object, error: {error}") "string_type": "Input should be a valid string",
case "string_type": return _("Input should be a valid string") "string_sub_type": "Input should be a string, not an instance of a subclass of str",
case "string_sub_type": return _("Input should be a string, not an instance of a subclass of str") "string_unicode": "Input should be a valid string, unable to parse raw data as a unicode string",
case "string_unicode": return _("Input should be a valid string, unable to parse raw data as a unicode string") "string_too_short": "String should have at least {min_length} character{expected_plural}",
case "string_too_short": return _("String should have at least {min_length} character{expected_plural}") "string_too_long": "String should have at most {max_length} character{expected_plural}",
case "string_too_long": return _("String should have at most {max_length} character{expected_plural}") "string_pattern_mismatch": "String should match pattern '{pattern}'",
case "string_pattern_mismatch": return _("String should match pattern '{pattern}'") "enum": "Input should be {expected}",
case "enum": return _("Input should be {expected}") "dict_type": "Input should be a valid dictionary",
case "dict_type": return _("Input should be a valid dictionary") "mapping_type": "Input should be a valid mapping, error: {error}",
case "mapping_type": return _("Input should be a valid mapping, error: {error}") "list_type": "Input should be a valid list",
case "list_type": return _("Input should be a valid list") "tuple_type": "Input should be a valid tuple",
case "tuple_type": return _("Input should be a valid tuple") "set_type": "Input should be a valid set",
case "set_type": return _("Input should be a valid set") "set_item_not_hashable": "Set items should be hashable",
case "set_item_not_hashable": return _("Set items should be hashable") "bool_type": "Input should be a valid boolean",
case "bool_type": return _("Input should be a valid boolean") "bool_parsing": "Input should be a valid boolean, unable to interpret input",
case "bool_parsing": return _("Input should be a valid boolean, unable to interpret input") "int_type": "Input should be a valid integer",
case "int_type": return _("Input should be a valid integer") "int_parsing": "Input should be a valid integer, unable to parse string as an integer",
case "int_parsing": return _("Input should be a valid integer, unable to parse string as an integer") "int_from_float": "Input should be a valid integer, got a number with a fractional part",
case "int_from_float": return _("Input should be a valid integer, got a number with a fractional part") "int_parsing_size": "Unable to parse input string as an integer, exceeded maximum size",
case "int_parsing_size": return _("Unable to parse input string as an integer, exceeded maximum size") "float_type": "Input should be a valid number",
case "float_type": return _("Input should be a valid number") "float_parsing": "Input should be a valid number, unable to parse string as a number",
case "float_parsing": return _("Input should be a valid number, unable to parse string as a number") "bytes_type": "Input should be a valid bytes",
case "bytes_type": return _("Input should be a valid bytes") "bytes_too_short": "Data should have at least {min_length} byte{expected_plural}",
case "bytes_too_short": return _("Data should have at least {min_length} byte{expected_plural}") "bytes_too_long": "Data should have at most {max_length} byte{expected_plural}",
case "bytes_too_long": return _("Data should have at most {max_length} byte{expected_plural}") "bytes_invalid_encoding": "Data should be valid {encoding}: {encoding_error}",
case "bytes_invalid_encoding": return _("Data should be valid {encoding}: {encoding_error}") "value_error": "Value error, {error}",
case "value_error": return _("Value error, {error}") "assertion_error": "Assertion failed, {error}",
case "assertion_error": return _("Assertion failed, {error}") # "custom_error" omitted intentionally — caller falls back to Pydantic's own message
case "custom_error": return None # handled separately "literal_error": "Input should be {expected}",
case "literal_error": return _("Input should be {expected}") "date_type": "Input should be a valid date",
case "date_type": return _("Input should be a valid date") "date_parsing": "Input should be a valid date in the format YYYY-MM-DD, {error}",
case "date_parsing": return _("Input should be a valid date in the format YYYY-MM-DD, {error}") "date_from_datetime_parsing": "Input should be a valid date or datetime, {error}",
case "date_from_datetime_parsing": return _("Input should be a valid date or datetime, {error}") "date_from_datetime_inexact": "Datetimes provided to dates should have zero time - e.g. be exact dates",
case "date_from_datetime_inexact": return _("Datetimes provided to dates should have zero time - e.g. be exact dates") "date_past": "Date should be in the past",
case "date_past": return _("Date should be in the past") "date_future": "Date should be in the future",
case "date_future": return _("Date should be in the future") "time_type": "Input should be a valid time",
case "time_type": return _("Input should be a valid time") "time_parsing": "Input should be in a valid time format, {error}",
case "time_parsing": return _("Input should be in a valid time format, {error}") "datetime_type": "Input should be a valid datetime",
case "datetime_type": return _("Input should be a valid datetime") "datetime_parsing": "Input should be a valid datetime, {error}",
case "datetime_parsing": return _("Input should be a valid datetime, {error}") "datetime_object_invalid": "Invalid datetime object, got {error}",
case "datetime_object_invalid": return _("Invalid datetime object, got {error}") "datetime_from_date_parsing": "Input should be a valid datetime or date, {error}",
case "datetime_from_date_parsing": return _("Input should be a valid datetime or date, {error}") "datetime_past": "Input should be in the past",
case "datetime_past": return _("Input should be in the past") "datetime_future": "Input should be in the future",
case "datetime_future": return _("Input should be in the future") "timezone_naive": "Input should not have timezone info",
case "timezone_naive": return _("Input should not have timezone info") "timezone_aware": "Input should have timezone info",
case "timezone_aware": return _("Input should have timezone info") "timezone_offset": "Timezone offset of {tz_expected} required, got {tz_actual}",
case "timezone_offset": return _("Timezone offset of {tz_expected} required, got {tz_actual}") "time_delta_type": "Input should be a valid timedelta",
case "time_delta_type": return _("Input should be a valid timedelta") "time_delta_parsing": "Input should be a valid timedelta, {error}",
case "time_delta_parsing": return _("Input should be a valid timedelta, {error}") "frozen_set_type": "Input should be a valid frozenset",
case "frozen_set_type": return _("Input should be a valid frozenset") "is_instance_of": "Input should be an instance of {class}",
case "is_instance_of": return _("Input should be an instance of {class}") "is_subclass_of": "Input should be a subclass of {class}",
case "is_subclass_of": return _("Input should be a subclass of {class}") "callable_type": "Input should be callable",
case "callable_type": return _("Input should be callable") "union_tag_invalid": "Input tag '{tag}' found using {discriminator} does not match any of the expected tags: {expected_tags}",
case "union_tag_invalid": return _("Input tag '{tag}' found using {discriminator} does not match any of the expected tags: {expected_tags}") "union_tag_not_found": "Unable to extract tag using discriminator {discriminator}",
case "union_tag_not_found": return _("Unable to extract tag using discriminator {discriminator}") "arguments_type": "Arguments must be a tuple, list or a dictionary",
case "arguments_type": return _("Arguments must be a tuple, list or a dictionary") "missing_argument": "Missing required argument",
case "missing_argument": return _("Missing required argument") "unexpected_keyword_argument": "Unexpected keyword argument",
case "unexpected_keyword_argument": return _("Unexpected keyword argument") "missing_keyword_only_argument": "Missing required keyword only argument",
case "missing_keyword_only_argument": return _("Missing required keyword only argument") "unexpected_positional_argument": "Unexpected positional argument",
case "unexpected_positional_argument": return _("Unexpected positional argument") "missing_positional_only_argument": "Missing required positional only argument",
case "missing_positional_only_argument": return _("Missing required positional only argument") "multiple_argument_values": "Got multiple values for argument",
case "multiple_argument_values": return _("Got multiple values for argument") "url_type": "URL input should be a string or URL",
case "url_type": return _("URL input should be a string or URL") "url_parsing": "Input should be a valid URL, {error}",
case "url_parsing": return _("Input should be a valid URL, {error}") "url_syntax_violation": "Input violated strict URL syntax rules, {error}",
case "url_syntax_violation": return _("Input violated strict URL syntax rules, {error}") "url_too_long": "URL should have at most {max_length} character{expected_plural}",
case "url_too_long": return _("URL should have at most {max_length} character{expected_plural}") "url_scheme": "URL scheme should be {expected_schemes}",
case "url_scheme": return _("URL scheme should be {expected_schemes}") "uuid_type": "UUID input should be a string, bytes or UUID object",
case "uuid_type": return _("UUID input should be a string, bytes or UUID object") "uuid_parsing": "Input should be a valid UUID, {error}",
case "uuid_parsing": return _("Input should be a valid UUID, {error}") "uuid_version": "UUID version {expected_version} expected",
case "uuid_version": return _("UUID version {expected_version} expected") "decimal_type": "Decimal input should be an integer, float, string or Decimal object",
case "decimal_type": return _("Decimal input should be an integer, float, string or Decimal object") "decimal_parsing": "Input should be a valid decimal",
case "decimal_parsing": return _("Input should be a valid decimal") "decimal_max_digits": "Decimal input should have no more than {max_digits} digit{expected_plural} in total",
case "decimal_max_digits": return _("Decimal input should have no more than {max_digits} digit{expected_plural} in total") "decimal_max_places": "Decimal input should have no more than {decimal_places} decimal place{expected_plural}",
case "decimal_max_places": return _("Decimal input should have no more than {decimal_places} decimal place{expected_plural}") "decimal_whole_digits": "Decimal input should have no more than {whole_digits} digit{expected_plural} before the decimal point",
case "decimal_whole_digits": return _("Decimal input should have no more than {whole_digits} digit{expected_plural} before the decimal point") "complex_type": (
case "complex_type": "Input should be a valid python complex object, a number, or a valid complex string "
return _( "following the rules at https://docs.python.org/3/library/functions.html#complex"
"Input should be a valid python complex object, a number, or a valid complex string " ),
"following the rules at https://docs.python.org/3/library/functions.html#complex" "complex_str_parsing": (
) "Input should be a valid complex string following the rules at "
case "complex_str_parsing": "https://docs.python.org/3/library/functions.html#complex"
return _( ),
"Input should be a valid complex string following the rules at " }
"https://docs.python.org/3/library/functions.html#complex"
)
case _: def __get_message_template(error_code:str) -> str | None:
pass template = _MESSAGE_TEMPLATES.get(error_code)
return None return _(template) if template is not None else None

View File

@@ -157,6 +157,24 @@ def _extract_log_messages(file_path:str, exclude_debug:bool = False) -> MessageD
if msg: if msg:
add_message(function_name, msg) add_message(function_name, msg)
# Extract messages from _MESSAGE_TEMPLATES dict (used by __get_message_template)
for n in ast.walk(tree):
target_ids:list[str] = []
dict_value:ast.AST | None = None
if isinstance(n, ast.Assign):
target_ids = [t.id for t in n.targets if isinstance(t, ast.Name)]
dict_value = n.value
elif isinstance(n, ast.AnnAssign):
if isinstance(n.target, ast.Name):
target_ids = [n.target.id]
dict_value = n.value
if "_MESSAGE_TEMPLATES" not in target_ids or not isinstance(dict_value, ast.Dict):
continue
for val_node in dict_value.values:
msg = extract_string_constant(val_node)
if msg: # skips None values (e.g. custom_error)
add_message("__get_message_template", msg)
print(f"Messages: {len(messages)} in {file_path}") print(f"Messages: {len(messages)} in {file_path}")
return messages return messages