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

accept None as value for all optional fields #1307

Merged
merged 4 commits into from Apr 15, 2020

Conversation

PrettyWood
Copy link
Member

@PrettyWood PrettyWood commented Mar 13, 2020

Change Summary

All optional list fields with None as value should be valid except if there is a custom validation
to handle this case.

ℹ️ Test skipped on python 3.6

        if unused_constraints:
            raise ValueError(
>               f'On field "{field_name}" the following field constraints are set but not enforced: '
                f'{", ".join(unused_constraints)}. '
                f'\nFor more details see https://pydantic-docs.helpmanual.io/usage/schema/#unenforced-field-constraints'
            )
E           ValueError: On field "bar" the following field constraints are set but not enforced: min_items. 
E           For more details see https://pydantic-docs.helpmanual.io/usage/schema/#unenforced-field-constraints

Related issue number

closes #1295

Checklist

  • Unit tests for the changes exist
  • Tests pass on CI and coverage remains at 100%
  • Documentation reflects the changes where applicable
  • changes/<pull request or issue id>-<github username>.md file added describing change
    (see changes/README.md for details)

@PrettyWood PrettyWood force-pushed the fix/none-mix-max-items branch 3 times, most recently from 25cceab to 3eb1614 Compare March 13, 2020 03:45
@codecov
Copy link

codecov bot commented Mar 13, 2020

Codecov Report

Merging #1307 into master will increase coverage by 0.10%.
The diff coverage is 100.00%.

@@             Coverage Diff             @@
##           master     #1307      +/-   ##
===========================================
+ Coverage   99.89%   100.00%   +0.10%     
===========================================
  Files          21        21              
  Lines        3708      3737      +29     
  Branches      731       739       +8     
===========================================
+ Hits         3704      3737      +33     
+ Misses          2         0       -2     
+ Partials        2         0       -2     
Impacted Files Coverage Δ
pydantic/types.py 100.00% <100.00%> (ø)
pydantic/errors.py 100.00% <0.00%> (ø)
pydantic/fields.py 100.00% <0.00%> (ø)
pydantic/typing.py 100.00% <0.00%> (ø)
pydantic/decorator.py 100.00% <0.00%> (ø)
pydantic/validators.py 100.00% <0.00%> (ø)
pydantic/schema.py 100.00% <0.00%> (+0.55%) ⬆️
pydantic/utils.py 100.00% <0.00%> (+1.07%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 938335c...d226cc3. Read the comment docs.

@@ -705,6 +705,10 @@ def _apply_validators(
) -> 'ValidateReturn':
for validator in validators:
try:
# optional field set to None should be valid unless a custom validator handles this case
if validator.__name__ in DEFAULT_VALIDATORS_NAMES and v is None and not self.required:
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not really satisfied with the check on name. But the validators are wrapped so no easy way to directly check that. I'm open to proposals

Copy link
Member

Choose a reason for hiding this comment

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

I'm afraid I'm not happy with this. This is the most performance critical part of the whole of pydantic.

I think we can fix this bug without modifying this funciton.

Copy link
Member

Choose a reason for hiding this comment

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

Instead modify ConstrainedList to something like

# This types superclass should be List[T], but cython chokes on that...
class ConstrainedList(list):  # type: ignore
    # Needed for pydantic to detect that this is a list
    __origin__ = list
    __args__: List[Type[T]]  # type: ignore

    min_items: Optional[int] = None
    max_items: Optional[int] = None
    item_type: Type[T]  # type: ignore

    @classmethod
    def __get_validators__(cls) -> 'CallableGenerator':
        yield cls.list_length_validator

    @classmethod
    def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
        update_not_none(field_schema, minItems=cls.min_items, maxItems=cls.max_items)

    @classmethod
    def list_length_validator(cls, v: 'List[T]', field) -> Optional['List[T]']:
        if v is None and not field.required:
            return None

        v = list_validator(v)
        v_len = len(v)

        if cls.min_items is not None and v_len < cls.min_items:
            raise errors.ListMinLengthError(limit_value=cls.min_items)

        if cls.max_items is not None and v_len > cls.max_items:
            raise errors.ListMaxLengthError(limit_value=cls.max_items)

        return v

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm afraid I'm not happy with this. This is the most performance critical part of the whole of pydantic.

I think we can fix this bug without modifying this funciton.

I definitely understand ! I was not satisfied either. But I wanted to tackle the whole problem instead of just the ConstrainList one. As a matter of fact it seems it's not needed! Sorry for the overkill

@@ -0,0 +1 @@
fix : value None should be valid for all optional fields by default
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
fix : value None should be valid for all optional fields by default
Allow `None` as input to all optional fields

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks ! Just added list fields as the fix is scoped to ConstrainedList

@@ -705,6 +705,10 @@ def _apply_validators(
) -> 'ValidateReturn':
for validator in validators:
try:
# optional field set to None should be valid unless a custom validator handles this case
if validator.__name__ in DEFAULT_VALIDATORS_NAMES and v is None and not self.required:
Copy link
Member

Choose a reason for hiding this comment

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

I'm afraid I'm not happy with this. This is the most performance critical part of the whole of pydantic.

I think we can fix this bug without modifying this funciton.

@@ -705,6 +705,10 @@ def _apply_validators(
) -> 'ValidateReturn':
for validator in validators:
try:
# optional field set to None should be valid unless a custom validator handles this case
if validator.__name__ in DEFAULT_VALIDATORS_NAMES and v is None and not self.required:
Copy link
Member

Choose a reason for hiding this comment

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

Instead modify ConstrainedList to something like

# This types superclass should be List[T], but cython chokes on that...
class ConstrainedList(list):  # type: ignore
    # Needed for pydantic to detect that this is a list
    __origin__ = list
    __args__: List[Type[T]]  # type: ignore

    min_items: Optional[int] = None
    max_items: Optional[int] = None
    item_type: Type[T]  # type: ignore

    @classmethod
    def __get_validators__(cls) -> 'CallableGenerator':
        yield cls.list_length_validator

    @classmethod
    def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
        update_not_none(field_schema, minItems=cls.min_items, maxItems=cls.max_items)

    @classmethod
    def list_length_validator(cls, v: 'List[T]', field) -> Optional['List[T]']:
        if v is None and not field.required:
            return None

        v = list_validator(v)
        v_len = len(v)

        if cls.min_items is not None and v_len < cls.min_items:
            raise errors.ListMinLengthError(limit_value=cls.min_items)

        if cls.max_items is not None and v_len > cls.max_items:
            raise errors.ListMaxLengthError(limit_value=cls.max_items)

        return v

@@ -1025,3 +1026,21 @@ class FunctionModel(BaseModel):

m = FunctionModel()
assert m.uid is uuid4


@pytest.mark.skipif(sys.version_info < (3, 7), reason='field constraints are set but not enforced with 3.6 and above')
Copy link
Member

Choose a reason for hiding this comment

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

surely this can be removed then?

Copy link
Member Author

Choose a reason for hiding this comment

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

As a matter of fact it seems it can't :/
The problem lies here

    def go(type_: Any) -> Type[Any]:
        if is_literal_type(annotation) or isinstance(type_, ForwardRef) or lenient_issubclass(type_, ConstrainedList):
            return type_
        origin = getattr(type_, '__origin__', None)

where typing.List.__origin__ is list on python 3.7+ but None on python 3.6
Not sure if we want to have a get_origin method to handle python 3.6 or if there is a backport of any kind

yield cls.list_length_validator

@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
update_not_none(field_schema, minItems=cls.min_items, maxItems=cls.max_items)

@classmethod
def list_length_validator(cls, v: 'List[T]') -> 'List[T]':
def list_length_validator(cls, v: 'Optional[List[T]]', field: 'ModelField') -> 'Optional[List[T]]': # type: ignore[name-defined] # noqa: E501,F821
Copy link
Member Author

Choose a reason for hiding this comment

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

@samuelcolvin Thank you for the review ! Just a question on the mypy policy as I could

  • not set the type of field and just add # type: ignore
  • same but explaining the ignored error but then add a noqa for length
  • do some refactoring to avoid circular imports (overkill probably)
  • let it like that

Copy link
Member

Choose a reason for hiding this comment

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

you just needed to import ModelField, imports inside if TYPE_CHECKING: don't cause circular import problems.

@samuelcolvin samuelcolvin merged commit 2c05415 into pydantic:master Apr 15, 2020
@PrettyWood PrettyWood deleted the fix/none-mix-max-items branch April 15, 2020 21:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

When using min_items and max_items pydantic cannot accept None
2 participants