Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FunctionModel for handling functions #1

Closed
wants to merge 12 commits into from
Closed

Conversation

JoshuaLeivers
Copy link
Owner

@JoshuaLeivers JoshuaLeivers commented Sep 4, 2023

Change Summary

Adds a wrapper model that uses a Pydantic model to validate function arguments and return values, exposing functions to pass in and retrieve arguments, and to call functions using the stored arguments.

Related issue number

This is a feature that has been asked for previously in pydantic#1391 but which was possible through v1 methods which are not present in v2 and so that issue is already closed.

Checklist

  • The pull request title is a good summary of the changes - it will be used in the changelog
  • Unit tests for the changes exist
  • Tests pass on CI
  • Documentation reflects the changes where applicable
  • My PR is ready to review, please add a comment including the phrase "please review" to assign reviewers

@JoshuaLeivers JoshuaLeivers added the enhancement New feature or request label Sep 4, 2023
@JoshuaLeivers JoshuaLeivers force-pushed the function-model branch 2 times, most recently from eac2e78 to 32ff1ce Compare September 4, 2023 12:44
@JoshuaLeivers JoshuaLeivers marked this pull request as ready for review September 4, 2023 12:45
@@ -0,0 +1,442 @@
"""Provides FunctionModel, a model for validating function arguments."""

from collections import OrderedDict

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC all Python dicts are guaranteed to be ordered since Python 3.6 and pydantic 2 supports only Python 3.7+.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced it, good point

pydantic/function_model.py Outdated Show resolved Hide resolved
_keyword_total: int
_positional_total: int

T = TypeVar('T')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably best to have this TypeVar outside of the class.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


params[name] = (
get_annotation_type(param.annotation),
param.default if param.default != Parameter.empty else ...,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this won't work if the default value is ....

Copy link
Owner Author

@JoshuaLeivers JoshuaLeivers Sep 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got the impression somewhere (possibly here, I'm not sure) that ... was the default value of field defaults. From looking further at that class, it seems they use PydanticUndefined, which I have changed it to and works just as well with the current tests. I can't find any documentation as to what exactly I should use here, but hopefully if there is an issue then it will get pointed out once we have an upstream PR - I'll make sure to ask explicitly when the time comes so it's not missed.

Comment on lines 80 to 84
config = (
ConfigDict(extra='allow', validate_assignment=True, validate_default=True)
if has_kwargs
else ConfigDict(extra='forbid', validate_assignment=True, validate_default=True)
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
config = (
ConfigDict(extra='allow', validate_assignment=True, validate_default=True)
if has_kwargs
else ConfigDict(extra='forbid', validate_assignment=True, validate_default=True)
)
config = ConfigDict(extra='allow' if has_kwargs else 'forbid', validate_assignment=True, validate_default=True)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

Comment on lines 167 to 164
if key == 'args':
# Values within *args are unrestricted
return value
elif key == 'kwargs':
# Values within **kwargs are unrestricted
return value

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if key == 'args':
# Values within *args are unrestricted
return value
elif key == 'kwargs':
# Values within **kwargs are unrestricted
return value
if key in {'args', 'kwargs'}:
# Values within *args and **kwargs are unrestricted
return value

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, I believe at some point I had it where the two keys were doing something different and I just forgot to reduce it down.

Comment on lines 174 to 176
field = self._model.model_fields.get(key)
assert field is not None # This should always be the case, but Python complains otherwise
return TypeAdapter(get_annotation_type(field.annotation)).validate_python(value)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
field = self._model.model_fields.get(key)
assert field is not None # This should always be the case, but Python complains otherwise
return TypeAdapter(get_annotation_type(field.annotation)).validate_python(value)
return TypeAdapter(get_annotation_type(self._model.model_fields[key].annotation)).validate_python(value)

?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment on lines 244 to 253
if pos >= len(param_keys):
if has_args:
in_args = True
else:
error = unexp_error # Unknown positional parameter and *args is not present
elif not self._parameters[param_keys[pos]]['positional']:
if has_args:
in_args = True
else:
error = unexp_error # Keyword-only parameter, so can't set via position

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if pos >= len(param_keys):
if has_args:
in_args = True
else:
error = unexp_error # Unknown positional parameter and *args is not present
elif not self._parameters[param_keys[pos]]['positional']:
if has_args:
in_args = True
else:
error = unexp_error # Keyword-only parameter, so can't set via position
if pos >= len(param_keys) or not self._parameters[param_keys[pos]]['positional']:
if has_args:
in_args = True
else:
error = unexp_error

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

param = params[key]
param['set_keyword'] = True if param['keyword'] else False

if len(errors) > 0:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if len(errors) > 0:
if errors:

pytest.skip('These tests use positional-only parameters in functions, which are not supported prior to Python 3.8')


"""def test_only_pos(): # noqa: C901

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this string?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were just old tests that I had commented out while I was working on replacing them. Forgot to remove them, but have removed them now.

JoshuaLeivers and others added 11 commits September 21, 2023 14:18
This model type uses a Pydantic model to validate function
arguments and return values, exposing functions to pass in and
retrieve arguments, and to call functions using the stored
arguments.

This is a feature that has been asked for previously in
pydantic#1391 but which was possible through v1 methods
which are not present in v2 and so that issue is already closed.
OrderedDict is unnecessary here, as dicts are ordered by default
in all Python versions that Pydantic supports (>=3.7).

Co-authored-by: Alexander Khabarov <alexander.khabarov@arm.com>
Some old tests were left in the module by mistake within a comment
string.
This may be a more appropriate value than the previous, which was
Python's ellipsis.
Co-authored-by: Alexander Khabarov <alexander.khabarov@arm.com>
Co-authored-by: Alexander Khabarov <alexander.khabarov@arm.com>
The new format functions the same, but is a bit shorter and more
readable.

Co-authored-by: Alexander Khabarov <alexander.khabarov@arm.com>
Co-authored-by: Alexander Khabarov <alexander.khabarov@arm.com>
Co-authored-by: Alexander Khabarov <alexander.khabarov@arm.com>
Co-authored-by: Alexander Khabarov <alexander.khabarov@arm.com>
Co-authored-by: Alexander Khabarov <alexander.khabarov@arm.com>
`get_signature` allows developers to directly access the
function signature of the model, which may be useful in situations
where their code has access to the model itself but not the
function.

Also, `FunctionModel` could not previously be imported via
`pydantic.FunctionModel`, which has been fixed.
@mattyclarkson
Copy link

Function call validation is implemented upstream here. We can close this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants