-
Notifications
You must be signed in to change notification settings - Fork 2
Generic-based typing for Validator classes #134
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
Conversation
These validators used to be StringValidator subclasses that overrode the validate return type in a type-incompatible way. Now they only inherit from Validator and use a separate StringValidator object for basic string validation.
|
I did a first review of your changes. Pretty cool! I am looking forward to better typing support! :D (less red underlines in my IDE are always good.) Before I approve this PR I would like to do a quick test in IDE. I think I will do that in the next few days. |
|
I've decided to create a "dev-mypy" branch for all changes related to #116, including this one, and changed the target branch of this PR. Reason: While this PR is basically self-contained, it doesn't make much sense to include it in a release before the other mypy-related changes are done. Also, the changes should be tested on an actual real-life code base before release, which also makes more sense in combination with the upcoming changes. |
This PR improves the typing of validator classes a lot by changing the
Validatorbase class to a Generic classValidator[T]. This came with a lot of necessary follow-up changes, so it's a rather large PR. Most of it are new tests, though.This is one big step towards full mypy support (see also: #116).
Generic Validator class
The
Validatorbase class is a Generic class now (Validator[T]), with a type parameter that specifies the type of the validator's output value, i.e. the return type of thevalidate()method.All classes have been adjusted to properly set this type parameter and add their own type parameters if needed to allow for proper typing. For example,
ListValidator[T](which already was a Generic) inherits fromValidator[list[T]]now, andNoneable[T_Wrapped, T_Default](new type parameters) inherits fromValidator[T_Wrapped | T_Default].Composition instead of inheritance for extended validators
Several "extended" validators that are based on another validator change the return type had to be modified to use composition rather than inheritance (otherwise changing the return type violates the LSP). For example, the
DecimalValidatorused to be a subclass ofStringValidatorto first validate the input as a string without reinventing the StringValidator, and then try to parse the string as a decimal. However,Decimalis not a subclass ofstr, so the return type ofDecimalValidatorwasn't compatible with the return type of its super class. Now,DecimalValidatorinherits directly fromValidator[Decimal]and uses a separateStringValidator(stored in an attribute) for the string validation. This is a breaking change, although one that should not matter that much in actual projects unless they meddle with a validator's internals (which are technically public, hence it's a breaking change).The following validators are affected by this change:
DataclassValidator: Used to inherit fromDictValidator, now it inherits fromValidator[T_Dataclass]and uses a separateDictValidator.(This is an implementation detail that can change at any point in the future.)DateValidator,DateTimeValidator,TimeValidator: Used to inherit fromStringValidator, now inherit fromValidator[datetime.date]etc. and use a separateStringValidatorfor basic string validation.DecimalValidator: Used to inherit fromStringValidator, now inherits fromValidator[Decimal].EnumValidator: Used to inherit fromAnyOfValidator, now inherits fromValidator[T_Enum]and uses a separateAnyOfValidator.It is recommended for users of the library to check and adjust their custom validators accordingly. Validators based on inheritance should still work the same way as before, but if the return type is changed in an incompatible way, the validator should be rewritten to use composition for its base validator.
Please note that extending a validator by inheritance is still perfectly fine as long as the return type is the same as (or a subclass of) the original return type of the super class. For example, the
FloatToDecimalValidatorstill inherits fromDecimalValidatorbecause it still returns aDecimal, it just modifies/extends the validation behavior._ensure_type with None
A minor change in the
Validator._ensure_type()method was also done: This function now acceptsNoneiftype(None)is included in the list of accepted types. This is a small extension of that function and not a breaking change. It allows to simplify some code (and typing) e.g. in theAnyOfValidatorbecause it doesn't need to handleNoneas a special case anymore.Deprecate importing and reusing TypeVars
Another small change is that the TypeVars used by the built-in validators (
T_Dataclass,T_EnumandT_ListItem) have been removed from__all__, i.e. the modules don't export these TypeVars anymore. They can still be imported and used, but this is considered deprecated now and linters should complain about importing them. Instead of reusing these TypeVars, it's recommended to define your own TypeVars (or use the new type parameter syntax when using Python 3.12 or above).This is only a deprecation, not a breaking change yet, but the compatibility imports in
validataclass.validatorswill be removed in a future version.Typing tests with pytest-mypy-plugins
In order to test all of these new changes and provide a consistent typing experience, we've added a second type of automated tests besides unit tests. There are now "typing tests" using the pytest plugin pytest-mypy-plugins. These can be found in
tests/mypy/and are defined using YAML files. They will be automatically run together with the unit tests as part of running pytest (you can usemake test-typingif you specifically only want to run the typing tests).