Skip to content

Pydantic v2 #2433

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

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open

Pydantic v2 #2433

wants to merge 3 commits into from

Conversation

yaugenst-flex
Copy link
Collaborator

@yaugenst-flex yaugenst-flex commented May 8, 2025

This PR completes the migration of our codebase from Pydantic V1 to Pydantic V2.

1. Imports & Dependency Updates:

  • Standardized all Pydantic imports to use the pydantic namespace (and pydantic_settings for BaseSettings), removing pydantic.v1 compatibility imports.
  • Switched to direct component imports (e.g., from pydantic import Field, model_validator).
  • Modernized typing imports (e.g., Optional, Union, Self, TypeAlias) and other general Python type hinting.
  • Updated project-specific imports, including removal of the skip_if_fields_missing decorator and replacing annotate_type with discriminated_union.
  • Incorporated schema utilities from pydantic_core and pydantic.json_schema.

2. Field Definitions & Type-Hinting:

  • Migrated pydantic.v1.Field usage to the new pydantic.Field syntax.
  • Made field optionality explicit using Optional[T] and appropriate Field defaults (e.g., default=None, default_factory).
  • Updated constrained types (e.g., PositiveFloat, NonNegativeInt) and replaced confloat with Annotated and Field.
  • Standardized on default_factory for mutable defaults (e.g., dict, list, custom classes).
  • Converted list defaults/assignments to tuples for immutability where appropriate.
  • Defined fields with Unions of 'Traced' types for autograd compatibility.

3. Validation Logic:

  • Replaced @pydantic.v1.validator with @pydantic.field_validator, adjusting signatures and return values.
  • Migrated @pydantic.v1.root_validator to @pydantic.model_validator, specifying mode='before' or mode='after' and updating logic for field access (e.g., self.field in mode='after').
  • Adjusted validator signatures, utilized the info object, and used direct attribute access/setting (e.g. object.__setattr__). Added @classmethod where needed.
  • Removed the custom skip_if_fields_missing decorator, integrating its logic into model_validator or relying on V2's improved validation flow.
  • Consolidated custom _post_init_validators logic into @model_validator(mode='after').

4. Configuration & Settings:

  • Replaced the inner Config class with model_config = ConfigDict(...).
  • Migrated pydantic.v1.BaseSettings to pydantic_settings.BaseSettings.
  • Removed obsolete V1-specific configurations like allow_population_by_field_name.

5. Serialization & Parsing:

  • Replaced .dict() with .model_dump() and .json() (or ._json()) with .model_dump_json().
  • Updated data parsing from cls.parse_obj() to cls.model_validate().
  • Streamlined serialization logic, removing custom encoders (e.g., ndarray_encoder) in favor of V2's built-in capabilities or Annotated serializers.
  • Standardized exception handling to pydantic.ValidationError and PydanticSerializationError.

6. Schema & Custom Types:

  • Replaced __get_validators__ with __get_pydantic_core_schema__ and __modify_schema__ with __get_pydantic_json_schema__ for custom type definitions.
  • Refactored custom types using Pydantic V2's Annotated type along with BeforeValidator/AfterValidator and PlainSerializer/WrapSerializer.
  • Replaced custom constrained_array with a new array_alias factory function using Annotated.
  • Updated custom complex number types to Pydantic V2-compliant versions.
  • Updated autograd utilities (e.g., TracedDict, get_static, hasbox).
  • Utilized pydantic.PrivateAttr for non-schema model attributes.

7. Model Methods & Internal Refactors:

  • Refactored copy() and updated_copy() methods to use model_copy(update=...).
  • Adapted __init_subclass__ for V2 and updated docstring generation to use model_fields.
  • Introduced new model traversal utilities (find_paths, find_submodels).
  • Reworked __eq__ methods for V2 compatibility and updated/added __str__, __repr__, and __rich_repr__ for improved representations.
  • Replaced __fields__ access with model_fields for introspection.

@yaugenst-flex yaugenst-flex self-assigned this May 8, 2025
@yaugenst-flex yaugenst-flex added the 2.9 will go into version 2.9.* label May 8, 2025
@yaugenst-flex yaugenst-flex linked an issue May 8, 2025 that may be closed by this pull request
@yaugenst-flex yaugenst-flex force-pushed the yaugenst-flex/pydantic-v2 branch from 0a529f4 to a7e7a49 Compare May 9, 2025 07:10
@yaugenst-flex yaugenst-flex mentioned this pull request May 9, 2025
@yaugenst-flex yaugenst-flex force-pushed the yaugenst-flex/pydantic-v2 branch 2 times, most recently from 348a931 to 5ecc9f9 Compare May 9, 2025 20:08
@momchil-flex
Copy link
Collaborator

It seems like fundamentally there's just a few things that change? Maybe I'm missing something important/tricky. Could you explain if there's anything like that, and what changes going forward?

Also very important - we will certainly need to update the backend python code too and might need to update denormalizer, metadata api, and who knows what else.

@yaugenst-flex
Copy link
Collaborator Author

Yes the biggest changes are related to custom types and serialization, everything else is relatively straightforward. I'll write something up.

@yaugenst-flex yaugenst-flex force-pushed the yaugenst-flex/pydantic-v2 branch from 93ce359 to dd10b26 Compare May 14, 2025 15:36
@yaugenst-flex yaugenst-flex force-pushed the yaugenst-flex/pydantic-v2 branch 3 times, most recently from dd0cd2b to 38d502a Compare May 16, 2025 10:50
@yaugenst-flex yaugenst-flex force-pushed the yaugenst-flex/pydantic-v2 branch from ea3b4cb to 29234a6 Compare May 19, 2025 12:06
@momchil-flex momchil-flex added the rc1 1st pre-release label May 19, 2025
@yaugenst-flex yaugenst-flex force-pushed the yaugenst-flex/pydantic-v2 branch 4 times, most recently from 6d3f88a to 2967f74 Compare May 22, 2025 08:58
@yaugenst-flex yaugenst-flex force-pushed the yaugenst-flex/pydantic-v2 branch 4 times, most recently from 0a72b9c to 715c758 Compare May 26, 2025 20:24
@yaugenst-flex yaugenst-flex force-pushed the yaugenst-flex/pydantic-v2 branch from c5e2f6d to 95d9429 Compare May 28, 2025 10:05
@frederikschubertflex
Copy link
Contributor

frederikschubertflex commented May 28, 2025

I wrote a little script for some deprecated symbols but was not sure whether you have already removed them or whether they are relevant. If not, you can ignore this comment @yaugenst-flex, otherwise I think its faster if you fix them?

parse_obj_as – deprecated; prefer TypeAdapter

./tidy3d/web/core/task_core.py
54: parse_obj_as(
127: parse_obj_as(
313: return parse_obj_as(list[SimulationTask], resp)

./tidy3d/web/api/material_libray.py
44: return parse_obj_as(list[MaterialLibray], resp) if resp else None

./tidy3d/web/api/material_library.py
61: return parse_obj_as(list[MaterialLibrary], resp) if resp else None

json_encoders – replaced by serializers

./tidy3d/plugins/adjoint/components/base.py
45: json_encoders={

parse_raw – removed; use model_validate_json()

./tidy3d/plugins/dispersion/web.py
297: best_medium = PoleResidue.parse_raw(run_result["message"])

@yaugenst-flex
Copy link
Collaborator Author

thanks @frederikschubertflex that's great. the adjoint plugin one is left intentionally but i missed those parse_ methods!

@yaugenst-flex yaugenst-flex force-pushed the yaugenst-flex/pydantic-v2 branch from bf74cd4 to 2c3609f Compare May 28, 2025 11:29
Comment on lines 1150 to 1151
@classmethod
def generate_docstring(cls) -> str:
Copy link
Collaborator

@daquinteroflex daquinteroflex May 28, 2025

Choose a reason for hiding this comment

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

Thanks for migrating this! I've triggered the docs to see how it goes.

Did a small before and after check on the terminal:

Rectangular prism.
       Also base class for :class:`Simulation`, :class:`Monitor`, and :class:`Source`.

    Parameters
    ----------
    attrs : dict = {}
        Dictionary storing arbitrary metadata for a Tidy3D object. This dictionary can be freely used by the user for storing data without affecting the operation of Tidy3D as it is not used internally. Note that, unlike regular Tidy3D fields, ``attrs`` are mutable. For example, the following is allowed for setting an ``attr`` ``obj.attrs['foo'] = bar``. Also note that `Tidy3D`` will raise a ``TypeError`` if ``attrs`` contain objects that can not be serialized. One can check if ``attrs`` are serializable by calling ``obj.json()``.
    center : Union[tuple[Union[float, autograd.tracer.Box], Union[float, autograd.tracer.Box], Union[float, autograd.tracer.Box]], Box] = (0.0, 0.0, 0.0)
        [units = um].  Center of object in x, y, and z.
    size : Union[tuple[Union[pydantic.v1.types.NonNegativeFloat, autograd.tracer.Box], Union[pydantic.v1.types.NonNegativeFloat, autograd.tracer.Box], Union[pydantic.v1.types.NonNegativeFloat, autograd.tracer.Box]], Box]
        [units = um].  Size in x, y, and z directions.

    Example
    -------
    >>> b = Box(center=(1,2,3), size=(2,2,2))

after

>>> print(td.Box(size=(1,1,1)).__doc__)
Rectangular prism.
       Also base class for :class:`Simulation`, :class:`Monitor`, and :class:`Source`.

    Parameters
    ----------
    attrs : dict = dict()
        Dictionary storing arbitrary metadata for a Tidy3D object. This dictionary can be freely used by the user for storing data without affecting the operation of Tidy3D as it is not used internally. Note that, unlike regular Tidy3D fields, ``attrs`` are mutable. For example, the following is allowed for setting an ``attr`` ``obj.attrs['foo'] = bar``. Also note that `Tidy3D`` will raise a ``TypeError`` if ``attrs`` contain objects that can not be serialized. One can check if ``attrs`` are serializable by calling ``obj.json()``.
    center : Optional[tuple[Union[float, Annotated[autograd.tracer.Box, BeforeValidator(func=<function traced_alias.<locals>._validate_box_or_container at 0x734e1f5099e0>, json_schema_input_type=PydanticUndefined), PlainSerializer(func=<function traced_alias.<locals>.<lambda> at 0x734e1f509b20>, return_type=PydanticUndefined, when_used='json')], Annotated[object, BeforeValidator(func=<function traced_alias.<locals>._validate_box_or_container at 0x734e1f5099e0>, json_schema_input_type=PydanticUndefined)]], Union[float, Annotated[autograd.tracer.Box, BeforeValidator(func=<function traced_alias.<locals>._validate_box_or_container at 0x734e1f5099e0>, json_schema_input_type=PydanticUndefined), PlainSerializer(func=<function traced_alias.<locals>.<lambda> at 0x734e1f509b20>, return_type=PydanticUndefined, when_used='json')], Annotated[object, BeforeValidator(func=<function traced_alias.<locals>._validate_box_or_container at 0x734e1f5099e0>, json_schema_input_type=PydanticUndefined)]], Union[float, Annotated[autograd.tracer.Box, BeforeValidator(func=<function traced_alias.<locals>._validate_box_or_container at 0x734e1f5099e0>, json_schema_input_type=PydanticUndefined), PlainSerializer(func=<function traced_alias.<locals>.<lambda> at 0x734e1f509b20>, return_type=PydanticUndefined, when_used='json')], Annotated[object, BeforeValidator(func=<function traced_alias.<locals>._validate_box_or_container at 0x734e1f5099e0>, json_schema_input_type=PydanticUndefined)]]]] = None
        [units = um].  Center of object in x, y, and z.
    size : tuple
        [units = um].  Size in x, y, and z directions.

    Example
    -------
    >>> b = Box(center=(1,2,3), size=(2,2,2))

The parameter table reference change is kind of expected given the nature of the size of the changes. Just got to see how it renders, but I suspect there might be issues. I'm actually keen to try to straight up upgrade our docs APIs to pydantic v2 extension based on this branch so trying that now separately. This table was actually a bit of a hack so it's time to sort it out properly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oooh yeah the type annotations are now much more extensive and probably need to be trimmed down significantly. a lot of the validation is attached to the type annotations

@flexcompute flexcompute deleted a comment from github-actions bot May 28, 2025
@yaugenst-flex yaugenst-flex force-pushed the yaugenst-flex/pydantic-v2 branch from 2c3609f to 0a1b9c3 Compare May 28, 2025 16:23
@tylerflex tylerflex self-requested a review May 29, 2025 17:58
Copy link
Collaborator

@tylerflex tylerflex left a comment

Choose a reason for hiding this comment

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

I went through a bunch of files, but it's becoming clear I'm definitely not going to be able to go through all of this line by line. One question for now: what if any are the changes that a user would experience from this PR? A few that come to mind right now are:
a. I guess if the ArrayLike field is initialized as a list it will become np.ndarray instead of a tuple?
b. users will need to also use the pydantic v2 methods instead of the v1 methods, so their scripts could break if they rely on those?

are there any others? and have you run notebooks from this branch yet (out of curiosity...)?

pyproject.toml Outdated
# TODO: <yaugenst-flex> - un-ignore warnings & adjoint tests
addopts = """
--doctest-modules -n auto --dist worksteal --assert=plain -m 'not numerical' \
-W ignore::UserWarning tests --ignore=tests/test_plugins/test_adjoint.py
Copy link
Collaborator

Choose a reason for hiding this comment

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

not sure if previous comment got through , are we planning to ignore these warnings indefinitely?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no this needs to go before 2.9, it's just for the rc's currently

Box.__repr__ = Box.__str__


def traced_alias(base_alias, *, name: Optional[str] = None) -> TypeAlias:
Copy link
Collaborator

Choose a reason for hiding this comment

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

what is this doing?

@yaugenst-flex yaugenst-flex added the ignore_diff_coverage Ignores CI comments on diff coverage label May 30, 2025
@flexcompute flexcompute deleted a comment from github-actions bot May 30, 2025
@flexcompute flexcompute deleted a comment from github-actions bot May 30, 2025
@flexcompute flexcompute deleted a comment from github-actions bot May 30, 2025
@yaugenst-flex yaugenst-flex force-pushed the yaugenst-flex/pydantic-v2 branch 2 times, most recently from de2a33a to e3bd958 Compare May 30, 2025 15:50
@yaugenst-flex yaugenst-flex force-pushed the yaugenst-flex/pydantic-v2 branch 4 times, most recently from 865f5fe to ce1f32e Compare May 31, 2025 21:22
@momchil-flex momchil-flex added rc2 2nd pre-release and removed rc1 1st pre-release labels Jun 12, 2025
@yaugenst-flex yaugenst-flex force-pushed the yaugenst-flex/pydantic-v2 branch from bc3751e to 8f478bd Compare June 13, 2025 18:39
@yaugenst-flex yaugenst-flex force-pushed the yaugenst-flex/pydantic-v2 branch 3 times, most recently from 022e586 to 46e6015 Compare June 25, 2025 06:02
add somefiles

replacing v1

more replacements

copy methods and ordering of basemodel

update get_submodels_by_hash

basemodel done (except for docs)

basemodel and modespec done

slowly but surely..

progress

next batch

going going

getting started on medium

more refactoring

new structure for medium.py

add medium

why it no work

fix medium

upgrade material library

most of it

first pass

add pydantic-settings

fixes to validators, mutable assignment, ...:

remove skip_if_fields_missing

cleaning up types & type serializaton

fix printing and serialization of autograd types

more type serialization updates

Fix traced ndarray serialization

fix equality check in basemodel

fix some v1 leftovers

fix some tests

fix equality check

sim_data tests passing

make serializer more robust and another fix for equality comparison

fix warn if none validators

Check for pydantic v2 ValidationErrors

Canonicalize coordinate handling of unstructured datasets

Simulation data tests passing

material library tests passing

fix bad name

fix material libray -> library

lotsa fixes, tests_web & test_package passing

fix remaining web test warnings

everything importable for doctests

rebase, wip

fix mutation

safer ndarray coercion and expressions fix

working on post init validation

fix multiphysics medium attribute lookup

remove test script from vcs

doctests passing

fix NedeljkovicSorefMashanovich

fix non-component tests

fix tracer serialization

remove unnecessary to_static call

the smallest changes really do take the longest

all of test_IO passing

fix caching and copy update

docstring for __init_subclass__

passing: IO, base, beam, boundaries, custom, eme, field_projection, geometry, log

passing: grid, grid_spec, heat, heat_charge, layerrefinement, lumped_element, medium

passing: meshgenerate, microwave, mode

passing: monitor, packaging, parameter_perturbation, perturbation_medium, scene, sidewall

passing: expressions

passing: smatrix, array_factor, design, dispersion_fitter, microwave, mode_solver

wip: adjoint & invdes

passing: source, structure, time_modulation

passing: types

add helpers to filter model fields

add tests for new basemodel helpers

small fixes

wip: autograd

fix waveguide

passing: autograd

rebase fixes

no more post-init validators

fix dataarray json schema

rework array constraints

wip: adjoint

ruff

chore(github): improve script  to determine test scope (#2510)

* chore(github): add script  to determine whether a PR was already approved

This step results in a full test suite run if an already approved PR receives changes to prevent breaking the main branch.

* cancel parallel triggers of the remote tests

* fix group assignment

* add python version to group assignment

fix bug when inserting snapping points near sim min boundary

fix contains check for lumped element, which was too strict (#2511)

fix[web]: use dedicated endpoint for `web.delete_old()`

Upgrade ruff 0.5.5 -> 0.11.11

Run `ruff check . --fix`

Run `ruff format .`

Move test-specifc `ruff.toml` to `per-file-ignores`

Sort test imports

Import `Literal` from `typing`

Force `from __future__ import annotations` import

Rewrite `dict()` as literal (2x faster)

Disallow function calls in default arguments

Be intentional about exception context

Upgrade all (compatible) generics to built-in types

Sort `__all__` blocks

Enable `RUF` ruleset

Prefer tuple unpacking over concatenation

Forbid implicit optional

No explicit string concatenation

Remove unnecessary `pass` statements

Remove unnecessary parantheses on raised exceptions

Disallow relative imports from parent modules

Remove unused import aliases

Add lint rule comments

Revert "Disallow function calls in default arguments"

This reverts commit 12a0a6a.

Fix some stragglers

Updated ruff version in workflow

Lint `data/` dirs

rebase fixes

sentinel pattern for function calls in default arguments

rebase fixes

fix python 3.9 compat

same behavior as v1 for `FreqArray`

Also updating FieldDataset in monitor_data/charge.py

Make `FreqArray` pure `ArrayFloat1D`

remove old material_libray.py

replace deprecated symbols

disable adjoint plugin

pin pydantic>2.8

fixing warnings

fix modesolver validators

Updating TriangularGridDataset.normal_axis and TemperatureData validator

Warning adjoint removal in apidocs

pin pydantic>=2.9, fix literal types

fixing stuff

fix: resolve circular validator dependency between Simulation and ModeSimulation

Port fix from PR #2590 to Pydantic v2 branch. The circular dependency that
wasn't problematic in Pydantic v1 becomes a real issue in v2.

Changes:
- Extract mode validation logic into centralized validate_mode_object function
- Remove circular validator from ModeSimulation._validate_mode_plane_radius
- Add static ModeSolver._validate_mode_plane_radius and _make_rotated_structures
- Update _warn_thick_pml parameter name for consistency
- Fix missing return self in _warn_rf_license validator

fix: update test expectations for Pydantic v2 validation behavior

- Update warning count in test_logging_warning_capture from 31 to 34
  The additional warnings are duplicates caused by Pydantic v2's
  different validation execution order
- Add missing bend radius validator to ModeSolver class
  The _validate_bend_radius model validator ensures bend radius
  validation occurs during ModeSolver initialization

fix: complete Pydantic v2 migration fixes for failing tests

- Fix circular validator dependency in ModeSimulation by moving
  _boundaries_for_zero_dims validator to the beginning of the class
- Make SolidSpec.capacity field optional to match develop branch behavior
- Fix pd.ValidationError references in test_heat_charge.py
- Update test_mode.py to handle both SetupError and pd.ValidationError
  for Pydantic v2 compatibility
- Clean up imports and remove duplicate ValidationError imports

fix: additional Pydantic v2 migration fixes

- Fixed merge conflict marker in mode_solver.py
- Fixed pd.Field reference in wave.py (should be Field)
- Fixed pd.ValidationError reference in test (should be ValidationError)

add dropped dependency
@yaugenst-flex yaugenst-flex force-pushed the yaugenst-flex/pydantic-v2 branch from 46e6015 to 0caf92f Compare June 26, 2025 06:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2.9 will go into version 2.9.* ignore_diff_coverage Ignores CI comments on diff coverage rc2 2nd pre-release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Full Pydantic v2 upgrade ⏫
5 participants