From a2690b8b68b87ae89e85c60a8f693acfc96e19d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Hovm=C3=B6ller?= Date: Fri, 17 Jan 2025 15:26:47 +0100 Subject: [PATCH] Fixed crash when specifying Field.parsed_data callback and the field was not editable. Also improved error messages for evaluation. --- iommi/evaluate.py | 21 +++++++++++-------- iommi/evaluate__tests.py | 40 +++++++++++++++++++++++++++++++------ iommi/form.py | 1 + iommi/form__tests.py | 10 ++++++++++ iommi/traversable.py | 19 +++++++++++++++++- iommi/traversable__tests.py | 6 ++---- 6 files changed, 78 insertions(+), 19 deletions(-) diff --git a/iommi/evaluate.py b/iommi/evaluate.py index 2874f3ad..0825edbc 100644 --- a/iommi/evaluate.py +++ b/iommi/evaluate.py @@ -51,7 +51,9 @@ def get_callable_description(c): return 'lambda found at: `{}`'.format(inspect.getsource(c).strip()) except OSError: # pragma: no cover pass - return f'`{c}`' + if isinstance(c, Namespace): + return f'`{c}`' + return f'{c.__module__}.{c.__name__}' def is_callable(v): @@ -71,14 +73,17 @@ def evaluate(func_or_value, *, __signature=None, __strict=False, __match_empty=T return func_or_value(**kwargs) if __strict: + arguments = '\n '.join(keys(kwargs)) + parameters = '\n '.join(inspect.getfullargspec(func_or_value)[0]) assert isinstance(func_or_value, Namespace) and 'call_target' not in func_or_value, ( - "Evaluating {} didn't resolve it into a value but strict mode was active, " - "the signature doesn't match the given parameters. " - "We had these arguments: {}".format( - get_callable_description(func_or_value), - ', '.join(keys(kwargs)), - ) - ) +f'''Evaluating {get_callable_description(func_or_value)} didn't resolve it into a value but strict mode was active. The signature doesn't match the given parameters. + + Possible inputs: + {arguments} + + Function inputs: + {parameters} +''') return func_or_value diff --git a/iommi/evaluate__tests.py b/iommi/evaluate__tests.py index 850087ad..ac89c2ad 100644 --- a/iommi/evaluate__tests.py +++ b/iommi/evaluate__tests.py @@ -171,10 +171,39 @@ def test_evaluate_strict(): with pytest.raises(AssertionError) as e: evaluate_strict(lambda foo: 1, bar=2, baz=4) - assert ( - str(e.value) - == "Evaluating lambda found at: `evaluate_strict(lambda foo: 1, bar=2, baz=4)` didn't resolve it into a value but strict mode was active, the signature doesn't match the given parameters. We had these arguments: bar, baz" - ) + expected = ''' +Evaluating lambda found at: `evaluate_strict(lambda foo: 1, bar=2, baz=4)` didn't resolve it into a value but strict mode was active. The signature doesn't match the given parameters. + + Possible inputs: + bar + baz + + Function inputs: + foo + ''' + assert str(e.value).strip() == expected.strip() + + assert evaluate_strict(lambda **_: 1, bar=2, baz=4) == 1 + + +def test_evaluate_strict_for_def(): + def bar(foo, **_): + return 1 + + with pytest.raises(AssertionError) as e: + evaluate_strict(bar, bar=2, baz=4) + + expected = ''' +Evaluating iommi.evaluate__tests.bar didn't resolve it into a value but strict mode was active. The signature doesn't match the given parameters. + + Possible inputs: + bar + baz + + Function inputs: + foo +''' + assert str(e.value).strip() == expected.strip() assert evaluate_strict(lambda **_: 1, bar=2, baz=4) == 1 @@ -193,8 +222,7 @@ def foo(a, b, c, *, bar, **kwargs): assert False # pragma: no cover description = get_callable_description(foo) - assert description.startswith('`.foo at') - assert description.endswith('`') + assert description == 'iommi.evaluate__tests.foo' def test_get_callable_description_nested_lambda(): diff --git a/iommi/form.py b/iommi/form.py index babb5db2..4c1edbc3 100644 --- a/iommi/form.py +++ b/iommi/form.py @@ -883,6 +883,7 @@ def bind_from_instance(self): if not self.editable: self.value = self.initial + self.parsed_data = MISSING else: self._read_raw_data() diff --git a/iommi/form__tests.py b/iommi/form__tests.py index 33257992..75164071 100644 --- a/iommi/form__tests.py +++ b/iommi/form__tests.py @@ -3930,3 +3930,13 @@ def test_required_truthy_bug(): assert form.actions.submit.iommi_name() == 'submit' assert 'genres' not in form.get_errors()['fields'] assert form.get_errors()['fields']['name'] == {'This field is required'} + + +def test_parsed_data_does_not_crash_on_non_editable(): + Form.create( + auto__model=Album, + fields__name=dict( + editable=False, + parsed_data=lambda **_: 1, + ) + ).bind(request=req('POST', **{'-submit': ''})) diff --git a/iommi/traversable.py b/iommi/traversable.py index 5250de54..a1528db0 100644 --- a/iommi/traversable.py +++ b/iommi/traversable.py @@ -1,5 +1,6 @@ import copy import functools +import inspect from typing import ( Any, Dict, @@ -10,6 +11,7 @@ evaluate_attrs, ) from iommi.base import ( + keys, NOT_BOUND_MESSAGE, items, ) @@ -262,7 +264,22 @@ def bind(self, *, parent=None, request=None): for k in get_special_evaluated_attributes(result): v = getattr(result, k) if is_callable(v) and not isinstance(v, type): - assert False, ('SpecialEvaluatedRefinable not evaluated', k, v, repr(result)) + arguments = '\n '.join(keys(result.iommi_evaluate_parameters())) + parameters = '\n '.join(inspect.getfullargspec(v)[0]) + assert False, f'''SpecialEvaluatedRefinable not evaluated + + Refinable name: + {k} + + Path: + {result.iommi_dunder_path} + + Possible inputs: + {arguments} + + Function inputs: + {parameters} +''' return result diff --git a/iommi/traversable__tests.py b/iommi/traversable__tests.py index 7d1b4c03..0c4736a1 100644 --- a/iommi/traversable__tests.py +++ b/iommi/traversable__tests.py @@ -493,10 +493,8 @@ def broken_callback(a): t.invoke_callback(broken_callback) actual = str(e.value) - assert actual.startswith( - 'TypeError when invoking callback `.broken_callback at 0x' - ) - assert actual.endswith('`.\nKeyword arguments:\n params\n request\n root\n traversable\n user') + expected = 'TypeError when invoking callback iommi.traversable__tests.broken_callback.\nKeyword arguments:\n params\n request\n root\n traversable\n user' + assert actual == expected def test_invoke_callback_transparent_type_error():