Skip to content

Commit 2ad06fe

Browse files
authored
Merge pull request #383 from reagento/feature/impoved-errors
Complete rework of error messages
2 parents 94bde21 + 592680e commit 2ad06fe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+761
-838
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Remove ``hide_traceback`` parameter of ``Retort`` (it is also removed from ``Retort.replace`` method).
2+
Now, you can control rendering error via ``error_renderer``. Yo can pass ``None`` to show python ``ExceptionGroup``
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Fix error hint generation for model conversion
1+
Fix error at hint generation for model conversion
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
Completely rework error rendering.
2+
Now, all errors of loader, dumper and converter generation uses new, compact and clear display mode.
3+
Also, many error texts are improved.
4+
5+
.. code-block:: text
6+
:caption: Old error example
7+
8+
| adaptix.AggregateCannotProvide: Cannot create loader for model. Loaders for some fields cannot be created (1 sub-exception)
9+
| Location: `Book`
10+
+-+---------------- 1 ----------------
11+
| adaptix.AggregateCannotProvide: Cannot create loader for model. Cannot fetch InputNameLayout (1 sub-exception)
12+
| Location: `Book.author: Person`
13+
+-+---------------- 1 ----------------
14+
| adaptix.CannotProvide: Required fields ['last_name'] are skipped
15+
| Location: `Book.author: Person`
16+
+------------------------------------
17+
18+
The above exception was the direct cause of the following exception:
19+
20+
Traceback (most recent call last):
21+
...
22+
adaptix.ProviderNotFoundError: Cannot produce loader for type <class '__main__.Book'>
23+
Note: The attached exception above contains verbose description of the problem
24+
25+
26+
.. code-block:: text
27+
:caption: New error example
28+
29+
Traceback (most recent call last):
30+
...
31+
adaptix.ProviderNotFoundError: Cannot produce loader for type <class '__main__.Book'>
32+
× Cannot create loader for model. Loaders for some fields cannot be created
33+
│ Location: ‹Book›
34+
╰──▷ Cannot create loader for model. Cannot fetch `InputNameLayout`
35+
│ Location: ‹Book.author: Person›
36+
╰──▷ Required fields ['last_name'] are skipped
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Now, ``Retort.replace`` method takes ``Omitted`` to skip value instead of ``None``.
Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
| adaptix.AggregateCannotProvide: Cannot create loader for model. Cannot fetch InputNameLayout (1 sub-exception)
2-
| Location: `User`
3-
+-+---------------- 1 ----------------
4-
| adaptix.CannotProvide: Required fields ['password_hash'] are skipped
5-
| Location: `User`
6-
+------------------------------------
7-
8-
The above exception was the direct cause of the following exception:
9-
101
Traceback (most recent call last):
112
...
12-
adaptix.NoSuitableProvider: Cannot produce loader for type <class 'docs.examples.extended_usage.fields_filtering_only.User'>
13-
Note: The attached exception above contains verbose description of the problem
3+
adaptix.ProviderNotFoundError: Cannot produce loader for type <class '__main__.User'>
4+
× Cannot create loader for model. Cannot fetch `InputNameLayout`
5+
│ Location: ‹User›
6+
╰──▷ Required fields ['password_hash'] are skipped
Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
| adaptix.AggregateCannotProvide: Cannot create loader for model. Cannot fetch InputNameLayout (1 sub-exception)
2-
| Location: `User`
3-
+-+---------------- 1 ----------------
4-
| adaptix.CannotProvide: Required fields ['password_hash'] are skipped
5-
| Location: `User`
6-
+------------------------------------
7-
8-
The above exception was the direct cause of the following exception:
9-
101
Traceback (most recent call last):
112
...
12-
adaptix.NoSuitableProvider: Cannot produce loader for type <class 'docs.examples.extended_usage.fields_filtering_skip.User'>
13-
Note: The attached exception above contains verbose description of the problem
3+
adaptix.ProviderNotFoundError: Cannot produce loader for type <class '__main__.User'>
4+
× Cannot create loader for model. Cannot fetch `InputNameLayout`
5+
│ Location: ‹User›
6+
╰──▷ Required fields ['password_hash'] are skipped

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ line-length = 120
122122
output-format = "concise"
123123

124124
[tool.ruff.lint]
125+
126+
allowed-confusables = ['×', '', '']
127+
125128
select = ['ALL']
126129
fixable = [
127130
'Q000',
@@ -177,7 +180,7 @@ ignore = [
177180
[tool.ruff.lint.per-file-ignores]
178181
"__init__.py" = ['F401']
179182

180-
"test_*" = ['S101', 'PLR2004', 'PLC0105', 'N806', 'FA102', 'UP035', 'UP006']
183+
"test_*" = ['S101', 'PLR2004', 'PLC0105', 'N806', 'FA102', 'UP035', 'UP006', 'E501']
181184
"tests/*/local_helpers.py" = ['S101', 'PLR2004', 'PLC0105', 'N806', 'FA102']
182185
"tests/*/data_*.py" = ['F821']
183186
"tests/tests_helpers/*" = ['INP001', 'S101']

src/adaptix/_internal/conversion/facade/retort.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,6 @@ def _calculate_derived(self) -> None:
6464
super()._calculate_derived()
6565
self._simple_converter_cache: dict[tuple[TypeHint, TypeHint, Optional[str]], Converter] = {}
6666

67-
def replace(self: AR, *, hide_traceback: Optional[bool] = None) -> AR:
68-
with self._clone() as clone:
69-
if hide_traceback is not None:
70-
clone._hide_traceback = hide_traceback
71-
return clone
72-
7367
def extend(self: AR, *, recipe: Iterable[Provider]) -> AR:
7468
with self._clone() as clone:
7569
clone._instance_recipe = (
@@ -187,7 +181,7 @@ def convert(self, src_obj: Any, dst: type[DstT], *, recipe: Iterable[Provider] =
187181
src = type(src_obj)
188182
if is_generic_class(src):
189183
raise ValueError(
190-
f"Can not infer the actual type of generic class instance ({src!r}),"
184+
f"Cannot infer the actual type of generic class instance ({src!r}),"
191185
" you have to use `get_converter` explicitly passing the type of object",
192186
)
193187

src/adaptix/_internal/conversion/linking_provider.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import itertools
22
from collections.abc import Iterable, Mapping
3-
from typing import Callable, NoReturn, Optional, TypeVar, Union
3+
from typing import Callable, Optional, TypeVar, Union
44

55
from ..common import Coercer, OneArgCoercer, VarTuple
66
from ..model_tools.definitions import DefaultFactory, DefaultValue, InputField, InputShape, Param, ParamKind
77
from ..model_tools.introspection.callable import get_callable_shape
88
from ..provider.essential import CannotProvide, Mediator, mandatory_apply_by_iterable
9-
from ..provider.fields import input_field_to_loc
109
from ..provider.loc_stack_filtering import LocStackChecker
11-
from ..provider.loc_stack_tools import format_loc_stack
10+
from ..provider.loc_stack_tools import format_loc_stack, get_callable_name
1211
from ..provider.location import FieldLoc
1312
from ..utils import add_note
1413
from .provider_template import LinkingProvider
@@ -99,26 +98,26 @@ def _get_linking(
9998
try:
10099
source = name_to_field_source[param.name]
101100
except KeyError:
102-
self._raise_link_error(request, input_field, "Cannot match `{}` with model field")
101+
raise CannotProvide(
102+
f"Cannot match function parameter ‹{input_field.id}› with any model field",
103+
is_terminal=True,
104+
is_demonstrative=True,
105+
)
103106
else:
104107
if idx == 0:
105108
return LinkingResult(linking=ModelLinking())
106109

107110
try:
108111
source = name_to_context_source[param.name]
109112
except KeyError:
110-
self._raise_link_error(request, input_field, "Cannot match `{}` with converter parameter")
113+
raise CannotProvide(
114+
f"Cannot match function parameter ‹{input_field.id}› with any converter parameter",
115+
is_terminal=True,
116+
is_demonstrative=True,
117+
)
111118

112119
return LinkingResult(linking=FieldLinking(source=source, coercer=None))
113120

114-
def _raise_link_error(self, request: LinkingRequest, input_field: InputField, template: str) -> NoReturn:
115-
dest = request.destination.append_with(input_field_to_loc(input_field).complement_with_func(self._func))
116-
raise CannotProvide(
117-
template.format(format_loc_stack(dest)),
118-
is_terminal=True,
119-
is_demonstrative=True,
120-
) from None
121-
122121
def _create_param_specs(
123122
self,
124123
mediator: Mediator,
@@ -161,13 +160,16 @@ def _provide_linking(self, mediator: Mediator, request: LinkingRequest) -> Linki
161160
param_specs = self._create_param_specs(
162161
mediator,
163162
request,
164-
lambda: "Cannot create linking for function. Linkings for some parameters are not found",
163+
lambda: (
164+
f"Cannot create linking for function ‹{get_callable_name(self._func)}›."
165+
f" Linkings for some parameters are not found"
166+
),
165167
)
166168
except CannotProvide as e:
167169
if len(request.sources) > 0:
168170
src_desc = format_loc_stack(request.sources[0].reversed_slice(1))
169171
dst_desc = format_loc_stack(request.destination)
170-
add_note(e, f"Linking: `{src_desc} => {dst_desc}`")
172+
add_note(e, f"Linking: {src_desc} ──▷ {dst_desc}")
171173
raise
172174

173175
return LinkingResult(

src/adaptix/_internal/conversion/model_coercer_provider.py

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from collections.abc import Iterable, Mapping
1+
import sys
2+
from collections.abc import Iterable, Mapping, Sequence
23
from inspect import Parameter, Signature
3-
from typing import Callable, Optional, Union
4+
from typing import Callable, Optional
45

56
from ..code_tools.compiler import BasicClosureCompiler, ClosureCompiler
67
from ..code_tools.name_sanitizer import BuiltinNameSanitizer, NameSanitizer
7-
from ..common import Coercer
8+
from ..common import Coercer, TypeHint
89
from ..conversion.broaching.code_generator import BroachingCodeGenerator, BroachingPlan, BuiltinBroachingCodeGenerator
910
from ..conversion.broaching.definitions import (
1011
AccessorElement,
@@ -27,12 +28,14 @@
2728
ModelLinking,
2829
UnlinkedOptionalPolicyRequest,
2930
)
31+
from ..feature_requirement import HAS_PY_310
3032
from ..model_tools.definitions import DefaultValue, InputField, InputShape, OutputShape, ParamKind, create_key_accessor
3133
from ..morphing.model.basic_gen import compile_closure_with_globals_capturing, fetch_code_gen_hook
3234
from ..provider.essential import AggregateCannotProvide, CannotProvide, Mediator, mandatory_apply_by_iterable
3335
from ..provider.fields import input_field_to_loc, output_field_to_loc
3436
from ..provider.loc_stack_filtering import LocStack
35-
from ..provider.location import AnyLoc, InputFieldLoc, InputFuncFieldLoc, OutputFieldLoc
37+
from ..provider.loc_stack_tools import format_loc_stack, format_type, get_callable_name
38+
from ..provider.location import AnyLoc, OutputFieldLoc
3639
from ..provider.shape_provider import InputShapeRequest, OutputShapeRequest, provide_generic_resolved_shape
3740
from ..utils import add_note
3841
from .provider_template import CoercerProvider
@@ -53,7 +56,7 @@ def _provide_coercer(self, mediator: Mediator, request: CoercerRequest) -> Coerc
5356
return self._make_coercer(mediator, request, broaching_plan)
5457

5558
def _fetch_shapes(self, mediator: Mediator, request: CoercerRequest) -> tuple[InputShape, OutputShape]:
56-
exception_and_type_list = []
59+
exception_and_type_list: list[tuple[CannotProvide, LocStack]] = []
5760
try:
5861
dst_shape = self._fetch_dst_shape(mediator, request.dst)
5962
except CannotProvide as e:
@@ -67,17 +70,31 @@ def _fetch_shapes(self, mediator: Mediator, request: CoercerRequest) -> tuple[In
6770
if len(exception_and_type_list) == 1:
6871
raise CannotProvide(
6972
parent_notes_gen=lambda: [
70-
f"Hint: Class `{self._loc_stack_to_view_string(exception_and_type_list[0][1])}` is not recognized as model."
73+
f"Hint: Type {format_type(exception_and_type_list[0][1].last.type)} is not recognized as model."
7174
" Did your forget `@dataclass` decorator? Check documentation what model kinds are supported",
7275
],
7376
)
7477
if len(exception_and_type_list) == 2: # noqa: PLR2004
7578
raise AggregateCannotProvide(
76-
"Classes are not recognized as models",
79+
"Types are not recognized as models",
7780
[exc for exc, tp in exception_and_type_list],
81+
parent_notes_gen=lambda: self._types_are_not_models_parent_notes_gen(
82+
loc_stack.last.type for exc, loc_stack in exception_and_type_list
83+
),
7884
)
7985
return dst_shape, src_shape
8086

87+
def _types_are_not_models_parent_notes_gen(self, types: Iterable[TypeHint]) -> Sequence[str]:
88+
if (
89+
HAS_PY_310
90+
and all(getattr(tp, "__module__", None) not in sys.stdlib_module_names for tp in types)
91+
):
92+
return [
93+
"Hint: Types are not recognized as models."
94+
" Did your forget `@dataclass` decorator? Check documentation what model kinds are supported",
95+
]
96+
return []
97+
8198
def _make_coercer(
8299
self,
83100
mediator: Mediator,
@@ -190,7 +207,6 @@ def _generate_field_linking_to_sub_plan(
190207
self,
191208
mediator: Mediator,
192209
request: CoercerRequest,
193-
loc: Union[InputFieldLoc, InputFuncFieldLoc],
194210
linking: FieldLinking,
195211
) -> BroachingPlan:
196212
if linking.coercer is not None:
@@ -200,7 +216,7 @@ def _generate_field_linking_to_sub_plan(
200216
CoercerRequest(
201217
src=linking.source,
202218
ctx=request.ctx,
203-
dst=request.dst.append_with(loc),
219+
dst=request.dst,
204220
),
205221
)
206222

@@ -287,7 +303,9 @@ def generate_sub_plan(input_field: InputField, linking_result: LinkingResult):
287303
if isinstance(linking_result.linking, FunctionLinking):
288304
return self._generate_function_linking_to_sub_plan(
289305
mediator=mediator,
290-
request=request,
306+
request=request.append_dst_loc(
307+
input_field_to_loc(input_field),
308+
),
291309
linking=linking_result.linking,
292310
)
293311
if isinstance(linking_result.linking, ModelLinking):
@@ -299,21 +317,31 @@ def generate_sub_plan(input_field: InputField, linking_result: LinkingResult):
299317
field_loc = input_field_to_loc(input_field)
300318
return self._generate_field_linking_to_sub_plan(
301319
mediator=mediator,
302-
request=request,
303-
loc=field_loc.complement_with_func(parent_func) if parent_func is not None else field_loc,
320+
request=request.append_dst_loc(
321+
field_loc.complement_with_func(parent_func) if parent_func is not None else field_loc,
322+
),
304323
linking=linking_result.linking,
305324
)
306325
raise TypeError
307326

308-
field_sub_plans = mandatory_apply_by_iterable(
309-
generate_sub_plan,
310-
field_linkings,
311-
lambda: (
312-
"Cannot create coercer for models. Coercers for some linkings are not found"
313-
if parent_func is None else
314-
"Cannot create coercer for model and function. Coercers for some linkings are not found"
315-
),
316-
)
327+
try:
328+
field_sub_plans = mandatory_apply_by_iterable(
329+
generate_sub_plan,
330+
field_linkings,
331+
lambda: (
332+
"Cannot create coercer for models. Coercers for some linkings are not found"
333+
if parent_func is None else
334+
f"Cannot create coercer for model and function ‹{get_callable_name(parent_func)}›."
335+
f" Coercers for some linkings are not found"
336+
),
337+
)
338+
except CannotProvide as e:
339+
if parent_func is not None:
340+
src_desc = format_loc_stack(request.src)
341+
dst_desc = format_loc_stack(request.dst)
342+
add_note(e, f"Linking: {src_desc} ──▷ {dst_desc}")
343+
raise
344+
317345
return {
318346
dst_field: sub_plan
319347
for (dst_field, linking), sub_plan in zip(field_linkings, field_sub_plans)
@@ -368,7 +396,7 @@ def _make_constructor_call(
368396
args.append(KeywordArg(param.name, sub_plan))
369397
elif param.kind == ParamKind.POS_ONLY and has_skipped_params:
370398
raise CannotProvide(
371-
"Can not generate consistent constructor call,"
399+
"Cannot generate consistent constructor call,"
372400
" positional-only parameter is skipped",
373401
is_demonstrative=True,
374402
)

0 commit comments

Comments
 (0)