From 5bdecf9442edd7177887ea62c8f87ca386345a53 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 6 Jul 2022 13:24:22 +0100 Subject: [PATCH 01/25] first draft of pydantic V2 blog --- docs/blog/pydantic-v2.md | 221 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 docs/blog/pydantic-v2.md diff --git a/docs/blog/pydantic-v2.md b/docs/blog/pydantic-v2.md new file mode 100644 index 0000000000..65b433975f --- /dev/null +++ b/docs/blog/pydantic-v2.md @@ -0,0 +1,221 @@ +# Pydantic V2 + +I've spoken to quite a few people about pydantic V2, and mention it in passing even more. + +I think I owe people a proper explanation of the plan for V2: +* What will change +* What will be added +* What will be removed +* How I'm intending to go about completing it and getting it released +* Some idea of timeframe :fearful: + +Here goes... + +# Plan & Timeframe + +I'm currently taking a kind of "sabbatical" after leaving my last job to get pydantic V2 released. +Why? Well I ask myself that quite often. I'm very proud of how much pydantic is used, but I'm less proud of its internals. +Since it's something people seem to use quite a lot (26m downloads a month, Used 72k public repos on GitHub), +I want it to be as good as possible. + +While I'm on the subject of why, how and my odd sabbatical: if you work for a large company who use pydantic a lot, +you should encourage the company to **sponsor me a meaningful amount**, +like [Salesforce did](https://twitter.com/samuel_colvin/status/1501288247670063104). +This is not charity, recruitment or marketing - the argument should be about how much the company will save if +pydantic is 10x faster, more stable and more powerful - it would be worth paying me 10% of that to make it happen. + +The plan is to have pydantic V2 released within 3 months of full time work +(again, that'll be sooner if I can continue to work on it full time). + +Before pydantic V2 can be released, we need to released pydantic v1.10 - there are lots of changes in the main +branch of pydantic contributed by the community, it's only fair to provide a release including those changes, +many of them will remain unchanged for V2, the rest will act as a requirement to make sure pydantic V2 includes +the capabilities they implemented. + +The basic road map for me is as follows: +1. implement a few more critical features in pydantic-core +2. release V0.1 of pydantic-core +3. work on getting pydantic V1.10 out - basically merge all open PRs that are finished +4. release pydantic V1.10 +5. delete all stale PRs which didn't make it into V1.10, apologise profusely to their authors who put their valuable + time into pydantic only to have their PRs closed :pray: +6. change the main branch of pydantic to target v2 +7. start tearing pydantic code apart and see how many existing tests can be made to pass +8. rinse, repeat +9. release pydantic V2 :tada: + +# Introduction + +Pydantic began life as an experiment in some code in a long dead project. +I ended up making the code into a package and releasing it. +It got a bit of attention on hacker news when it was first released, but started to get really popular when +Sebastian Ramirez used it in FastAPI. +Since then the package and its usage have grown enormously. +The core logic however has remained relatively unchanged since the initial experiment. +It's old, it smells, it needs to be rebuilt. +The release of version 2 is an opportunity to rebuild pydantic and correct many things that don't make sense. + +Much of the work on V2 is already done, but there's still a lot to do. +Now seems a good opportunity to explain what V2 is going to look like and get feedback from uses. + +## Headlines + +For good and bad, here are some of the biggest changes expected in V2. + +The core validation logic of pydantic V2 will be performed by a separate package +[pydantic-core](https://github.com/samuelcolvin/pydantic-core) which I've been building over the last few months. + +*pydantic-core* is written in Rust using the excellent [pyo3](https://pyo3.rs/) library which provides rust bindings +for python. + +**Note:** the python interface to pydantic shouldn't change as a result of using pydantic-core, instead +pydantic will use type annotations to build a schema for pydantic-core to use. + +pydantic-core is usable now, albeit with a fairly unintuitive API, if you're interested, please give it a try. + +pydantic-core provides validators for all common data types, +[see a list here](https://github.com/samuelcolvin/pydantic-core/blob/main/pydantic_core/_types.py#L291). +Other, less commonly used data types will be supported via validator functions. + +### Performance :smile: + +As a result of the move to rust for the validation logic +(and significant improvements in how validation objects are structured) pydantic V2 will be significantly faster +than pydantic V1.X. + +Looking at the pydantic-core [benchmarks](https://github.com/samuelcolvin/pydantic-core/tree/main/tests/benchmarks), +pydantic V2 is between 4x and 50x faster than pydantic V1.X. + +In general, pydantic V2 is about 17x faster than V1.X when validating a representative model containing a range +of common fields. + +### Strict Mode :smile: + +People have long complained about pydantic preference for coercing data instead of throwing an error. +E.g. input to an `int` field could be `123` or the string `"123"` which would be converted to `123`. + +pydantic-core comes with "strict mode" built in. With this only the exact data type is allowed, e.g. passing +`"123"` to an `int` field would result in a validation error. + +Strictness can be defined on a per-field basis, or whole model. + +#### IsInstance checks :smile: + +Strict mode also means it makes sense to provide an `is_instance` method on validators which effectively run +validation then throw away the result while avoiding the (admittedly small) overhead of creating and raising +and error or returning the validation result. + +### Formalised Conversion Table :smile: + +As well as complaints about coercion, another (legitimate) complaint was inconsistency around data conversion. + +In pydantic V2, the following principle will govern when data should be converted in "lax mode" (`strict=False`): + +> If the input data has a SINGLE and INTUITIVE representation, in the field's type, AND no data is lost +> during the conversion, then the data will be converted, Otherwise a validation error is raised. +> There is one exception to this rule: string fields - +> virtually all data has an intuitive representation as a string (e.g. `repr()` and `str()`), therefore +> a custom rule is required: only `str`, `bytes` and `bytearray` are valid as inputs to string fields. + +| Field Type | Input | Single & Intuitive R. | data Loss | Result | +|------------|-------------------------|-----------------------|------------------|---------| +| `int` | `"123"` | :material-check: | :material-close: | Convert | +| `int` | `123.0` | :material-check: | :material-close: | Convert | +| `int` | `123.1` | :material-check: | :material-check: | Error | +| `date` | `"2020-01-01"` | :material-check: | :material-close: | Convert | +| `date` | `"2020-01-01T12:00:00"` | :material-check: | :material-check: | Error | + +In addition to the general rule, we'll provide a conversion table which defines exactly what data will be allowed +to which field types. See [the table below](TODO) for a start on this. + +### Built in JSON support :smile: + +pydantic-core can parse JSON directly into a model or output type, this both improves performance and avoids +issue with strictness - e.g. if you have a "strict" model with a `datetime` field, the input must be a +`datetime` object, but clearly that makes no sense when parsing JSON which has no `datatime` type. +Same with `bytes` and many other types. + +Pydantic v2 will therefore allow some conversion when validating JSON directly, even in strict mode +(e.g. `ISO8601 string -> datetime`, `str -> bytes`) even though this would not be allowed when validating +a python object. + +In future direct validation of JSON will also allow: +* parsing in a separate thread while starting validation in the main thread +* line numbers from JSON to be included in the validation errors + +### Validation without a Model :smile: + +In pydantic v1 the core of all validation was a pydantic model, this led to significant overheads and complexity +when the output data type was not a model. + +pydantic-core operates on a tree of validators with no "model" type required at the base of the tree. +It can therefore validate a single `string` or `datetime` value, a `TypeDict` or `Model` equally easily. + +This feature will provide significant addition performance improvements in scenarios like: +* adding validation to `dataclass` +* validating URL arguments, query strings, headers, etc. in FastAPI +* adding validation to `TypedDict` +* function argument validation + +Basically anywhere were you don't care about a traditional model class. + +### Strict API & API documentation :smile: + +When preparing a pydantic V2, we'll make a strict distinction between the public API and private functions & classes. +Private objects clearly identified as private via `_internal` sub package to discourage use. + +The public API will have API documentation. I've recently been working with the wonderful +[mkdocstrings](https://github.com/mkdocstrings/mkdocstrings) for both +[dirty-equals](https://dirty-equals.helpmanual.io/) and +[watchfiles](https:://watchfiles.helpmanual.io/) documentation. I intend to use `mkdocstrings` to generate complete +API documentation. + +This wouldn't replace the current example-based somewhat informal documentation style byt instead will augment it. + +### No pure python implementation :frowning: + +Since pydantic-core is written in Rust, and I have absolutely no intention of rewriting it in python, +pydantic V2 will only work where a binary package can be installed. + +pydantic-core will provide binaries in PyPI for (at least): + +* **Linux**: `x86_64`, `aarch64`, `i686`, `armv7l`, `musl-x86_64` & `musl-aarch64` +* **MacOS**: `x86_64` & `arm64` (except python 3.7) +* **Windows**: `amd64` & `win32` +* **Web Assembly**: `wasm32` (pydantic-core is already compiled for wasm32 using emscripten and unit tests pass, + except where cpython itself has [problems](https://github.com/pyodide/pyodide/issues/2841)) + +Other binaries can be added provided they can be (cross-)compiled on github actions. +If no binary is available from PyPI, pydantic-core can be compiled from source if Rust stable is available. + +The only place where I know this will cause problems is Raspberry Pi, which is a bit of +[mess](https://github.com/piwheels/packages/issues/254) when it comes to packages written in rust for python. +Effectively, until that's fixed you'll likely have to install pydantic-core with +`pip install -i https://pypi.org/simple/ pydantic-core`. + +### Pydantic becomes a pure python package :confused: + +Pydantic v1.X is a pure python code base but is compiled with cython to provide some performance improvements. +Since the "hot" code is moved to pydantic-core, pydantic itself can go back to being a pure python package. + +This should significantly reduce the size of the pydantic package and make unit tests of pydantic much faster. +In addition, some constraints on pydantic code can be removed once it no-longer has to be compilable with cython. + +Some pieces of edge logic could get a little slower as they're no longer compiled. + +## Other Improvements :smile: + +* Recursive models +* Documentation examples you can edit and run + +TODO + +## Removed Features :neutral_face: + +* `__root__` models + +TODO + +## Conversion Table + +TODO From 5bb1ebbc6850201813a18bc458c3ca28031a7850 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 6 Jul 2022 15:27:57 +0100 Subject: [PATCH 02/25] more blog --- docs/blog/pydantic-v2.md | 227 +++++++++++++++++++++++++++++++++++---- 1 file changed, 205 insertions(+), 22 deletions(-) diff --git a/docs/blog/pydantic-v2.md b/docs/blog/pydantic-v2.md index 65b433975f..81ccf7b434 100644 --- a/docs/blog/pydantic-v2.md +++ b/docs/blog/pydantic-v2.md @@ -14,8 +14,9 @@ Here goes... # Plan & Timeframe I'm currently taking a kind of "sabbatical" after leaving my last job to get pydantic V2 released. -Why? Well I ask myself that quite often. I'm very proud of how much pydantic is used, but I'm less proud of its internals. -Since it's something people seem to use quite a lot (26m downloads a month, Used 72k public repos on GitHub), +Why? Well I ask myself that question quite often. +I'm very proud of how much pydantic is used, but I'm less proud of its internals. +Since it's something people seem to care about and use quite a lot (26M downloads a month, Used 72K public repos on GitHub), I want it to be as good as possible. While I'm on the subject of why, how and my odd sabbatical: if you work for a large company who use pydantic a lot, @@ -24,8 +25,8 @@ like [Salesforce did](https://twitter.com/samuel_colvin/status/15012882476700631 This is not charity, recruitment or marketing - the argument should be about how much the company will save if pydantic is 10x faster, more stable and more powerful - it would be worth paying me 10% of that to make it happen. -The plan is to have pydantic V2 released within 3 months of full time work -(again, that'll be sooner if I can continue to work on it full time). +The plan is to have pydantic V2 released within 3 months of full-time work +(again, that'll be sooner if I can continue to work on it full-time :face_with_raised_eyebrow:). Before pydantic V2 can be released, we need to released pydantic v1.10 - there are lots of changes in the main branch of pydantic contributed by the community, it's only fair to provide a release including those changes, @@ -33,16 +34,17 @@ many of them will remain unchanged for V2, the rest will act as a requirement to the capabilities they implemented. The basic road map for me is as follows: -1. implement a few more critical features in pydantic-core -2. release V0.1 of pydantic-core -3. work on getting pydantic V1.10 out - basically merge all open PRs that are finished -4. release pydantic V1.10 -5. delete all stale PRs which didn't make it into V1.10, apologise profusely to their authors who put their valuable +1. Implement a few more critical features in pydantic-core +2. Release V0.1 of pydantic-core +3. Work on getting pydantic V1.10 out - basically merge all open PRs that are finished +4. Release pydantic V1.10 +5. Delete all stale PRs which didn't make it into V1.10, apologise profusely to their authors who put their valuable time into pydantic only to have their PRs closed :pray: -6. change the main branch of pydantic to target v2 -7. start tearing pydantic code apart and see how many existing tests can be made to pass -8. rinse, repeat -9. release pydantic V2 :tada: +6. Rename `master` to `main`, seems like a good time to do this +7. Change the main branch of pydantic to target v2 +8. Start tearing pydantic code apart and see how many existing tests can be made to pass +9. Rinse, repeat +10. Release pydantic V2 :tada: # Introduction @@ -68,8 +70,9 @@ The core validation logic of pydantic V2 will be performed by a separate package *pydantic-core* is written in Rust using the excellent [pyo3](https://pyo3.rs/) library which provides rust bindings for python. -**Note:** the python interface to pydantic shouldn't change as a result of using pydantic-core, instead -pydantic will use type annotations to build a schema for pydantic-core to use. +!!! note + The python interface to pydantic shouldn't change as a result of using pydantic-core, instead + pydantic will use type annotations to build a schema for pydantic-core to use. pydantic-core is usable now, albeit with a fairly unintuitive API, if you're interested, please give it a try. @@ -143,6 +146,12 @@ In future direct validation of JSON will also allow: * parsing in a separate thread while starting validation in the main thread * line numbers from JSON to be included in the validation errors +!!! note + Pydantic has always had special support for JSON, that is not going to change. While in theory other formats + could be specifically supported, the overheads are significant and I don't think there's another format that's + used widely enough to be worth specific logic. Other formats can be parsed to python then validated, similarly + when serialising, data can be exported to a python object, then serialised, see below. + ### Validation without a Model :smile: In pydantic v1 the core of all validation was a pydantic model, this led to significant overheads and complexity @@ -159,6 +168,108 @@ This feature will provide significant addition performance improvements in scena Basically anywhere were you don't care about a traditional model class. +We'll need to add standalone methods for generating json schema and dumping these objects to JSON etc. + +### Validator Function Improvements :smile: :smile: :smile: + +This is one of the changes in pydantic V2 that I'm most excited about, I've been talking about something +like this for a long time, see [#1984](https://github.com/samuelcolvin/pydantic/issues/1984), but couldn't +find a way to do this until now. + +Fields which use a function for validation can be any of the following types: +* **function before mode** - where the function is called before the inner validator is called +* **function after mode** - where the function is called after the inner validator is called +* **plan mode** - where there's no inner validator +* **wrap mode** - where the function takes a reference to a function which calls the inner validator, + and can therefore modify the input before inner validation, modify the output after inner validation, conditionally + not call the inner validator or catch errors from the inner validator and return a default value + +An example how a wrap validator might look: + +```py +from datetime import datetime +from pydantic import BaseModel, ValidationError, validator + +class MyModel(BaseModel): + timestamp: datetime + + @validator('timestamp', mode='wrap') + def validate_timestamp(cls, v, handler): + if v == 'now': + # we don't want to bother with further validation, so we just return the value + return datetime.now() + try: + return handler(v) + except ValidationError: + # validation failed, in this case we want to return a default value + return datetime(2000, 1, 1) +``` + +### Improvements to Dumping/Serialization/Export :smile: :question: + +(I haven't worked on this yet, so things are a bit less set in stone) + +There has long been a debate about how to handle converting data when extracting it from a model. +One of the features people have long requested is the ability to convert data to JSON compliant types while +converting a model to a dict. + +My plan is to move data export into pydantic-core, with that one implementation can support all export modes without +compromising (and hopefully significantly improving) performance. + +I see four different export/serialisation scenarios: + +1. Extracting the field values of a model with no conversion, effectively `model.__dict__` but with the current filtering + logic provided by `.dict()` +2. Extracting the field values of a model recursively (effectively what `.dict()` does now) - sub-models are converted to + dicts, but other fields remain unchanged. +3. Extracting data and converting at the same time (e.g. to JSON compliant types) +4. Serialising data straight to JSON + +I think all 4 modes can be supported in a single implementation, with a kind of "3.5" mode where a python function +is used to convert the data as the use wishes. + +The current `include` and `exclude` logic is extremely complicated, hopefully it won't be too hard to +translate it to rust. + +We should also add support for `validate_alias` and `dump_alias` to allow for customising field keys. + +### Model namespace cleanup :smile: + +For years I've wanted to clean up the model namespace, +see [#1001](https://github.com/samuelcolvin/pydantic/issues/1001). This would avoid confusing gotchas when field +names clash with methods on a model, it would also make it safer to add more methods to a model without risking +new clashes. + +After much deliberation (and even giving a lightning talk at the python language submit about alternatives, see +[here](https://discuss.python.org/t/better-fields-access-and-allowing-a-new-character-at-the-start-of-identifiers/14529)) +I've decided to go with the simplest and clearest approach, the expense of a bit more typing: + +All methods on models with start with `model_`, fields names will not be allowed to start with `"model"`, +aliases can be used if required. + +This will mean the following methods and attributes on a model: +* `.__dict__` as currently, holds a dict of validated data +* `.__fields_set__` as currently, set containing which fields were set (vs. populated from defaults) +* `.model_validate()` (currently `.parse_obj()`) - validate data +* `.model_validate_json()` (currently `parse_raw(j..., content_type='application/json')`) - validate data from JSON +* `.model_dump()` (currently `.dict()`) - as above, with new `mode` argument +* `.model_json()` (currently `.json()`) - alias of `.model_dump(mode='json')` +* `.model_schema()` (currently `.schema()`) +* `.model_schema_json()` (currently `.schema_json()`) +* `.model_update_forward_refs()` (currently `.update_forward_refs()`) - update forward references +* `.model_copy()` (currently `.copy()`) - copy a model +* `.model_construct()` (currently `.construct()`) - construct a model with no validation +* `._model_validator` attribute holding the internal pydantic `SchemaValidator` +* `.model_fields` (currently `.__fields__`) - although the format will have to change a lot, might be an alias of + `._model_validator.schema` +* `ModelConfig` (currently `Config`) - configuration class for models + +The following methods will be removed: +* `.parse_file()` +* `.parse_raw()` +* `.from_orm()` - the functionality has been configured as a `Config` property call `from_attributes` which can be set + either on `ModelConfig` or on a specific `ModelClass` or `TypedDict` field + ### Strict API & API documentation :smile: When preparing a pydantic V2, we'll make a strict distinction between the public API and private functions & classes. @@ -172,6 +283,10 @@ API documentation. This wouldn't replace the current example-based somewhat informal documentation style byt instead will augment it. +### Error descriptions :smile: + + + ### No pure python implementation :frowning: Since pydantic-core is written in Rust, and I have absolutely no intention of rewriting it in python, @@ -203,18 +318,86 @@ In addition, some constraints on pydantic code can be removed once it no-longer Some pieces of edge logic could get a little slower as they're no longer compiled. -## Other Improvements :smile: +### I'm dropping the word "parse" and just using "validate" :confused: -* Recursive models -* Documentation examples you can edit and run +Partly due to the issues with the lack of strict mode. I've previously gone back and forth between using +"parse" and "validate" for what pydantic does. -TODO +While pydantic is not simply a validation library (and I'm sure some would argue validation is not strictly what it does), +most people use the **validation**. -## Removed Features :neutral_face: +it's time to stop fighting that, use consistent names. -* `__root__` models +The word "parse" will no longer be used except when talking about JSON parsing. -TODO +## Changes to custom field types :neutral_face: + +Since the core structure of validators has changed from "a list of validators to call on each field" to +"a tree of validators which call each other", the +[`__get_validators__`](https://pydantic-docs.helpmanual.io/usage/types/#classes-with-__get_validators__) +way of defining custom field types no longer makes sense. + +Instead we'll look for the attribute `__pydantic_schema__` which must be a +pydantic-core compliant schema for validating data to this field type, except for the `'function'` +item which can be a string which should be a property (function) of the class. + +Here's an example of how a custom field type could be defined: + +```py +from pydantic import ValiationSchema + +class Foobar: + __pydantic_schema__: ValiationSchema = { + 'type': 'function', + 'mode': 'after', + 'function': 'validate', + 'schema': {'type': 'str'} + } + + def __init__(self, value: str): + self.value = value + + @classmethod + def validate(cls, value): + if 'foobar' in value: + return Foobar(value) + else: + raise ValueError('expected foobar') + +``` + +What's going on here: `__pydantic_schema__` defines a schema which effectively says: + +> Validate input data as a string, then call the `validate` with that string, use the output of `validate` +> as the final result of validation. + +`ValiationSchema` is just an alias to +[`pydantic_core.types.Schema`](https://github.com/samuelcolvin/pydantic-core/blob/main/pydantic_core/_types.py#L291). + +!!! note + pydantic-core schema has full type definitions although since the type is recursive, + mypy can't provide static type analysis, pyright however can. + +## Other Improvements :smile: + +1. Recursive models with cyclic references - although recursive models were supported by pydantic V1, + data with cyclic references cause recursion errors, in pydantic-core code is correctly detected + and a validation error is raised +2. The reason I've been so keen to get pydantic-core to compile and run with wasm is that I want all examples + in the docs of pydantic V2 to be editable and runnable in the browser +3. `from_orm` has become `from_attributes` and is now defined at schema generation time + (either via model config or field config) + +## Removed Features :neutral_face: + +* `__root__` custom root models are no longer necessary since validation on any support data type is supported + without a model +* `parse_file` and `parse_raw`, partially replaced with `.model_validate_json()` +* `TypeError` are no longer considered as validation errors, but rather as internal errors, this is to better + catch errors in argument names in function validators. +* Subclasses of builtin types like `str`, `bytes` and `int` are coerced to their parent builtin type, + this is a limitation of how pydantic-core converts these types to rust types during validation, if you have a + specific need to keep the type, you can use wrap validators (see above) or custom type validation ## Conversion Table From 129684874ae21a6ff8070a944ab73a6584d0ea65 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 6 Jul 2022 16:27:46 +0100 Subject: [PATCH 03/25] blog rendering and formatting --- docs/blog/pydantic-v2.md | 83 +++++++++++++++++++++++--------------- docs/extra/ad.js | 3 +- docs/extra/tweaks.css | 13 ++++++ docs/img/samuelcolvin.jpg | Bin 0 -> 35372 bytes docs/theme/main.html | 2 +- mkdocs.yml | 5 +++ 6 files changed, 71 insertions(+), 35 deletions(-) create mode 100644 docs/img/samuelcolvin.jpg diff --git a/docs/blog/pydantic-v2.md b/docs/blog/pydantic-v2.md index 81ccf7b434..e66566fa71 100644 --- a/docs/blog/pydantic-v2.md +++ b/docs/blog/pydantic-v2.md @@ -1,11 +1,22 @@ # Pydantic V2 + + I've spoken to quite a few people about pydantic V2, and mention it in passing even more. I think I owe people a proper explanation of the plan for V2: -* What will change + * What will be added * What will be removed +* What will change * How I'm intending to go about completing it and getting it released * Some idea of timeframe :fearful: @@ -13,10 +24,11 @@ Here goes... # Plan & Timeframe -I'm currently taking a kind of "sabbatical" after leaving my last job to get pydantic V2 released. +I'm currently taking a kind of sabbatical after leaving my last job to get pydantic V2 released. Why? Well I ask myself that question quite often. I'm very proud of how much pydantic is used, but I'm less proud of its internals. -Since it's something people seem to care about and use quite a lot (26M downloads a month, Used 72K public repos on GitHub), +Since it's something people seem to care about and use quite a lot +(26M downloads a month, used by 72K public repos on GitHub). I want it to be as good as possible. While I'm on the subject of why, how and my odd sabbatical: if you work for a large company who use pydantic a lot, @@ -34,6 +46,7 @@ many of them will remain unchanged for V2, the rest will act as a requirement to the capabilities they implemented. The basic road map for me is as follows: + 1. Implement a few more critical features in pydantic-core 2. Release V0.1 of pydantic-core 3. Work on getting pydantic V1.10 out - basically merge all open PRs that are finished @@ -80,7 +93,7 @@ pydantic-core provides validators for all common data types, [see a list here](https://github.com/samuelcolvin/pydantic-core/blob/main/pydantic_core/_types.py#L291). Other, less commonly used data types will be supported via validator functions. -### Performance :smile: +### Performance :smiley: As a result of the move to rust for the validation logic (and significant improvements in how validation objects are structured) pydantic V2 will be significantly faster @@ -92,7 +105,7 @@ pydantic V2 is between 4x and 50x faster than pydantic V1.X. In general, pydantic V2 is about 17x faster than V1.X when validating a representative model containing a range of common fields. -### Strict Mode :smile: +### Strict Mode :smiley: People have long complained about pydantic preference for coercing data instead of throwing an error. E.g. input to an `int` field could be `123` or the string `"123"` which would be converted to `123`. @@ -102,13 +115,13 @@ pydantic-core comes with "strict mode" built in. With this only the exact data t Strictness can be defined on a per-field basis, or whole model. -#### IsInstance checks :smile: +#### IsInstance checks :smiley: Strict mode also means it makes sense to provide an `is_instance` method on validators which effectively run validation then throw away the result while avoiding the (admittedly small) overhead of creating and raising and error or returning the validation result. -### Formalised Conversion Table :smile: +### Formalised Conversion Table :smiley: As well as complaints about coercion, another (legitimate) complaint was inconsistency around data conversion. @@ -131,7 +144,7 @@ In pydantic V2, the following principle will govern when data should be converte In addition to the general rule, we'll provide a conversion table which defines exactly what data will be allowed to which field types. See [the table below](TODO) for a start on this. -### Built in JSON support :smile: +### Built in JSON support :smiley: pydantic-core can parse JSON directly into a model or output type, this both improves performance and avoids issue with strictness - e.g. if you have a "strict" model with a `datetime` field, the input must be a @@ -152,7 +165,7 @@ In future direct validation of JSON will also allow: used widely enough to be worth specific logic. Other formats can be parsed to python then validated, similarly when serialising, data can be exported to a python object, then serialised, see below. -### Validation without a Model :smile: +### Validation without a Model :smiley: In pydantic v1 the core of all validation was a pydantic model, this led to significant overheads and complexity when the output data type was not a model. @@ -161,6 +174,7 @@ pydantic-core operates on a tree of validators with no "model" type required at It can therefore validate a single `string` or `datetime` value, a `TypeDict` or `Model` equally easily. This feature will provide significant addition performance improvements in scenarios like: + * adding validation to `dataclass` * validating URL arguments, query strings, headers, etc. in FastAPI * adding validation to `TypedDict` @@ -170,13 +184,14 @@ Basically anywhere were you don't care about a traditional model class. We'll need to add standalone methods for generating json schema and dumping these objects to JSON etc. -### Validator Function Improvements :smile: :smile: :smile: +### Validator Function Improvements :smiley: :smiley: :smiley: This is one of the changes in pydantic V2 that I'm most excited about, I've been talking about something like this for a long time, see [#1984](https://github.com/samuelcolvin/pydantic/issues/1984), but couldn't find a way to do this until now. Fields which use a function for validation can be any of the following types: + * **function before mode** - where the function is called before the inner validator is called * **function after mode** - where the function is called after the inner validator is called * **plan mode** - where there's no inner validator @@ -186,7 +201,7 @@ Fields which use a function for validation can be any of the following types: An example how a wrap validator might look: -```py +```py title="Wrap mode validator function" from datetime import datetime from pydantic import BaseModel, ValidationError, validator @@ -205,9 +220,9 @@ class MyModel(BaseModel): return datetime(2000, 1, 1) ``` -### Improvements to Dumping/Serialization/Export :smile: :question: +### Improvements to Dumping/Serialization/Export :smiley: :confused: -(I haven't worked on this yet, so things are a bit less set in stone) +(I haven't worked on this yet, so these ideas are only provisional) There has long been a debate about how to handle converting data when extracting it from a model. One of the features people have long requested is the ability to convert data to JSON compliant types while @@ -233,7 +248,7 @@ translate it to rust. We should also add support for `validate_alias` and `dump_alias` to allow for customising field keys. -### Model namespace cleanup :smile: +### Model namespace cleanup :smiley: For years I've wanted to clean up the model namespace, see [#1001](https://github.com/samuelcolvin/pydantic/issues/1001). This would avoid confusing gotchas when field @@ -248,6 +263,7 @@ All methods on models with start with `model_`, fields names will not be allowed aliases can be used if required. This will mean the following methods and attributes on a model: + * `.__dict__` as currently, holds a dict of validated data * `.__fields_set__` as currently, set containing which fields were set (vs. populated from defaults) * `.model_validate()` (currently `.parse_obj()`) - validate data @@ -265,12 +281,13 @@ This will mean the following methods and attributes on a model: * `ModelConfig` (currently `Config`) - configuration class for models The following methods will be removed: + * `.parse_file()` * `.parse_raw()` * `.from_orm()` - the functionality has been configured as a `Config` property call `from_attributes` which can be set either on `ModelConfig` or on a specific `ModelClass` or `TypedDict` field -### Strict API & API documentation :smile: +### Strict API & API documentation :smiley: When preparing a pydantic V2, we'll make a strict distinction between the public API and private functions & classes. Private objects clearly identified as private via `_internal` sub package to discourage use. @@ -283,9 +300,9 @@ API documentation. This wouldn't replace the current example-based somewhat informal documentation style byt instead will augment it. -### Error descriptions :smile: - +### Error descriptions :smiley: +TODO ### No pure python implementation :frowning: @@ -308,7 +325,7 @@ The only place where I know this will cause problems is Raspberry Pi, which is a Effectively, until that's fixed you'll likely have to install pydantic-core with `pip install -i https://pypi.org/simple/ pydantic-core`. -### Pydantic becomes a pure python package :confused: +### Pydantic becomes a pure python package :neutral_face: Pydantic v1.X is a pure python code base but is compiled with cython to provide some performance improvements. Since the "hot" code is moved to pydantic-core, pydantic itself can go back to being a pure python package. @@ -318,7 +335,7 @@ In addition, some constraints on pydantic code can be removed once it no-longer Some pieces of edge logic could get a little slower as they're no longer compiled. -### I'm dropping the word "parse" and just using "validate" :confused: +### I'm dropping the word "parse" and just using "validate" :neutral_face: Partly due to the issues with the lack of strict mode. I've previously gone back and forth between using "parse" and "validate" for what pydantic does. @@ -338,12 +355,12 @@ Since the core structure of validators has changed from "a list of validators to way of defining custom field types no longer makes sense. Instead we'll look for the attribute `__pydantic_schema__` which must be a -pydantic-core compliant schema for validating data to this field type, except for the `'function'` -item which can be a string which should be a property (function) of the class. +pydantic-core compliant schema for validating data to this field type, except for the `function` +item which can be a string which should be the name of a class function - `validate` below. Here's an example of how a custom field type could be defined: -```py +```py title="New custom field types" from pydantic import ValiationSchema class Foobar: @@ -363,7 +380,6 @@ class Foobar: return Foobar(value) else: raise ValueError('expected foobar') - ``` What's going on here: `__pydantic_schema__` defines a schema which effectively says: @@ -372,13 +388,14 @@ What's going on here: `__pydantic_schema__` defines a schema which effectively s > as the final result of validation. `ValiationSchema` is just an alias to -[`pydantic_core.types.Schema`](https://github.com/samuelcolvin/pydantic-core/blob/main/pydantic_core/_types.py#L291). +[`pydantic_core.Schema`](https://github.com/samuelcolvin/pydantic-core/blob/main/pydantic_core/_types.py#L291) +which is a type defining the schema for validation schemas. !!! note pydantic-core schema has full type definitions although since the type is recursive, mypy can't provide static type analysis, pyright however can. -## Other Improvements :smile: +## Other Improvements :smiley: 1. Recursive models with cyclic references - although recursive models were supported by pydantic V1, data with cyclic references cause recursion errors, in pydantic-core code is correctly detected @@ -390,14 +407,14 @@ What's going on here: `__pydantic_schema__` defines a schema which effectively s ## Removed Features :neutral_face: -* `__root__` custom root models are no longer necessary since validation on any support data type is supported - without a model -* `parse_file` and `parse_raw`, partially replaced with `.model_validate_json()` -* `TypeError` are no longer considered as validation errors, but rather as internal errors, this is to better - catch errors in argument names in function validators. -* Subclasses of builtin types like `str`, `bytes` and `int` are coerced to their parent builtin type, - this is a limitation of how pydantic-core converts these types to rust types during validation, if you have a - specific need to keep the type, you can use wrap validators (see above) or custom type validation +1. `__root__` custom root models are no longer necessary since validation on any support data type is supported + without a model +2. `parse_file` and `parse_raw`, partially replaced with `.model_validate_json()` +3. `TypeError` are no longer considered as validation errors, but rather as internal errors, this is to better + catch errors in argument names in function validators. +4. Subclasses of builtin types like `str`, `bytes` and `int` are coerced to their parent builtin type, + this is a limitation of how pydantic-core converts these types to rust types during validation, if you have a + specific need to keep the type, you can use wrap validators (see above) or custom type validation ## Conversion Table diff --git a/docs/extra/ad.js b/docs/extra/ad.js index db3eb85947..2a854c7e8e 100644 --- a/docs/extra/ad.js +++ b/docs/extra/ad.js @@ -21,4 +21,5 @@ function main () { document.head.appendChild(script) } -main() +// ads disabled for now +// main() diff --git a/docs/extra/tweaks.css b/docs/extra/tweaks.css index 2713d66363..a426d4e5bb 100644 --- a/docs/extra/tweaks.css +++ b/docs/extra/tweaks.css @@ -29,3 +29,16 @@ width: 75%; border-radius: 5px; } + +/*blog post*/ +aside.blog { + display: flex; + align-items: center; +} + +aside.blog img { + width: 50px; + height: 50px; + border-radius: 25px; + margin-right: 20px; +} diff --git a/docs/img/samuelcolvin.jpg b/docs/img/samuelcolvin.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f4887096e01b7db9bacd1fda94f62f70a4dc23d7 GIT binary patch literal 35372 zcmbTe1z1~M*Crg?olsm;9E!WUySqbhcc-{J#oa@3r-f47-D$DlP_(7AbkgU2zj?oF z=9;+H2oaxA&?XZ_Dt0Irgpq8tDY4i2CIdjS6I3F^uEgY5tSMMY)+ zDgXdL29Uxb0pMUr57x;3jm=<~5f0%WJvg5$xHK@7RIXT!ld0?I#TwFpN{6ZXj)SSFRJOV=8JTPmx(toYz?;QRL zY*ujpiuyMc+<(LHuo)rzHG%07{yjTn*h}R9hJpXaf9>Gl{}l%rW)J^wEHCgkz~8m} zef;_FHToMDX8C6eAPqo8Mn*wKLPbGAK|@1D$0WqU#K6EL!6(2Wq#&iDq#z|Hrv|Yz zP}8!~k&`p>GqG}T^6>CbF$jtZaEY*U^KktQ0*8i%hKYemjDWQgsQ(r?cmzZwWE501bPSk52QC0sDFk>#1SBLx zM3{95Y#e}yhlEeVDTz#=X^BGXLC6)6UWQ61)iMOsTKERyw(^WbLnnGgOhWpao`I2x znTMB;UqDbuT1Hk*UO`bwTSr$<-@wqw+Q!z--U00B&0wT)ayx`#dU=0Be5s8Kq8DCNp#nOX- zmMa34P%6EwWeAOqTk9Lp%5wpo2*k7Z`sQzH|77<6EwRY|Uzz>S#Qu}lCIAxw4z}?S z@Brd~0=;jp9Uza6tc9XNBy2g9BLjQMo2qRnSsuqDsb?VbRp zKZHWy%A>Arq;#Hl$8@LjDG7VcPTc#azH;>sv;E0SIs;5kuJFs04-fPHN=NjqJ>YMY z$}!9pWv6q;rh>vcL=%YaLN_wt1{3n4Ik8o5NGLyt@pelWN>pE!EpJymTQ+USM{F+S zrDB#Pqt|(y=%#3+D8!ATM4y{#JXqiN(AaHupUyG~1zz#e6~}%Ge5pfw4t|pGYA;0o z#Vr3V`Xa5PYh2(psF}sd4zeJy zH9EhG8K7ZU(>PADE0a1YM+LHV$_#*Cg)2?(#Scu zhY)))^a|wt(hu?-Wx=qTqnBs<16Xa~%W0Km%wnm!{?_S{aMCU+G&`=cU@T8ok>!UD zN0lsSlmBq!kZF;$XCxU3W(4~_pny6BPdi-P@jpRHNCbvyNb2A3Uz#9yWU5nICFRPG zzvymA^Y-{E7Xw{Jor z_){>wJZ4!9aBM-YFJI?xuHHA`0rE&5bU`n*cmX;tNiVs0;Db5C1-Ptp>W*@Tdqtpz zE;Sghv#Ul*a=%w<|7OvfRNMq*DA$2VwxIk7#YlnVz3h3`){ou*uV%kWQl#jex(F93 zdFn&&Q~6fE?cni=C71H{*37weWgq8nj=L3g4)b7S;@9f!{M2tC*4sbGi{zco2*`AY zIG-SkjbWb{%CGK_KB#%<^>iv|In_n+4b;Z1F768)Df(mL&^8)7d_v1Pg2U*7&MAEn zBX5n^4Dvsa(;PjTW#PSatI6Ka_hFiyPflGdS2C64aT$aEoos4ogXB+rUm3HwI;s;u zpTaLv9Jt2mRr(`5GV-@YC!K)&(Ab{gw;$LCTA3j`7mcw$cCE%9c&^zR>9fDTHjhh2 zxaf{IsT&d)MqlXt5mDpV>zq^HNT2Ro9}PmdHYpm7MZB$GPK>pl&`zE0V|jyUXl%>` zp5$f(F`jtL@suC4;i+fi`x^<+ERizVC5>Yr3BukFPMfa9ySlq&M33U$-TSp4-#X7^ z71-x`Mm1kSyYKe!-z4SYb0Z0w6ds@JJ(YJdD00tNuCc0C|8TqP5%xndI;On|_T5}g z2RW`D0=eOa+}#D<3@Gd4)Qq`OsEVGwbN?9NFsVa33no8c%#*+cJ2GUiP9`psX%%QT8Q0<PglhY_Ak(*`!Kk21CJ^kZuxV{jDkoF=M^s?~>2vIN1t^IkF6O>0}5uBa#f!T2L9k zt`yM(WVCag>tDQJu;V1FUQq0Fc-J3*Q4uuys;JSZ%{=1u`Xw zW!enWEP~S<;2?c=qeEK%qhH%#5g9e!6KR*FJ>&hU(7UBR!nF!wG#xLGY z<}i{`4H6zat>oOSHl+{+G5S%4+y4ST8$?A3Jx^$@&~yWy-qR-cwS8G^FYC$h#$MnG zBE945TfK4<+u;eLvnXd2XQCKU_wm~I-%#j9&B^{}np4=6y47Y3`mK+^K@<8nN8Jc_r&xFC^cbA47{?%)G@h({|>04yRyB+rp zZ`-t5xu{Wk$`tua4cEuNbrz?!q`}?2r(5hDoa*-|F*@*`k-ivj{CXjRAJdy?JWh(k z!}UQJ&>S()I z%y_jHDyfULbWV_^mcS~=U79h z^ht5T{8PKJ){Ljv{@4mVZ~zZ7!H^k*P*Ce8?ixTJ?i<2Su**q z3XEjSZ8GYvPWD8fy*nrkws~ck{HrxRoJ6D8%hVsY%@rNPhs~sOAkbpfg|1!C8utyO z_AgP+>$Q8>(PjN$8FF!eEStC2%%Oe$V^Nj%L709RAp=s(+&=$~#QIh6bP75@aovme=wF65p{Rb%_s}N5Rwy`*K_+ z-7=S?cVh#H$*Tmj(T1p2ANkePJ0z>Y`xl^#4@N9R=qU8%%a!j;6iHnXy@pVLuVOpi zZx3{nC9zMgUvPOmR8>7NQAmx-p@OJ9bgLumyg(_aW*?Tx!p}2~%1mL7uM&mmWed5>2+4> z{*?6MQ`lHpluYNFvi8hAUZ$JqWO2_MU3a4F!sCV@OxY;$+|lNQ7qa-uv!+Z$%gj^~ zleMvxk~6yj%q94)$ZhXr@g$QePmSwBUd8t}*fp59k|}_cwz7%4_@ktrw{q4yzL_*(wbZ!NlkjFe}Cr&2~(UNe!b+Gkarh{bX~sA^!dKk-^=9dk?UF7qit^ z!`cv(UXV$5523<5YcW4Lef??AkAp2$j<#c1o_)RI4*ghoaPX%_X~aaeNA6nSY!c$a z7MwA{Hy~SaMdNu;Wg9>alH6tuZSV^vVt;YgccG1%OkZqrcvN+1;6aw%ZgCwI zE57$^{}p-m`BCrZwuH!yq1Sf_6!F>jJ7P_xteytQbPT@!za1C1c~sv?49;@vyk7d) zZf|e3L6lxyRjODh!3}3?kIe-vi8TpwPihZ<^vkg-<5@$tDXbmT|BGKf~HXkY+ z(atK4jt}YDzH$mrj?kh#L-n6BvxZwQ#{427O#BSxQb)!ar)?u;NSR*yUO9;*Ti55t z82iK^3oAWPiZUj8XQy5|ZA`o^$l8~sc+H!U6Sw1|Ln8ldIKo(mup-S4SA&yR{9)1) z4gu%fOw_N_#dC&>KY){)dbpvi6!pah*{|y#>{Vsog^v@z1h2#ezX{<*wPh#hyvYGAK<*|moe;i_()jb<&za)U8+6s3cv2fLOVF&7#xv{#dMbKR8# zH@b`M)vD}=RDGk{_IHg1mbD`Ld$bPAM_pn9Vk=xlkEo+tMg4ly#=(2?w}ADF zZ>1*z!g_4g4d`2Md%9Jh-c2jGZ}$oV!{VL8Rvu} zyO_Eo`7k$b*$<{4na(G8H`X_~x5oN`a86me`Ov#dDaIUJuD*4Gu?}yd7tSRsb^3iR zcAel!-1}ek+WKT_BO&_QLhkPv`T&M||W#Vv7bAT5&?m^*C{kIOAzeh7e$RB4X zPj=u&<*S0|6ZeU3*fIz$&|F&CFYt6p7<%Re1kU2^+QS{$=YzM>B3C>=ZR~d_h@C~< zeuXxG@C?78Mul@nW#%u-M@qq~^;5n(m-0)evH>r<4nB+ax;p%z>04g?u8%FBU*IW- zvRZ^s+b6agv}AVlo}H9Y61|@w$*yUY7RuRWy-$&}u1HK7K&>z7$9gkL7cn%ZI@q1rvRDn>5ql zr=+_Fh;E;q!}6t^E#i9;V3d`0?C|PBpQ1YfRY0h9s;1EtE#b3F0>>~-s--@#^qprw z1sN@>0l%5>@`-O56>TlKRa3l$yeFr#R|jJ>)VG3Q>#945bAVRm;U=vko}Q*Gh_zd&{(=F6sRz0 z+8?2n(c|Y!XO3;`vO$tYu*Ey>UwxoFe`Fxi@hoYcFSQ}?UM9y9(RuoZ2AATc8O?#A z)rYl8^y!Jz?6#%NtPG=UwJSN^; z>n^fd<}BzA&Wf^-2vgjbRtEH*Uh)WLquT&RF*N%>8+Nht>PK5woq+mlN*gzg=kI^(cx4>dc=^prBW39oq;o z-t{G88+$-;z1DyQ9*B{m7m$==3YO(J&cj%Z9eF=Z)T)#YNeS157z$W$55No*=XKvY zE^@kR5qc}WjVK;`NvMOIN-eCEJ;{m>=O?T?rYrL()24713+W~5c+MD0uTr-fQX+R| zS8}Bp>07M>9j`yFrM@Zx2ksq!k}IzD#$pdz`W8lJS`X@Gy`t8D?XKH;q2;f88^!mSn|)0+d8>J%5|rU#vEr>nk>6+ zHwG!K>yK|nu-|kyC54Eq7YEtp{OVyH_VfIbZP=Ugvgz(zE+F%SMu9WnN~o}4OpRS$ z3dZc0`^e->71*TC=~Z|%GuwVbs-VN~m+Y0PjrK6y%o<^h4kuB1bLIrDx-Das;2Ez* zY|lg@Ay40oMcEe2wJvGyTV{XYc38vztn+}*A-@Lf@NsGw?kM|U8e8WMH+E{Iv9%%J zsU5=rJ%F%7cu*9JwPMk0!b9qH*p`DQ?gf)c_g&~&viq!gw3-=*dME;+9a;h%7)a@F zNNudR;JyY~mAPT73s`!AXymQ9OBU-&aL)AHUp2p#j(P2?fUGEP^or{Qd-_Crv}KHt zwLoHwp>R>@oT7n>6B!PavNHDNWOH&S_BlAMja`}PQ`B{WT|tdwHGfx7{4v`U5rZY= z*Z{VAvuA!WCBk%HZ-%dLmOYpP*-2_kKSi(-iMV!-wyi{IxuQ!EQjsOx`KdBBJ7kC# z^pmgs#5(oj=Bq{bVb^ND3$-J(ZmltgwVX5k<6CU-jiZ_WZDqtTznW7{iS!z^t(PBO zW-Ai$h$QcIyo=TEWsV;0?~zG=0L-E_9-al1)ID9ww&4?<0U*@MN6L;cwR{YR(CJKj zl%zszr<`1gHFkD<7b$KIIy!ReQ~k{0SNa}Mg-S&<@>n|BT<(DjefAt45&PO<+r0@% zi|HRGuGeO?)&(>Cn1`iqlLNw<-`Gp(-jVlx3bnP%^cCke3uxF-u2OltwHF=rWa8U} zwl6C5RLa>d^W%-5u&jE=6-p8_pa73v@Xb5UkY;@&@*l{Iv%5&F0GIin0^53H5}=!M z31YtWU7wIuP!7EtpJ&vT<&;e##$2-}cYld?Ji-KyBbWBx z(AQyfR1G9nh94>>r8wELu&FVz^cCa;7Urs1=c7>%J|CywH&Q6f@pa%|KGC^id*T2S zq38Tu!W440g0s`bAC(3wRYfwdx&s&lss8}5Q6B|(1iwgVp1Do<-U>01#JO*;#vx3Q z*~>|4@=^yT2u5RcW)GFLvP2&M?$9*2_`hPMB%Acz?7zX@PYk~oHF z`9%sm_7`Yx4cM}0Q)gY}7;=QI%*1WTIt_cYzCmv^!vPFR*}U1 z6%O~(VRh^9MLzFJoudxmJ<-@e;)80NLc;p4>+3gUlwDmz``cZACydY zrs!k(?5|aj1kRU$#6`ZiI2f0PLM{(~0GThjG282y$eelw+HKUMb`p|@-a`;}h1^#8 z$AHRA*TGnt1_H?&(f04+wunf2G#Z%sJrI)-`ggRS`MI$}v*%6%g^c+(e|2&Zed!Kk zj_>l;A&T|lok#!3KRt$Cs>8X>N?j1c+_+nGr+d(9AfLReJE8y9cF(MFMUm-fL|Iw-%l#5{NWtNK5-MXp$YE8=^|3_o~XCcNMK{Kf~= z+Vb_~dM9kw(X>fRh_APYD|nADg@bvc@JTuwb}`e+&X8tuseyi+sotG0msBZn70P|8 zZ{@1$=R;;>CY-}W=qzS(Q00G}xEJ}Ya)P;y>wpdt7}M6K_H7}F@3cx_tVi@bt3)9Z zyq|F@(w>4pi{hq!=!D+dvi7!Sj^V`X1426nsNSm^`}cXxYT__4m7s>;CJV!`6=R2B z5@KFUM2X6pH(ObM67s0WV$b2j{ADsy?cjp5b=yGs28@zn_HbGy8DyTk^FQ zM@p&`Sx|8Cl)>0A%lASK&Fu#s$aWQliW|zQwi#>no>CfDN7R0zabk#?f~KNIn9u_o z?YuFdr3nEYDk5pq>Npm7i~{5{&Q}sL_(~mlCBoUEsPpgubmhW`c-O{ivV8so74KrWW9mwkZ!) zO-dVFae9wC<%E%#XGHR|9xqLPwNG0<|B7v(UA3`4LkjlsO<8YP69UF4GYh=_PSGei zqrFIQ$(rgzgEaaG2rDruj(G zyf-Rwzco>K8%^cy*A!^*vf)WK$;=%tdV0aq>BubNr>)R(9MMILxZF9vsVF?%{C-pY zOJid9)uoLh-u5WmHjd?pgIssBUVO6wra-xH?7P6PvV`)8D5BiwIu$Vzt&Mx(mPG?z z8vU~CER&{VN;NEwIPNlB0SYM7>b$B1Au7oRXDyW;!~P%urhHeXqRZZnmVwL^my9{o z#`(_32w3o}jWYWq^nzH}T{Li+(OgeEJ4KBO%dz!+0c%6r7cU0l8JB_n z)m4G5p0)Bi2M z?O-cmF@WuBF&VH^RVc>^*OfPBHTQMdO&f_(SV4D*j{5om0|& z*8RoTcv;VtmsSDSrhz+*X&g_ABE=n`OcqKs&;!pCt`}vP8&v`ut7-H=zb)rf(HP-W z=KF2>b=QSgRQy@pg4d>IZ8L@EgneX|o7~H5+<^EJnV3p0&ht-j(Xb=vCO6J|v5(_RBEtecMB1ZYVgab>`10K`=^gP;m8 zHsT`TeqZrUwVA;cH5jJa#RF;z;DysVLXVn&T;QbvKafSmdYT zC}x2syLYEMUM7p3-K?R$H745<-Kp2Y?u0(Pg^Q@m(G%8-wahzXEN1kSy3eJ{cji*} z&Sx&^`gE?dLYXF^?HkKfcEpFPqTiEv`ObMf_lljYi%Ql#U}c32dOFZ^}bSDRFZF6Wo z%@rl9zF_vhMe+E>WZfQcltAM8+L8t_i-GdjBc!6@be>{Y>X^KZJo88X znUcy~QPO91(coure~!mO?$xuN%KTY60!S!|1a5)^WYs%C7rDpBLq{@8xZhx6IB4nD z`FktLo=}OEK#qs>zSHsJX58)7$-?6=ETQYqX|Kz&o$VcZk*TpAB@wwDaoZOflMEQ` zM1JZkxwu#-PT|oZQ~AU=sHw{k85c@9`fZ+YHY?l4U4A`;c0#U@q6;-tY0{W)gYp;o zIu+@Fo$=%k%%yg9Q%Jo-G||8qcaH=?n+^;}Qi!s7Tc~>*D_WsuBUPfq7bq#-WkClOPR#%kUCS5Y; z`~lCiKzZN4N#?{JBz|aJH~0NAZ)?QK4AR z_(ra(H;Ba`PLInr?f{*|^Cy&%=OK44h5yr6B0$%%$>oO<51(4rt~72wF7hy(PG;jj z04(z}()#IE>ga%B``Zz9?hkQ&EM)3oq+o~`AwYm+!tSwgp>fA}do;I2uJ$*27t2LJ zWzV<9MdG+VA6@Dv$B*oiwQl#yl{q{vkqiQ0KToF4&l!AGIZSN3-guc`L{{00c(g@Y z(!&GcM%4nLbyJq^pj(Vrnsd9yln*YO7^v13-OOU{DAXa$0av_aq~f;0m`b5)@mip3 zuG$!WS3!!8q@$`vX!O#q?62)!&JGgVuzTv}m=ef1@!`U4MAf^1CgUcJT|QtIBZW0Q z0$R@^WU=hTV@trP(VbHluHhvg8+X(!l}M=WjG_;yW+sDUe&>8uW|{T zG2v+`AM%5;rn&a(D#Ru_ygOcBV1ka<>?p?p)BdAT;xpwgnXSW~c8UV;90g@fFcmkG zqvvVndoFv?F`g#}Pde6iy_L_k#2nVDL-uS}og}cj*-XbIsnx$Hz|u zZ+9b`Gpg6b-M>kBc=U$ScuQea*Z0~rL3|vtLl50oUj9PEJnL8Zt5NXOl;>U^s2@Nf zjK+45>=L)BPgAhH67!0c4u7JjB?GWu-rtqKM`X-LoD^IZi8;a z@Nx%KAHL0GJGF!^+XRr?;rqrQP~s|cO24hriLCM%Hix)ECbDnx6vm~#RWPJl&X{Q z@RJ7P>J@e+F8kd^mXs$O>QdF^Y;2R+v+ctqUhG#o)|A=f{^iEf0XFt|^*r_66t^Bm zw_9=Lccf2Z@zxnU6P{s9a{4MFT_t(xYf+iABO9fuBdCyQf= zXH=2a-uY|S(h1P^-1m^(X21&^_-URR;hv?*x<^+_NEMw4YpVzq=%WOtRbuJ_Q#4)S zL`N0x>z7Pb_H`rXQQWeJP!eo%wB?;=ULS5rOko|`MGzSsD!aQ_WL;_A@rf%<-zb@c zyn{<8!cLmzlI3+{v)XwC>!kmZ9q2h+p|CfI(ui=@^UjwNLYRLp&JvAaQ%$B{1ZD5q zYaQvhiWRQ*$`gj;@hzn0{Q>yv(Pvr5n|)#9q{jbz>|&grkV^kr1E+bYE@edV%*ZLv z8YDAD7Y^=vRSZVvp^-TJxr|pkm}KpKK4K_ID9tWG39T(Ce5vyjpCXrNv`Fd}b>i3S zO1F&BdmT?QLE&VsAFkFiW=mrn#Yj%cIqOJacZNpc95M4*8Y*zUS?=!2F83oWnJy2` zATsc$-dl-mbORvoJMUPgG>o;ARp2C;N?Z1q%87NH>8(L;c_lNc z&!dv`%Sgq0wdJuCJ!i^zjZYyLhXqrNgA}FM;siSF2<#Oe$EK(iy6PYMe7)8^w9nJ~ zf7;yq8aXsA4YW^M@`i8;V+aSi6s-S-f^fYIMTeZt`ScWVave_7zZb(!IqbW63iU1T z+S2$oXUlcj*@-17Lk)7NAf_z~p`WA6b)VLtci5`FY&Q6(w()WteyK*$5UXd2LdkfX zy1`$yk8V9C=PhIEdrt^F^>?Gmlb6occlDY6N_rjm(mYoe?z2mtEFHH^Re4aV+%L^l zLCD%~87vQ@mNn|)r}tfc3D&LGf#0yxrvC8#5LN^Z2#kx+iS3=dg9^ic38w?L)b%FX zNE`%;`4%5HHwf8ee%Wj@tI67n(}?PFJC%Rk-t9#mY{cFml0v4gAUQ;AH!4n`+WGCJ z%)0jIk}d}x4YS|o`Ob9tJDSVq=^?<^^ws);=>E4;aND|_;RB8X8L!&8SdmMgoA9^k z(wjd$X{VYwC>Qx%R>*tff!w<<$0MmL0Wz9(010bvmG-7>D7gH z3g?#}7_VlxP<*HfZgEkLHD&9~RGIGDr(#vVFzUi?JLuvTZOK!d0WH}kB=XRftst6L zpP_xYMlyEd4jF3`)a1x7KA*M~kAHl4h=Q*z5O^4WFWdLds?$>vfjnp}+u<%?GpC9U zlUr@uety5a@2JN0SPz}Q&euU5Kr)CR>Qub!y>HTQh|=BEvKDARpjnKspKj(er{u2k zwF;l+_t#(@;R=RuFH_T%as|{;7HngXp#o)l;i1J>y=1sifuFi3gejZ~Y2E;xug-&2 zzPlAkT6Bw-ru{141qWpS*4t|#htSsZAq+p1VpNlAJ7QfRm21(7t|!^toz2J{?oQn?WRSSrWK9a=f7v9ghK*jSx*`bOa5elLhwV#2V&oR23}}hJ8%GvtGKb zqmxO{_g|aRLU0waS(CR%lv=`yafS4)wtX0L`&~(&BX3K0l;n-F8vCl7zDX|(Umn-s zn%3X22L&-n`_d;7&C2?*_77kO1+>y&aPi>5lC95tXc;1kC8UhSH}<{69#c*0PC2^!i2i8CP^p9WXBh9Xz}o3`=%(x&<=XqaM`6!lXIi13+4FR+uqFi z)X_|5e0ix{4<)ongCRNqf+lzCOuP*y9I@z~NhJ8O^9ysI%%MG_g43!39_f^VlZ$5e?G)|o(lv@g@o&)Il00!{I7#tn6lmj7Q$)-2 zJaymZl8hSU>;67!9aY2d8S$yzAmL+X5xQnq81>L1F6X@RL5#>ph6e)R=Cxh9au)G^ zZ}MBb{;=d|r+H^{z{}e&54J;KTLbE=(_K)HyH{kWdK8P?%*^IGuWp)8@QJVDEuE@q zy0k|!sO}4=(k>MEuIPgDZwKJD7DYaOvHJ3^vg5;to^O6zZtq>`d-@AUMJCVynD>Y` zn~f){AB>PrCTe`UcNDu?1%R0UjI4=l`@jlB_O zVYx^lZhPQeP)kB1uKxqDwHHZI#jlC~uwSl4);)6M$R=Mw;WAe%^LNSe zWK=j}3*izD>!O|Zcy!lBg58jMA?reY3-~QpIfy1llLem=Qu!jVf z=}UD}-Og`}5N#TpHt7`6C(P~JTRug`b_V5A6GU90P++f88I|0lPh^SY#|Vhex_s|FwxkCLtUwadM8qKUrpFdw zrf?61WRub4<4^E1e3j7U`IymGg7T`3O){Rs-~@y~B{^1Fglijvg#Yc_RGJ`g0ls+@ znFH`OP@lhMjOJqoINWYrcdYIY;6X2YW#X>#_$Wmzv|~Z3Xn`v`?$UgA**@)Vvputh z+J3W@NB<5V9AvZKLFb&AzuVdlS)Y9)B{`Lz#^idK!U85WxxkneWlAqvN)y)FPUS=| zerwg*)t~qwn%FKSNSvpWmb@OjBsoAm(dCQAD#@q<@2LlqI^XvF1q=SQoPPK2@AD>H z#Wpb)HKUhFVx*>b2yQ9M_|rnKMAc7?eugxE65|LY1ua%TOHa!o@5>1%taWt!o;^CC zsCS&#s$AOMOtwdlzvp!>L&k$zB{G|hI&ORP8DrIu5az5}>ck%Ae*<;2T<-9C zA+afshbk2-%gWHX%@{#9tX&0akHQrOq1iF%;XKXP-6>9bn~ZGaG75L7I|)6&W{pKk zDa_;LH3rCbGE?Y!@&`|U)h|Jt11eZ--;0vU`v;3^C`F2?HP#T z=X4~%q@S|`wE}lC?ldoiD7+#<2H9dB83Ca0P4`Ey%Z@jxLA!pe+%=K9RKB*k+DsxZry5+XDpmKC#mQV zL5ol(l3(_?JRl>x-H0&@Xb;}pBPqa=>csm?G}a#WhKN*%VOOs| zG)s+6cWyF@{6#lVxJ7`*pq7sE!4YuinXzFKkTk;o2&Z4DvsBrc#+sMMNFsV;Dx>|Y zlF1nx15ao7s4&kko}n6iYb?oRVhqmzq8bxW;00t=6ZO!wD=MD!WpLdWtooQYRn_+% z6=&+c_Kk7qJ4DT-HXylvi(Gz!zM#$3j4BX*JyM8#wNg4#RktmC%Lrs2iNfnb>@wIC z*b2>b=stOO{C;(@FN{$3$${#;3#FQ+d% zCfJl>U%~CRkiZZROJC zevm=FqHpqLn&bgc0nAl>s(L{1OTbM1T<41CISbtk- zWu0DC#lg-x<5E{EGLZZ!6wa8Z!sUeS6q7&SsVaSHdE5D-fLg+&-HpByX~`NeM;FOV zR_o3sbm;wanW-RPgSZHChdxXOWUQ;`Leg4v4^ggqEJju}x(n_elM>3WL&Kp>fwuLV zHew0w3LCp)P5;7q)F&@F{jtVJ^eTJiX6d_*W^|apPe{Nv!efwEi|F`Sn9yp6#7@tt z!KSyEiIQ-!VpGN}9=YZETKmBI`Yl=Tt%T>j6#xxuP6&#?hrZ(NuslD&& zuF?23`P?Oeg(mNC18aeScj6mg?bC*zR$r{xPn%94ZBS=h9Y5X%GIN4srMP+&@>TjG z_EGI(S=Y`AM>f;qeGU{Wr>QaNFWOh$nmIY*e>MPr_TifqrKQa^wAAGkRb*kTMHu6- z)Y;m_3y}i=aB=na)RLE?h7stfQJ?@680iul@CLwaY3=1MsiC3rUo^h|y?!#pSSy8oCm{%)()07Fx&{kupguv z8o}`TU%l18@Wo$j{}2Al0|gLz>S#&BawCLcYWx3&t^OOfcJOq8*}xc|0FaG~D=a>| z!N0K0UmW}wyEyy6=Jt>L%@EVpRaYC{7L`u*MNCh!Mwl#YnX=&zzyI6u>8j#@OK-)Y+&-A+Irb>bN!=&!;}I5 zkaqw4xqA%&pkx97ztjHwc`p0&=XW`bak>ov47&cu->nn?5O{#;)Bd9al>h)ZZvlYL z&;QX`b(^hP$Pw9*1 z4y;xxGXTIl7`c|-9v0i=|ByF~H2a^r{ah>#ec5J>nB%M}6UgNTHNgao^4 z#lRx`f0jR=U@TX_IA9zB4o2|&FP1C$UyAQP^0yX$1;%hifJcD?pu)JKr2poM{%hp# z^FI_3^C9__v#8+z4NF6?3 zvmFfBi*%zznay;?aEo1br;WnA)GJo_40^SAs!mb>V|PYf<&JjVT3HP_1o%~NWbA;& ze8JiZVxjQ_8^OE#|uzA?utqQLekEg<*6( z$tWJ}qwR-|(-=z_aY=`CWeKQ1!lP%V6+Lz_1<)4NH6@t+!dZKDTpazLjR>=q0Ah;U z$}m4QySiKwH`c2Pj-kJ3cashclL|G}E#zR07U(O-dh5pPSY*$4A)I5l!4batAj#vz z8V$9L&LbRm_&_1>JHzTCbCLHZ8oTRqx zwE&r~*OdylKcze#JztVObrF1>`4J-gG3LW!);qhY6e|cly1lL9TQ}KDXht^cSu7?t z6}C3>LjNr_k`3V32==c&dqh1mxnt!?>9tkzEGkmA;zhkdRcqYUn zv$t1iJR#Uq{~~fuN2h{X(EB5rvTy2f0Auw27|J<`BBc{zJ{h$}077p~^0?bnX}AfB zyb}^awyU3rrl<^4@;kp1+OJ*;&QlFExy|;?fZ&Y$c2jO%K3=`;Ki_$}F+}n$vn_Aj z@hTa254k&j4)xS=QJvgJGdsqntPu@MU1_CSuek^pMd!r);CG6C7lKKW7UDNH^XRn@ zZbhDUq-#r%GWov!j#Yf_@tvKA!_7Ol`EPBBqWjh3PpxlGO7;o8F@s^mmM?S9&%RG- z9~JK!d+BEp$F+<*-RRo21eZ*u>v;LcemY*1tA4+Vr~Hof3P|j=bh>T9tDQrRTRroB zGQ{tT^{pkl@1>QFwYzA+lxa3cygXRm-4_18>-= zIab9Yu(9RzJ!V&g>#a>M9O5%gtq0%Sdj{m^Yp3XZ3550n~Gz-Vky)URsr`eEj3p9-pTUwJF?nU)WUz>#o}(Q?6umpqA6z>%h}~e4+l;eoK#x z{M@e2Y%&t39jnLX1(fJ>jTx}E-}X1*v=DhWkfJwy`V{afL^4l*OgFqHBRc**M`=F~ z=Z%Z?rN#%B@@>j@OGxielZ6oL9gwGvICsQ~JdZGXNmQsTlY5gd`+x&3c$2$Tw#+F0 z%Bmq6q<24SMDsb%NJ*E@8XD%Bjyqqg!OrJ(DwMZY>QqC!qsS*I98{1jq<`-Rf3EgR zVxDhS@W=yFi|Ns#tXe0%IWWXS(^M5wEzba^f0unMPsrr;rOo>vT)ks^Wo;9#J7aZh z+qTiMZQJhHwlQN{9a|mSM#r|D^v?UNwLk1V{=zuwx~s0LIty^uAApI2vfxXnC=xIu zQy()1A`wIjotvVT^-0-o`ZsmRZmCNAx%|&97S;l75G8FI9jb25rjA+kiTrp0@0Lzs zvdEJ%3uag`yw4|gEJ?d|w4jl@2RtBkjPF@gpSw&ab@QZV@ErxoTpw3xAm5tT9da#UEuyX~E49|W>c0JBA|J-+xj%WRMq?ejS#@h>1A7n3&a9>2EJwch9 z>`rUtbURUg3M}{RmxFc*{pv{J7E?b*5N0&z6P#iHiypvl$4NadL39iw>I`e6oTgtrEfW$Gp)f5xhDxvFRsVmry!#1CLK!e_m- z49+6uRE+%ZsKehbufHDc*^rF-Ewuz?6BDPAc`SFP4(Z^AvIe8nx*8TD{TZCWsc;_l zVw9Pt&=pu)t}GT8xcTX9b#7wxTpHoJNdD@83ujQ-w1NN_72bDiJ&y_Zru|z9g+RcGK_khl%A$ zD=M^u&w9KpIA}1L-y#7}0StP_- z%iWA92_1!!Ngy!NdhVesHK5Fp%B)55tF6~V2(6capI`{kD>hPv=^xhU%boI-50mH& zt*zp!HHZE*0}`m`>+gE(LQsEk@&^lFeA0Ollst2)F7PqVU?STM4cSC7hwFVvPbmJ>Q{p6(iinu>y4_=06inhK^u)oY?VqbSR)EK>*)UAEMHoTnqlIt2 zI#*B+z{cEXDlyN3a}z9JkoLO7I&!(e3zKr5-I8b8)JS9QP%U~|9c-0h_tGe-;eq{v z4AnXI^dZA7Dv`aZ9nOiqq3Ebjtht(7`UjX9!B$sKOHBT|+NIsnsecj_&$bQBRF^?K z75gP4q=h;0CtA*rq6VV~jftD{yl)}A`kl{O zm+Eg-j)&j*Y?cXK^v_|g-cx(M)JvO_8Z8qgr|>>xPbg_EuOXc;a<9ME`o~1*m(|L_ z&0ll>0MPkca-Az3*ofE=ye9;(IfqE^#3~*;lgwsl&l(+lH+R$ujy0g;Er#g(1aMJV z;LQ?U(1&hm?CYlZ&S({BWe(atx6v;vFReI_&UCC<-e?bTUFP+C4bA69PxsVPq;7ysdv;@8iB4ZIih?_d9fPyEx%k-HOfvIm;m=@FAu zT7B#EB#SQ=#aI2tvDbfqpoP5e+zl4{T=;oKtncW!L~3V$;3s0wwLzT>;xO|K>R3be zAS$2YQW;T_E~YV))uU5vsTn)MHIN`(kdED?h~voJ# z88{v#0lgDaG)FKvFD@CAOb4tDCL2pSP@+atl}CER3gW9kqH;OOtkQ(Fq}k4*fUl`p!@US^mURsrPwQiqA5cUv4jIy)Qdxm&urO=cE}FFIKy;zq$TI4O%Aj6$YA6`eRdC0MPDO7PX;uSOW! zB%$_ubWGC5at<5C2dKp2FOu;=TaRGp+`Y-HZ^w4DQCS4}z<7IZhF`!f zU2%-}BPCdr zg|vWV|F?4(aVnXo1jpfW;Bw#_qZJZLiOMMemwwFVN|?-ArH|0$i?Z}U?{IP^_ROM> zP{}i@9r;AMxe$vNcM@;cfYT$ru0+zdjZ2L{rENX_dzku}vJ5XSn-5VAc|rC>EQkC; z*@-85Y*8P2Oeu+WjPgVqI8uSe*@qFui|x#jMv|21^1OJk;IAwjPidE}!sBl*t3>6+ zHh(9;3uExBG#o)bTRvNH;mO2U#5RU8g-demHw1+Vg;ZM_b$WxmUb(z7Z}elAnS4+CCBnj$7oyB|hCy_fpx< zZClA)id~YCw^UbPCN<2|CKj8>y8POS9zrgS+7YM52pw3DE0j>Z9%qqI9P8Q3Bq&xY zzZ3P?`15KacJX>4Tu=BgS-+z)YK2i7$TplzN@!*mC|Q__92rH4vtyDFZPGAWN|nPV zBR~H82i<8wg+wrnAc^<5s%iS+NfSk_d90Xhq&gKUNyNIUgj>(R&J{b^D^gSW(DpCP zi-FaqJ;QKZA6R;Hl1z4F-6%UpX=m<4tn|iuXT6wWXYD(KLZ1vNJ4}Zc0f@K1nX^t& z38^WLL%3|`Y_WPcQJEJ4O7QO!HgsOGzz;Z*U=!XkNYYrMq&sa1i=#64as|SXf z{}+MFyj!+hmU>mP;bb?T^cqQHDV4&O7alEZQ@Ng5Vi=hRenMe60vJ+-PLkwU$(4nE z*M?{w+rB(hnE~S0l!UF(?6zoP0mw{?>4gF1&kLX&>HcXAmoiEbjcGGH$KeFUDf}y2 zudptm0G_!4dGn5jI5rQP|KDQk>4F#q2%WWGID$pAF8&D%ZItH@k#L?kFg*u3_+!2q znng+}w`^@0;!PN($--8SzJrGr1JSwl#l6eyFX_v-7D;1^59_8y^6}A_(q!nwpBdc7 z6*x418}!CdRU`Zc8O6Jp7A?OOi}yeB;5VWV91QAzy7fO>7669)%^d_s{i#I4B5Z_2 z>J*5kTmV7Fs$!hjhwfZYM9#J=GI#TT%>_t7Q22q{t_FzepD9VTXf)*FgIaMn0#eGQ zI&n#hFD=@nuFL2}@4{tNh^iB}*h>z~Of6bZ^-ac)5jNNBu&QlQseo-~qAzNqQ_CTZ z=7xzZ`YUP{xTthel`dOqaV_Cy5(MKvnFjehj^P8H{NIfW%wvf(~t zEbb+I14v#ToA5d&9I8Ka12)Z4hMFqg#2I{>V&$_ajJnjX8NG8$3M+UXKoxf`vkW*F zERpEDLtbm5;)Yk!3vR4xRjT3T65J?s!t---_ndlr0wae;?#0lVPW;H^D_0ev;QEW0y>e-n#!YQq#hx(2kKBqw ze!7KaV!F~hgn}f~NBD3Bn!3cPb)Nl+;--D^x>nNe=o)Xgj?+AF7MAAd0KJ_8PH@}G ztTuA_=xtmLlPPBXU}JLl^9abCC2eT!bqXlXdTgSkcteG@(f?~g>SgD44RKl$exwy0 zAo7HY;WMZ6;&uv34-U6Jr@Aos&@e8#bQrv6&ilveRepq8cA?zqe9cHZW*TyulVvdj zxX~&V=1%Mc?ttE8dcG;7fD}Q$cuqr)U?-dMiYfIIH$vah0HuirH{GvJ)ev36S9Z5t znWjX6IjgxPI{oklxRhlxSo#>I#?jIkX{3VI8m26KMnkh&bDV{E2Fe5<0H$u9O080k z?}X{pylb)kms~pKcOUQgRpdAieY~dlur5BY@H7J3f?CfWDLO2DemI&iFBX<1!nyxE z4P`i$_K3(arQ}JpZBxqYj`{G0xMDsrjtM{?`xI$iO-0bNXxfZ};d$Ao2P1h~sMrqc z3nvG}w<0o7$6(t^<5&y52FP<_oSChGtcF?ClE7g%u=r)1cZ*kciSwKBp-V8zy~60Q z%S4KLp}a`!DKZz`8K^zJ;E(F|^oMO4~ezk?l?Z`l{Tn?X~hjWU3{?be2R z!Jd$9L|n5~yhM_avhtpW-M8Ha^b4VA&(1w4qeQj$u)bm9tWXgg=vp~QSf6I2`oucu z%5%=ugOkoMbdMmOLrlLX@Fw@Qkc9K-M%whdQY+GU_>-h@-(hliIn|n7?g4AaR`TcU zWA?a}p@yf|Z{)YNmH>vo#}p+x6|*)??7hg=S%)BqiKIqbM7?S13!YAr8%6FZ^oKlR zVzM>uxKo<61oQF4Ypf3R4ZVwfu8Inp3cEBI4%7?C++qDygz60?rz%{ZwfH)rWv?z3 z+%c=QX1B8K?6!Pc(4Kg>IyOL10^$>z1*7JF08JZQwcZS?-^HTE`Ev>!oqc6#kFk?> zUcv@17ILzE)==YApd(AyJi1~#=c*03J5%#NXu>Gf2e*uAY#gY8V(mP>M2G7oHfA=L zAmnffl8}rn^~!$pnY>%+xBB{fj^a5QXc1$L+t)B8D?yiuPpLUw(mk;|#4IbP%!8e@QG45Km-1Vu!duwaFix7zZ^2WDcOu)DX~`-H9I#C!iSw+% z7e*B%HFFLrwx;`M?A@p+IafNsEITy^8Pj2sxHomS!FxL}9Yv4FU1@^s>YP%~w^Wqg zIF9X2dW49zw4P&1Rj=FKFh8u0SLjL4KXlq5=M2}asAMJ@8bh|5UxOigKL!nIintA# zITxo6o?}fu=L)?d0wR2vb6+6sxI8OMl*dDIP0+VhMcCx=rv!gN{I(ofe_;es2`jkEv0$jCWi6|L)o4K`aTUC z<>wQLU$~$hb-dVJbcd4&)BPz?mr<`qEHWnBr2T+UPlhwZ+au}}6{ z7M8^vRT&4>_CjMAFJ0I6jT;IEI*sNWt{xkKv;^A>7EgkjRjPX7IIY0^S za?tO&;UH?2=(ZDC_!bGmogtiQ-Phu-?B?@oKgHpg8(qSiE8l>k-h31E-1@+i8+neu?npHL^qk6uJ)B)boZ-5Bvpu^>%mTPN}aM3*|yEc7-`&5%XO2zh+^~ z%EmeF%?Pq$E;^qx_gZ@Bx3%Il9E|t|hj~M>weQ1us*ru;@1}T%=e10npjQ`vx{?-` zS|=FbZa$_)#oBHELdS$v!vkH7HJgg}WzX2(;lUabw^<;fBmPmzn+&P`wJ_c}X|0_~ zYqGQaz0%WEffCcLdi@wui}w7}F&~8CLn3@3^N{IqvD(Y9&wmhp!VOGcs{)`+e>YTp zdGsbe5#%K6mC^f__u}f{ZP3-ISlvA(M@q#*jxVwO51@kGPGY|e+w##6wLKg}t*yIE zG3fT3eHO4#_jvMSc&0Uh;wICFlOFT6L*971V5K6x{jhD5EBL5_pT3DvE>zD2Bj()5 z$@mAY0<7`9`mH;%<3sbnL&{cBc$J+lJ|nkrvdNj}Jr>yKE)l*F4SO0tXSOH*AB-0p zwig=hXI434uu1qe4Jnv6y;g9vl^CWYmx zTnvOfR{?2e1n5mws&DIe3tDA=nwym9Z|2S3`mtx;OqMz5q0uo< zOgfmof7Ja0Y$~az-<@9y=%w;%JI6C-%B@{?5knG-2Z+A;@YzMQrLs4Vgo^HSB^RmV z1wJqKy-jxY!cFvJs-GiQwNS~`+RXetCs#0ZeIYT}9DryJP7KQ;mn*`BRsYg&O{a3# zWz5Qh-*-M;gm%}fMI-;ya^V0K$=J>U z>0j){hhF7p{_8N{mXh@iCWp7^fmYDt^ q)#Ca_;aT4B!NDrm1oCN7LM9fnteQZ? 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 literal 0 HcmV?d00001 diff --git a/docs/theme/main.html b/docs/theme/main.html index e8984f4b1f..c604de407e 100644 --- a/docs/theme/main.html +++ b/docs/theme/main.html @@ -3,7 +3,7 @@ {% block content %} {# no ad on the home page #} {% if not page.is_index %} -
+ {% endif %} {{ super() }} {% endblock %} diff --git a/mkdocs.yml b/mkdocs.yml index 2e5933b798..b89bc50d73 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,6 +55,8 @@ nav: - usage/postponed_annotations.md - 'Usage with mypy': usage/mypy.md - 'Usage with devtools': usage/devtools.md +- Blog: + - blog/pydantic-v2.md - Contributing to pydantic: contributing.md - 'Mypy plugin': mypy_plugin.md - 'PyCharm plugin': pycharm_plugin.md @@ -72,6 +74,9 @@ markdown_extensions: - pymdownx.highlight - pymdownx.extra - mdx_truly_sane_lists +- pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg plugins: - search From 1dd4ad7202cf1ee64c79ea5662bb77529c46dc23 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 6 Jul 2022 16:50:27 +0100 Subject: [PATCH 04/25] more section --- docs/blog/pydantic-v2.md | 67 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/docs/blog/pydantic-v2.md b/docs/blog/pydantic-v2.md index e66566fa71..ec986ebe0f 100644 --- a/docs/blog/pydantic-v2.md +++ b/docs/blog/pydantic-v2.md @@ -184,6 +184,28 @@ Basically anywhere were you don't care about a traditional model class. We'll need to add standalone methods for generating json schema and dumping these objects to JSON etc. +### Required vs. Nullable Cleanup :smiley: + +Pydantic previously had a somewhat confused idea about "required" vs. "nullable". This mostly resulted from +my misgivings about marking a field as `Optional[int]` but requiring a value to be provided but allowing it to be +`None`. + +In pydantic V2, pydantic will move to match dataclasses, thus: + +```py title="Required vs. Nullable" +from pydantic import BaseModel + +class Foo(BaseModel): + # required, cannot be None + f1: str + # required, can be None - same as `Optional[str]` or `Union[str, None]` + f2: str | None + # optional, can be None + f3: str | None = None + # optional, but cannot be none + f4: str = None +``` + ### Validator Function Improvements :smiley: :smiley: :smiley: This is one of the changes in pydantic V2 that I'm most excited about, I've been talking about something @@ -302,7 +324,43 @@ This wouldn't replace the current example-based somewhat informal documentation ### Error descriptions :smiley: -TODO +The way line errors (the individual errors with a `ValidationError`) are built has become much more sophisticated +in pydantic-core. + +There's a well=defined +[set of error codes and messages](https://github.com/samuelcolvin/pydantic-core/blob/main/src/errors/kinds.rs). + +More will be added when other type are validated via pure python validators in pydantic. + +I would like to add a dedicated section to the documentation with extra information for each type of error. + +This would be another key in a line error `documentation`, which would link to the appropriate section in the +docs. + +Thus, errors might look like: + +```py +[ + { + 'kind': 'greater_than_equal', + 'loc': ['age'], + 'message': 'Value must be greater than or equal to 18', + 'input_value': 11, + 'context': {'ge': 18}, + 'documentation': 'https://pydantic.dev/errors/#greater_than_equal', + }, + { + 'kind': 'bool_parsing', + 'loc': ['is_developer'], + 'message': 'Value must be a valid boolean, unable to interpret input', + 'input_value': 'foobar', + 'documentation': 'https://pydantic.dev/errors/#bool_parsing', + }, +] +``` + +(I own the `pydantic.dev` domain and will use it for at least these errors so that even if the docs URL +changes, the error will still link to the correct place.) ### No pure python implementation :frowning: @@ -402,8 +460,13 @@ which is a type defining the schema for validation schemas. and a validation error is raised 2. The reason I've been so keen to get pydantic-core to compile and run with wasm is that I want all examples in the docs of pydantic V2 to be editable and runnable in the browser -3. `from_orm` has become `from_attributes` and is now defined at schema generation time +3. Full (pun intended) support for `TypedDict`, including `full=False` - e.g. omitted keys +4. `from_orm` has become `from_attributes` and is now defined at schema generation time (either via model config or field config) +5. `input_value` has been added to each line error in a `ValidationError` which should provide. +6. `on_error` logic in a schema which allows either a default value to be used in the event of an error, + or that value to be omitted (in the case of a `full=False` `TypeDict`), + [#151](https://github.com/samuelcolvin/pydantic-core/issues/151) ## Removed Features :neutral_face: From 3a77a579843bf1c0038ba1f331d2d42a19fd7052 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 6 Jul 2022 18:20:45 +0100 Subject: [PATCH 05/25] completing conversion table --- docs/blog/pydantic-v2.md | 103 ++++++++++++++++++++++++++++++++++++++- mkdocs.yml | 1 + 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/docs/blog/pydantic-v2.md b/docs/blog/pydantic-v2.md index ec986ebe0f..6f69d7f755 100644 --- a/docs/blog/pydantic-v2.md +++ b/docs/blog/pydantic-v2.md @@ -467,6 +467,8 @@ which is a type defining the schema for validation schemas. 6. `on_error` logic in a schema which allows either a default value to be used in the event of an error, or that value to be omitted (in the case of a `full=False` `TypeDict`), [#151](https://github.com/samuelcolvin/pydantic-core/issues/151) +7. `datetime`, `date`, `time` & `timedelta` validation is improved, see the + [speedate] rust library I built specifically for this purpose for more details ## Removed Features :neutral_face: @@ -481,4 +483,103 @@ which is a type defining the schema for validation schemas. ## Conversion Table -TODO +The table below provisionally defines what times are allowed to which field types. + +An updated and complete version of this table will be included in the docs for V2. + +| Field Type | Input | Mode | Input Source | Conditions | +|---------------|-------------|--------|--------------|-----------------------------------------------------------------------------| +| `str` | `str` | both | python, JSON | - | +| `str` | `bytes` | lax | python | assumes UTF-8, error on unicode decoding error | +| `str` | `bytearray` | lax | python | assumes UTF-8, error on unicode decoding error | +| `bytes` | `bytes` | both | python | - | +| `bytes` | `str` | both | JSON | - | +| `bytes` | `str` | lax | python | - | +| `bytes` | `bytearray` | lax | python | - | +| `int` | `int` | strict | python, JSON | max abs value 2^64 - `i64` is used internally, `bool` explicitly forbidden | +| `int` | `int` | lax | python, JSON | `i64` | +| `int` | `float` | lax | python, JSON | `i64`, must be exact int, e.g. `f % 1 == 0`, `nan`, `inf` raise errors | +| `int` | `Decimal` | lax | python, JSON | `i64`, must be exact int, e.g. `f % 1 == 0` | +| `int` | `bool` | lax | python, JSON | - | +| `int` | `str` | lax | python, JSON | `i64`, must be numeric only, e.g. `[0-9]+` | +| `float` | `float` | strict | python, JSON | `bool` explicitly forbidden | +| `float` | `float` | lax | python, JSON | - | +| `float` | `int` | lax | python, JSON | - | +| `float` | `str` | lax | python, JSON | must match `[0-9]+(\.[0-9]+)?` | +| `float` | `Decimal` | lax | python | - | +| `float` | `bool` | lax | python, JSON | - | +| `bool` | `bool` | both | python, JSON | - | +| `bool` | `int` | lax | python, JSON | allowed: `0, 1` | +| `bool` | `float` | lax | python, JSON | allowed: `0, 1` | +| `bool` | `Decimal` | lax | python, JSON | allowed: `0, 1` | +| `bool` | `str` | lax | python, JSON | allowed: `'f', 'n', 'no', 'off', 'false', 't', 'y', 'on', 'yes', 'true'` | +| `None` | `None` | both | python, JSON | - | +| `date` | `date` | both | python | - | +| `date` | `datetime` | lax | python | must be exact date, eg. no H, M, S, f | +| `date` | `str` | both | JSON | format `YYYY-MM-DD` | +| `date` | `str` | lax | python | format `YYYY-MM-DD` | +| `date` | `bytes` | lax | python | format `YYYY-MM-DD` (UTF-8) | +| `date` | `int` | lax | python, JSON | interpreted as seconds or ms from epoch, see [speedate], must be exact date | +| `date` | `float` | lax | python, JSON | interpreted as seconds or ms from epoch, see [speedate], must be exact date | +| `datetime` | `datetime` | both | python | - | +| `datetime` | `date` | lax | python | - | +| `datetime` | `str` | both | JSON | format `YYYY-MM-DDTHH:MM:SS.f` etc. see [speedate] | +| `datetime` | `str` | lax | python | format `YYYY-MM-DDTHH:MM:SS.f` etc. see [speedate] | +| `datetime` | `bytes` | lax | python | format `YYYY-MM-DDTHH:MM:SS.f` etc. see [speedate], (UTF-8) | +| `datetime` | `int` | lax | python, JSON | interpreted as seconds or ms from epoch, see [speedate] | +| `datetime` | `float` | lax | python, JSON | interpreted as seconds or ms from epoch, see [speedate] | +| `time` | `time` | both | python | - | +| `time` | `str` | both | JSON | format `HH:MM:SS.FFFFFF` etc. see [speedate] | +| `time` | `str` | lax | python | format `HH:MM:SS.FFFFFF` etc. see [speedate] | +| `time` | `bytes` | lax | python | format `HH:MM:SS.FFFFFF` etc. see [speedate], (UTF-8) | +| `time` | `int` | lax | python, JSON | interpreted as seconds, range 0 - 86399 | +| `time` | `float` | lax | python, JSON | interpreted as seconds, range 0 - 86399.9* | +| `time` | `Decimal` | lax | python, JSON | interpreted as seconds, range 0 - 86399.9* | +| `timedelta` | `timedelta` | both | python | - | +| `timedelta` | `str` | both | JSON | format ISO8601 etc. see [speedate] | +| `timedelta` | `str` | lax | python | format ISO8601 etc. see [speedate] | +| `timedelta` | `bytes` | lax | python | format ISO8601 etc. see [speedate], (UTF-8) | +| `timedelta` | `int` | lax | python, JSON | interpreted as seconds | +| `timedelta` | `float` | lax | python, JSON | interpreted as seconds | +| `timedelta` | `Decimal` | lax | python, JSON | interpreted as seconds | +| `dict` | `dict` | both | python | - | +| `dict` | `Object` | both | JSON | - | +| `dict` | `mapping` | lax | python | must implement the mapping interface and have an `items()` method | +| `TypeDict` | `dict` | both | python | - | +| `TypeDict` | `Object` | both | JSON | - | +| `TypeDict` | `Any` | both | python | builtins not allowed, uses `getattr`, requires `from_attributes=True` | +| `TypeDict` | `mapping` | lax | python | must implement the mapping interface and have an `items()` method | +| `list` | `list` | both | python | - | +| `list` | `Array` | both | JSON | - | +| `list` | `tuple` | lax | python | - | +| `list` | `set` | lax | python | - | +| `list` | `frozenset` | lax | python | - | +| `list` | `dict_keys` | lax | python | - | +| `tuple` | `tuple` | both | python | - | +| `tuple` | `Array` | both | JSON | - | +| `tuple` | `list` | lax | python | - | +| `tuple` | `set` | lax | python | - | +| `tuple` | `frozenset` | lax | python | - | +| `tuple` | `dict_keys` | lax | python | - | +| `set` | `set` | both | python | - | +| `set` | `Array` | both | JSON | - | +| `set` | `list` | lax | python | - | +| `set` | `tuple` | lax | python | - | +| `set` | `frozenset` | lax | python | - | +| `set` | `dict_keys` | lax | python | - | +| `frozenset` | `frozenset` | both | python | - | +| `frozenset` | `Array` | both | JSON | - | +| `frozenset` | `list` | lax | python | - | +| `frozenset` | `tuple` | lax | python | - | +| `frozenset` | `set` | lax | python | - | +| `frozenset` | `dict_keys` | lax | python | - | +| `is_instance` | `Any` | both | python | `isinstance()` check returns `True` | +| `is_instance` | - | both | JSON | never valid | +| `callable` | `Any` | both | python | `callable()` check returns `True` | +| `callable` | - | both | JSON | never valid | + +!!!note + The `ModelClass` validator (use to create instances of a class) uses the `TypeDict` validator, then creates an instance + with `__dict__` and `__fields_set__` set, so same rules apply as `TypeDict`. + +[speedate]: https://docs.rs/speedate/latest/speedate/ diff --git a/mkdocs.yml b/mkdocs.yml index b89bc50d73..5f16165fa7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,7 @@ nav: - changelog.md markdown_extensions: +- tables - markdown_include.include: base_path: docs - toc: From 8f6a9eff720fe6a52e0274656b3564ccaabd3e79 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 6 Jul 2022 18:29:25 +0100 Subject: [PATCH 06/25] prompt build --- .github/workflows/ci.yml | 1 + docs/blog/pydantic-v2.md | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5cb3916630..43b1780956 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: - master - 1.9.X-fixes + - pydantic-v2-blog tags: - '**' diff --git a/docs/blog/pydantic-v2.md b/docs/blog/pydantic-v2.md index 6f69d7f755..7628a4dd70 100644 --- a/docs/blog/pydantic-v2.md +++ b/docs/blog/pydantic-v2.md @@ -1,4 +1,4 @@ -# Pydantic V2 +# Pydantic V2 Plan @@ -23,6 +23,18 @@ I owe people a proper explanation of the plan for V2: Here goes... +--- + +Enormous thanks to +[Eric Jolibois](https://github.com/PrettyWood), [Laurence Watson](https://github.com/Rabscuttler), +[Sebastián Ramírez](https://github.com/tiangolo), [Adrian Garcia Badaracco](https://github.com/adriangb), +[Tom Hamilton Stubber](https://github.com/tomhamiltonstubber), [Zac Hatfield-Dodds](https://github.com/Zac-HD), +[Tom](https://github.com/czotomo) & [Hasan Ramezani](https://github.com/hramezani) +for reviewing this blog post, putting up with (and correcting) my horrible typos and making great suggestions +that have made this post and Pydantic V2 materially better. + +--- + ## Plan & Timeframe I'm currently taking a kind of sabbatical after leaving my last job to get pydantic V2 released. @@ -707,6 +719,8 @@ Some other things which will also change, IMHO for the better: 13. Computed fields, these having been an idea for a long time in pydantic - we should get them right 14. Model validation that avoids instances of subclasses leaking data (particularly important for FastAPI), see [pydantic-core#155](https://github.com/samuelcolvin/pydantic-core/issues/155) +15. We'll now follow [semvar](https://semver.org/) properly and avoid breaking changes between minor versions, + as a result, major versions will become more common ## Removed Features & Limitations :frowning: From 65514326a8c25c33ddd95e0ce1af77c412056c7c Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sun, 10 Jul 2022 11:25:03 +0100 Subject: [PATCH 24/25] suggestions from @PrettyWood --- docs/blog/pydantic-v2.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/blog/pydantic-v2.md b/docs/blog/pydantic-v2.md index 28c957cc85..302ac7e9f6 100644 --- a/docs/blog/pydantic-v2.md +++ b/docs/blog/pydantic-v2.md @@ -479,9 +479,9 @@ class BaseModel: construct a model with no validation """ @classmethod - def model_customise_schema(cls, schema: dict[str, Any]) -> dict[str, Any]: + def model_customize_schema(cls, schema: dict[str, Any]) -> dict[str, Any]: """ - new, way to customise validation, + new, way to customize validation, e.g. if you wanted to alter how the model validates certain types, or add validation for a specific type without custom types or decorated validators @@ -502,6 +502,8 @@ The following methods will be removed: * `.from_orm()` - the functionality has been moved to config, see [other improvements](#other-improvements) below * `.schema_json()` - mostly since it causes confusion between pydantic validation schema and JSON schema, and can be replaced with just `json.dumps(m.model_json_schema())` +* `.copy()` instead we'll implement `__copy__` and let people use the `copy` module + (this removes some functionality) from `copy()` but there are bugs and ambiguities with the functionality anyway ### Strict API & API documentation :thumbsup: @@ -729,29 +731,31 @@ The emoji here is just for variation, I'm not frowning about any of this, these 1. `__root__` custom root models are no longer necessary since validation on any supported data type is allowed without a model -2. `parse_file` and `parse_raw`, partially replaced with `.model_validate_json()` -3. `TypeError` are no longer considered as validation errors, but rather as internal errors, this is to better +2. `.parse_file()` and `.parse_raw()`, partially replaced with `.model_validate_json()`, + see [model methods](#model-namespace-cleanup) +3. `.schema_json()` & `.copy()`, see [model methods](#model-namespace-cleanup) +4. `TypeError` are no longer considered as validation errors, but rather as internal errors, this is to better catch errors in argument names in function validators. -4. Subclasses of builtin types like `str`, `bytes` and `int` are coerced to their parent builtin type, +5. Subclasses of builtin types like `str`, `bytes` and `int` are coerced to their parent builtin type, this is a limitation of how pydantic-core converts these types to Rust types during validation, if you have a specific need to keep the type, you can use wrap validators or custom type validation as described above -5. integers are represented in rust code as `i64`, meaning if you want to use ints where `abs(v) > 2^63 − 1` +6. integers are represented in rust code as `i64`, meaning if you want to use ints where `abs(v) > 2^63 − 1` (9,223,372,036,854,775,807), you'll need to use a [wrap validator](#validator-function-improvements) and your own logic -6. [Settings Management](https://pydantic-docs.helpmanual.io/usage/settings/) ??? - I definitely don't want to +7. [Settings Management](https://pydantic-docs.helpmanual.io/usage/settings/) ??? - I definitely don't want to remove the functionality, but it's something of a historical curiosity that it lives within pydantic, perhaps it should move to a separate package, perhaps installable alongside pydantic with `pip install pydantic[settings]`? -7. The following `Config` properties will be removed: +8. The following `Config` properties will be removed: * `fields` - it's very old (it pre-dates `Field`), can be removed * `allow_mutation` will be removed, instead `frozen` will be used - * `error_msg_templates`, it's not properly documented anyway, error messages can be customised with external logic if required + * `error_msg_templates`, it's not properly documented anyway, error messages can be customized with external logic if required * `getter_dict` - pydantic-core has hardcoded `from_attributes` logic * `json_loads` - again this is hard coded in pydantic-core * `json_dumps` - possibly * `json_encoders` - see the export "mode" discussion [above](#improvements-to-dumpingserializationexport) * `underscore_attrs_are_private` we should just choose a sensible default * `smart_union` - all unions are now "smart" -8. `dict(model)` functionality should be removed, there's a much clearer distinction now that in 2017 when I +9. `dict(model)` functionality should be removed, there's a much clearer distinction now that in 2017 when I implemented this between a model and a dict ## Features Remaining :neutral_face: From 1808116c5c7c0c5595a6ca78cf76c40abe5ed1e6 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sun, 10 Jul 2022 11:26:54 +0100 Subject: [PATCH 25/25] suggestions from @PrettyWood, model_dump_json comment --- docs/blog/pydantic-v2.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/blog/pydantic-v2.md b/docs/blog/pydantic-v2.md index 302ac7e9f6..49506499d1 100644 --- a/docs/blog/pydantic-v2.md +++ b/docs/blog/pydantic-v2.md @@ -451,8 +451,9 @@ class BaseModel: """ def model_dump_json(self, ...) -> str: """ - previously `json()`, argument as above - effectively an alias of `json.dump(self.model_dump(..., mode='json'))` + previously `json()`, arguments as above + effectively equivalent to `json.dump(self.model_dump(..., mode='json'))`, + but more performant """ def model_json_schema(self, ...) -> dict[str, Any]: """