Skip to content

Commit

Permalink
improvement: bring back wrapped aliases and custom root validators in… (
Browse files Browse the repository at this point in the history
  • Loading branch information
armandobelardo authored Aug 6, 2024
1 parent f3c992a commit 31fdab1
Show file tree
Hide file tree
Showing 247 changed files with 26,707 additions and 46 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/python-generator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,13 @@ jobs:
poetry install
- name: Unit Test - Pydantic V1
working-directory: ./generators/python
run: poetry run pytest -sv
# We have to ignore these tests as they pull in the custom config which uses a model validator,
# the syntax of which is not valid in Pydantic V1, now that we've moved to V2.
run: poetry run pytest -sv --ignore ./tests/utils/casing --ignore ./tests/sdk

- name: Install Dependencies - Pydantic V2
working-directory: ./generators/python
run: |
run: |
poetry add pydantic=^2.8.2
poetry lock
poetry install
Expand Down
16 changes: 14 additions & 2 deletions generators/python/fastapi/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.4.0] - 2024-08-05

- Improvement: The generated server code now respects the pydantic version flag, generating V1 only code and V2 only code if specified. If not, the server is generated as it is today, with compatibility for BOTH Pydantic versions. This cleans up the generated code, and brings back features liked wrapped aliases and custom root validators for V1-only servers.
```yaml
generators:
- name: fernapi/fern-fastapi-server
config:
pydantic_config:
version: "v1" # Other valid options include: "v2" and "both"
```
- Fix: Partial classes created for validation now appropriately ignore the universal root model and only create partials off true extended classes.
## [1.3.0] - 2024-08-04
- Internal: The generator has now been upgraded to use Pydantic V2 internally. Note that
- Internal: The generator has now been upgraded to use Pydantic V2 internally. Note that
there is no change to the generated code, however by leveraging Pydantic V2 you should notice
an improvement in `fern generate` times.
an improvement in `fern generate` times.

## [1.2.0] - 2024-07-31

Expand Down
2 changes: 1 addition & 1 deletion generators/python/fastapi/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.3.0
1.4.0
16 changes: 14 additions & 2 deletions generators/python/pydantic/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.3.0] - 2024-08-05

- Improvement: The generated models now respects the pydantic version flag, generating V1 only code and V2 only code if specified. If not, the models is generated as it is today, with compatibility for BOTH Pydantic versions. This cleans up the generated code, and brings back features liked wrapped aliases and custom root validators for V1-only models.

```yaml
generators:
- name: fernapi/fern-pydantic-model
config:
pydantic_config:
version: "v1" # Other valid options include: "v2" and "both"
```
## [1.2.0] - 2024-08-04
- Internal: The generator has now been upgraded to use Pydantic V2 internally. Note that
- Internal: The generator has now been upgraded to use Pydantic V2 internally. Note that
there is no change to the generated code, however by leveraging Pydantic V2 you should notice
an improvement in `fern generate` times.
an improvement in `fern generate` times.

## [1.1.0-rc0] - 2024-07-31

Expand Down
2 changes: 1 addition & 1 deletion generators/python/pydantic/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.2.0
1.3.0
14 changes: 14 additions & 0 deletions generators/python/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.5.0] - 2024-08-05

- Improvement: The generated SDK now respects the pydantic version flag, generating V1 only code and V2 only code if specified. If not, the SDK is generated as it is today, with compatibility for BOTH Pydantic versions. This cleans up the generated code, and brings back features liked wrapped aliases for V1-only SDKs.

Pydantic compatibility can be specified through the config below:

```yaml
generators:
- name: fernapi/fern-python-sdk
config:
pydantic_config:
version: "v1" # Other valid options include: "v2" and "both"
```
## [3.4.2] - 2024-08-05
- Fix: The Python generator now instantiates `Any` types as `Optional[Any]` to be able to meet some breaks in Pydantic V2.
Expand Down
2 changes: 1 addition & 1 deletion generators/python/sdk/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.4.2
3.5.0
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from typing import Literal, Optional

from typing_extensions import Self

import pydantic

from ...external_dependencies.pydantic import PydanticVersionCompatibility
Expand All @@ -13,6 +15,20 @@ class BasePydanticModelCustomConfig(pydantic.BaseModel):
require_optional_fields: bool = False
use_str_enums: bool = True

wrapped_aliases: bool = False

@pydantic.model_validator(mode="after")
def check_wrapped_aliases_v1_only(self) -> Self:
version_compat = self.version
use_wrapped_aliases = self.wrapped_aliases

if use_wrapped_aliases and version_compat != PydanticVersionCompatibility.V1:
raise ValueError(
"Wrapped aliases are only supported in Pydantic V1, please update your `version` field to be 'v1' to continue using wrapped aliases."
)

return self


class PydanticModelCustomConfig(BasePydanticModelCustomConfig):
include_validators: bool = False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@
import fern.ir.resources as ir_types

from fern_python.codegen import AST, LocalClassReference, SourceFile
from fern_python.external_dependencies.pydantic import PydanticVersionCompatibility
from fern_python.pydantic_codegen import PydanticField, PydanticModel

from ..context import PydanticGeneratorContext
from .custom_config import PydanticModelCustomConfig
from .validators import PydanticValidatorsGenerator, ValidatorsGenerator
from .validators import (
PydanticV1CustomRootTypeValidatorsGenerator,
PydanticValidatorsGenerator,
ValidatorsGenerator,
)


class FernAwarePydanticModel:
Expand Down Expand Up @@ -43,6 +48,11 @@ def __init__(
snippet: Optional[str] = None,
include_model_config: Optional[bool] = True,
force_update_forward_refs: bool = False,
# Allow overriding the base model from the unchecked base model, or the typical
# pydantic base model to the universal root model if needed. This is used instead
# of `base_models` since that field is used for true `extends` declared within
# the IR, and used as such when constructing partial classes for validators within FastAPI.
pydantic_base_model_override: Optional[AST.ClassReference] = None,
):
self._class_name = class_name
self._type_name = type_name
Expand Down Expand Up @@ -71,7 +81,8 @@ def __init__(
frozen=custom_config.frozen,
orm_mode=custom_config.orm_mode,
smart_union=custom_config.smart_union,
pydantic_base_model=self._context.core_utilities.get_unchecked_pydantic_base_model(),
pydantic_base_model=pydantic_base_model_override
or self._context.core_utilities.get_unchecked_pydantic_base_model(),
require_optional_fields=custom_config.require_optional_fields,
is_pydantic_v2=self._context.core_utilities.get_is_pydantic_v2(),
universal_field_validator=self._context.core_utilities.universal_field_validator,
Expand Down Expand Up @@ -189,6 +200,23 @@ def add_method_unsafe(
) -> AST.FunctionDeclaration:
return self._pydantic_model.add_method(declaration=declaration, decorator=decorator)

def set_root_type_v1_only(
self,
root_type: ir_types.TypeReference,
annotation: Optional[AST.Expression] = None,
is_forward_ref: bool = False,
) -> None:
self.set_root_type_unsafe_v1_only(
root_type=self.get_type_hint_for_type_reference(root_type),
annotation=annotation,
is_forward_ref=is_forward_ref,
)

def set_root_type_unsafe_v1_only(
self, root_type: AST.TypeHint, annotation: Optional[AST.Expression] = None, is_forward_ref: bool = False
) -> None:
self._pydantic_model.set_root_type_unsafe_v1_only(root_type=root_type, annotation=annotation)

def add_ghost_reference(self, type_id: ir_types.TypeId) -> None:
self._pydantic_model.add_ghost_reference(
self.get_class_reference_for_type_id(type_id),
Expand All @@ -203,15 +231,22 @@ def finish(self) -> None:
self._pydantic_model.finish()

def _get_validators_generator(self) -> ValidatorsGenerator:
unique_name = []
if self._type_name is not None:
unique_name = [path.snake_case.unsafe_name for path in self._type_name.fern_filepath.package_path]
unique_name.append(self._type_name.name.snake_case.unsafe_name)
return PydanticValidatorsGenerator(
model=self._pydantic_model,
extended_pydantic_fields=self._get_extended_pydantic_fields(self._extends or []),
unique_name=unique_name,
)
v1_root_type = self._pydantic_model.get_root_type_unsafe_v1_only()
if v1_root_type is not None and self._custom_config.version == PydanticVersionCompatibility.V1:
return PydanticV1CustomRootTypeValidatorsGenerator(
model=self._pydantic_model,
root_type=v1_root_type,
)
else:
unique_name = []
if self._type_name is not None:
unique_name = [path.snake_case.unsafe_name for path in self._type_name.fern_filepath.package_path]
unique_name.append(self._type_name.name.snake_case.unsafe_name)
return PydanticValidatorsGenerator(
model=self._pydantic_model,
extended_pydantic_fields=self._get_extended_pydantic_fields(self._extends or []),
unique_name=unique_name,
)

def _get_extended_pydantic_fields(self, extends: Sequence[ir_types.DeclaredTypeName]) -> List[PydanticField]:
extended_fields: List[PydanticField] = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
ClassDeclaration,
)
from fern_python.external_dependencies import Pydantic
from fern_python.generators.pydantic_model.type_declaration_handler.type_utilities import (
declared_type_name_to_named_type,
)
from fern_python.pydantic_codegen import PydanticField, PydanticModel

from ....context import PydanticGeneratorContext
Expand Down Expand Up @@ -155,7 +158,7 @@ def generate(self) -> None:
source_file=self._source_file,
docstring=self._docs,
snippet=self._snippet,
base_models=[self._context.core_utilities.get_universal_root_model()],
pydantic_base_model_override=self._context.core_utilities.get_universal_root_model(),
# No reason to have model config overrides on the base model, but
# also Pydantic V2's RootModel doesn't allow for a lot of the configuration.
include_model_config=False,
Expand Down Expand Up @@ -230,7 +233,9 @@ def generate(self) -> None:
name=BUILDER_ARGUMENT_NAME,
type_hint=self._context.get_type_hint_for_type_reference(
ir_types.TypeReference.factory.named(
self._to_named_type(declared_type_name=declared_type_name)
declared_type_name_to_named_type(
declared_type_name=declared_type_name
)
)
),
)
Expand All @@ -244,7 +249,7 @@ def generate(self) -> None:
no_properties=lambda: None,
),
return_type=self._context.get_type_hint_for_type_reference(
ir_types.TypeReference.factory.named(self._to_named_type(self._name))
ir_types.TypeReference.factory.named(declared_type_name_to_named_type(self._name))
),
),
body=AST.CodeWriter(
Expand Down Expand Up @@ -320,7 +325,7 @@ def generate(self) -> None:
),
type=external_pydantic_model.get_type_hint_for_type_reference(
ir_types.TypeReference.factory.named(
self._to_named_type(declared_type_name=declared_type_name)
declared_type_name_to_named_type(declared_type_name=declared_type_name)
)
),
),
Expand All @@ -342,13 +347,6 @@ def generate(self) -> None:
)
)

def _to_named_type(self, declared_type_name: ir_types.DeclaredTypeName) -> ir_types.NamedType:
return ir_types.NamedType(
type_id=declared_type_name.type_id,
fern_filepath=declared_type_name.fern_filepath,
name=declared_type_name.name,
)

def _create_body_writer(
self,
single_union_type: ir_types.SingleUnionType,
Expand Down
Loading

0 comments on commit 31fdab1

Please sign in to comment.