]> git.ipfire.org Git - thirdparty/fastapi/sqlmodel.git/commitdiff
✨ Add support for Pydantic v2 (while keeping support for v1 if v2 is not available...
authorSebastián Ramírez <tiangolo@gmail.com>
Mon, 4 Dec 2023 14:42:39 +0000 (15:42 +0100)
committerGitHub <noreply@github.com>
Mon, 4 Dec 2023 14:42:39 +0000 (15:42 +0100)
Co-authored-by: Mohamed Farahat <farahats9@yahoo.com>
Co-authored-by: Stefan Borer <stefan.borer@gmail.com>
Co-authored-by: Peter Landry <peter.landry@gmail.com>
Co-authored-by: Anton De Meester <antondemeester+github@gmail.com>
79 files changed:
.github/workflows/test.yml
docs/tutorial/fastapi/multiple-models.md
docs/tutorial/fastapi/update.md
docs/tutorial/index.md
docs_src/tutorial/fastapi/app_testing/tutorial001/main.py
docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py
docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py
docs_src/tutorial/fastapi/delete/tutorial001.py
docs_src/tutorial/fastapi/delete/tutorial001_py310.py
docs_src/tutorial/fastapi/delete/tutorial001_py39.py
docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py
docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py
docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py
docs_src/tutorial/fastapi/multiple_models/tutorial001.py
docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py
docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py
docs_src/tutorial/fastapi/multiple_models/tutorial002.py
docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py
docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py
docs_src/tutorial/fastapi/read_one/tutorial001.py
docs_src/tutorial/fastapi/read_one/tutorial001_py310.py
docs_src/tutorial/fastapi/read_one/tutorial001_py39.py
docs_src/tutorial/fastapi/relationships/tutorial001.py
docs_src/tutorial/fastapi/relationships/tutorial001_py310.py
docs_src/tutorial/fastapi/relationships/tutorial001_py39.py
docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py
docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py
docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py
docs_src/tutorial/fastapi/teams/tutorial001.py
docs_src/tutorial/fastapi/teams/tutorial001_py310.py
docs_src/tutorial/fastapi/teams/tutorial001_py39.py
docs_src/tutorial/fastapi/update/tutorial001.py
docs_src/tutorial/fastapi/update/tutorial001_py310.py
docs_src/tutorial/fastapi/update/tutorial001_py39.py
pyproject.toml
sqlmodel/_compat.py [new file with mode: 0644]
sqlmodel/main.py
tests/conftest.py
tests/test_deprecations.py [new file with mode: 0644]
tests/test_enums.py
tests/test_field_sa_relationship.py
tests/test_instance_no_args.py
tests/test_main.py
tests/test_missing_type.py
tests/test_nullable.py
tests/test_query.py
tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py
tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py
tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py
tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py
tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py
tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py
tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py
tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py
tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py
tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py
tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py
tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py
tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py
tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py
tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py
tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py
tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py
tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py
tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py
tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py
tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py
tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py
tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py
tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py
tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py
tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py
tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py
tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py
tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py
tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py
tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py
tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py
tests/test_validation.py

index 9f2688dff73b3edaf11df2b6432e9eb0d0011ac3..ade60f255968a398ee8449b47e539f7d14be9f05 100644 (file)
@@ -27,6 +27,9 @@ jobs:
           - "3.10"
           - "3.11"
           - "3.12"
+        pydantic-version:
+          - pydantic-v1
+          - pydantic-v2
       fail-fast: false
 
     steps:
@@ -57,9 +60,15 @@ jobs:
       - name: Install Dependencies
         if: steps.cache.outputs.cache-hit != 'true'
         run: python -m poetry install
+      - name: Install Pydantic v1
+        if: matrix.pydantic-version == 'pydantic-v1'
+        run: pip install "pydantic>=1.10.0,<2.0.0"
+      - name: Install Pydantic v2
+        if: matrix.pydantic-version == 'pydantic-v2'
+        run: pip install "pydantic>=2.0.2,<3.0.0"
       - name: Lint
         # Do not run on Python 3.7 as mypy behaves differently
-        if: matrix.python-version != '3.7'
+        if: matrix.python-version != '3.7' && matrix.pydantic-version == 'pydantic-v2'
         run: python -m poetry run bash scripts/lint.sh
       - run: mkdir coverage
       - name: Test
index d57d1bd9b4e925a255d78d8219af164b27f8221c..3995daa65071ecf466695fe71db0d532edc97875 100644 (file)
@@ -175,15 +175,17 @@ Now we use the type annotation `HeroCreate` for the request JSON data in the `he
 # Code below omitted 👇
 ```
 
-Then we create a new `Hero` (this is the actual **table** model that saves things to the database) using `Hero.from_orm()`.
+Then we create a new `Hero` (this is the actual **table** model that saves things to the database) using `Hero.model_validate()`.
 
-The method `.from_orm()` reads data from another object with attributes and creates a new instance of this class, in this case `Hero`.
+The method `.model_validate()` reads data from another object with attributes (or a dict) and creates a new instance of this class, in this case `Hero`.
 
-The alternative is `Hero.parse_obj()` that reads data from a dictionary.
+In this case, we have a `HeroCreate` instance in the `hero` variable. This is an object with attributes, so we use `.model_validate()` to read those attributes.
 
-But as in this case, we have a `HeroCreate` instance in the `hero` variable. This is an object with attributes, so we use `.from_orm()` to read those attributes.
+/// tip
+In versions of **SQLModel** before `0.0.14` you would use the method `.from_orm()`, but it is now deprecated and you should use `.model_validate()` instead.
+///
 
-With this, we create a new `Hero` instance (the one for the database) and put it in the variable `db_hero` from the data in the `hero` variable that is the `HeroCreate` instance we received from the request.
+We can now create a new `Hero` instance (the one for the database) and put it in the variable `db_hero` from the data in the `hero` variable that is the `HeroCreate` instance we received from the request.
 
 ```Python hl_lines="3"
 # Code above omitted 👆
index 27c413f387d41e1530d5fd9da5132a0b43b3bd76..cfcf8a98e79dc1c8f809699270729289bd159db3 100644 (file)
@@ -90,7 +90,7 @@ So, we need to read the hero from the database, with the **same logic** we used
 
 The `HeroUpdate` model has all the fields with **default values**, because they all have defaults, they are all optional, which is what we want.
 
-But that also means that if we just call `hero.dict()` we will get a dictionary that could potentially have several or all of those values with their defaults, for example:
+But that also means that if we just call `hero.model_dump()` we will get a dictionary that could potentially have several or all of those values with their defaults, for example:
 
 ```Python
 {
@@ -102,7 +102,7 @@ But that also means that if we just call `hero.dict()` we will get a dictionary
 
 And then, if we update the hero in the database with this data, we would be removing any existing values, and that's probably **not what the client intended**.
 
-But fortunately Pydantic models (and so SQLModel models) have a parameter we can pass to the `.dict()` method for that: `exclude_unset=True`.
+But fortunately Pydantic models (and so SQLModel models) have a parameter we can pass to the `.model_dump()` method for that: `exclude_unset=True`.
 
 This tells Pydantic to **not include** the values that were **not sent** by the client. Saying it another way, it would **only** include the values that were **sent by the client**.
 
@@ -112,7 +112,7 @@ So, if the client sent a JSON with no values:
 {}
 ```
 
-Then the dictionary we would get in Python using `hero.dict(exclude_unset=True)` would be:
+Then the dictionary we would get in Python using `hero.model_dump(exclude_unset=True)` would be:
 
 ```Python
 {}
@@ -126,7 +126,7 @@ But if the client sent a JSON with:
 }
 ```
 
-Then the dictionary we would get in Python using `hero.dict(exclude_unset=True)` would be:
+Then the dictionary we would get in Python using `hero.model_dump(exclude_unset=True)` would be:
 
 ```Python
 {
@@ -152,6 +152,9 @@ Then we use that to get the data that was actually sent by the client:
 
 ///
 
+/// tip
+Before SQLModel 0.0.14, the method was called `hero.dict(exclude_unset=True)`, but it was renamed to `hero.model_dump(exclude_unset=True)` to be consistent with Pydantic v2.
+
 ## Update the Hero in the Database
 
 Now that we have a **dictionary with the data sent by the client**, we can iterate for each one of the keys and the values, and then we set them in the database hero model `db_hero` using `setattr()`.
@@ -208,7 +211,7 @@ So, if the client wanted to intentionally remove the `age` of a hero, they could
 }
 ```
 
-And when getting the data with `hero.dict(exclude_unset=True)`, we would get:
+And when getting the data with `hero.model_dump(exclude_unset=True)`, we would get:
 
 ```Python
 {
@@ -226,4 +229,4 @@ These are some of the advantages of Pydantic, that we can use with SQLModel. 
 
 ## Recap
 
-Using `.dict(exclude_unset=True)` in SQLModel models (and Pydantic models) we can easily update data **correctly**, even in the **edge cases**. 😎
+Using `.model_dump(exclude_unset=True)` in SQLModel models (and Pydantic models) we can easily update data **correctly**, even in the **edge cases**. 😎
index 1e78c9c4f756a87941038b69b2f699e4639cf079..9b0939b0c1ecb6aa9cb07c1ce9f37f33910eeb61 100644 (file)
@@ -82,8 +82,8 @@ There's a chance that you have multiple Python versions installed.
 
 You might want to try with the specific versions, for example with:
 
-* `python3.11`
 * `python3.12`
+* `python3.11`
 * `python3.10`
 * `python3.9`
 
index 3f0602e4b4f9b32aeeafebd20c137ffb78fb003a..7014a73918117b532e1ccb7d3716d0a9a64c4530 100644 (file)
@@ -54,7 +54,7 @@ def on_startup():
 
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
-    db_hero = Hero.from_orm(hero)
+    db_hero = Hero.model_validate(hero)
     session.add(db_hero)
     session.commit()
     session.refresh(db_hero)
@@ -87,7 +87,7 @@ def update_hero(
     db_hero = session.get(Hero, hero_id)
     if not db_hero:
         raise HTTPException(status_code=404, detail="Hero not found")
-    hero_data = hero.dict(exclude_unset=True)
+    hero_data = hero.model_dump(exclude_unset=True)
     for key, value in hero_data.items():
         setattr(db_hero, key, value)
     session.add(db_hero)
index e8615d91df474c637c281efd805b6932dc3b5ea4..cf1bbb713080c9da506fa510c2009f91ac46e6b7 100644 (file)
@@ -52,7 +52,7 @@ def on_startup():
 
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
-    db_hero = Hero.from_orm(hero)
+    db_hero = Hero.model_validate(hero)
     session.add(db_hero)
     session.commit()
     session.refresh(db_hero)
@@ -85,7 +85,7 @@ def update_hero(
     db_hero = session.get(Hero, hero_id)
     if not db_hero:
         raise HTTPException(status_code=404, detail="Hero not found")
-    hero_data = hero.dict(exclude_unset=True)
+    hero_data = hero.model_dump(exclude_unset=True)
     for key, value in hero_data.items():
         setattr(db_hero, key, value)
     session.add(db_hero)
index 9816e70eb075a3d618ff5777588a945dac1bb02c..9f428ab3e852a4800451e5e6e10bbde4a7c452f6 100644 (file)
@@ -54,7 +54,7 @@ def on_startup():
 
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
-    db_hero = Hero.from_orm(hero)
+    db_hero = Hero.model_validate(hero)
     session.add(db_hero)
     session.commit()
     session.refresh(db_hero)
@@ -87,7 +87,7 @@ def update_hero(
     db_hero = session.get(Hero, hero_id)
     if not db_hero:
         raise HTTPException(status_code=404, detail="Hero not found")
-    hero_data = hero.dict(exclude_unset=True)
+    hero_data = hero.model_dump(exclude_unset=True)
     for key, value in hero_data.items():
         setattr(db_hero, key, value)
     session.add(db_hero)
index 3069fc5e87a81ee6e3f2e37b371d9ec66a361cc1..532817360a8b728254d4eccdb234a242190d30cf 100644 (file)
@@ -50,7 +50,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
@@ -79,7 +79,7 @@ def update_hero(hero_id: int, hero: HeroUpdate):
         db_hero = session.get(Hero, hero_id)
         if not db_hero:
             raise HTTPException(status_code=404, detail="Hero not found")
-        hero_data = hero.dict(exclude_unset=True)
+        hero_data = hero.model_dump(exclude_unset=True)
         for key, value in hero_data.items():
             setattr(db_hero, key, value)
         session.add(db_hero)
index 5b2da0a0b1bf03cff8dbb9116422195c20e582e6..45e2e1d515d5383956f5a3e91799dac21f85357a 100644 (file)
@@ -48,7 +48,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
@@ -77,7 +77,7 @@ def update_hero(hero_id: int, hero: HeroUpdate):
         db_hero = session.get(Hero, hero_id)
         if not db_hero:
             raise HTTPException(status_code=404, detail="Hero not found")
-        hero_data = hero.dict(exclude_unset=True)
+        hero_data = hero.model_dump(exclude_unset=True)
         for key, value in hero_data.items():
             setattr(db_hero, key, value)
         session.add(db_hero)
index 5f498cf136ef5970e456a945a3732f81d628de3e..12f6bc3f9b9bd9deb9c560ea6c1f270479d6f76f 100644 (file)
@@ -50,7 +50,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
@@ -79,7 +79,7 @@ def update_hero(hero_id: int, hero: HeroUpdate):
         db_hero = session.get(Hero, hero_id)
         if not db_hero:
             raise HTTPException(status_code=404, detail="Hero not found")
-        hero_data = hero.dict(exclude_unset=True)
+        hero_data = hero.model_dump(exclude_unset=True)
         for key, value in hero_data.items():
             setattr(db_hero, key, value)
         session.add(db_hero)
index 2b8739ca70ef5f96ffc00a037aecfaf9481117c9..2352f3902225dc321954a0733b3a05f9cfd980b3 100644 (file)
@@ -44,7 +44,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
index 874a6e84389f56d9069c5d78b4623f47e81bf74a..ad8ff95e3ad4ec21470465eb12207d49a84e0137 100644 (file)
@@ -42,7 +42,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
index b63fa753ff5cc82539b5741c5c884b58189196b1..b1f7cdcb6afb51fd7382d983b87049d3aeb4e96a 100644 (file)
@@ -44,7 +44,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
index df20123333c985299b8325795cacc510c9347c48..7f59ac6a1dba7eb8074082cf7166d0bd914eff28 100644 (file)
@@ -46,7 +46,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
index 13129f383f0d58fa23e5f22c80a685b2fe4651bb..ff12eff55c1fad875dfe7571f5022d025628dc07 100644 (file)
@@ -44,7 +44,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
index 41a51f448dd7f76e765ea0d09883b54fc699f16b..977a1ac8db346b9c0de70c412063656a5bd11046 100644 (file)
@@ -46,7 +46,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
index 392c2c5829726e525abb3624aa0b7130f8aff766..fffbe72496b20eabb2b1e368143fb86cd8398d4d 100644 (file)
@@ -44,7 +44,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
index 3eda88b1949bb90cfb4b2818a5d2cd3a15d27814..7373edff5e562fc3dfbfc72af37367bf7b2eeebf 100644 (file)
@@ -42,7 +42,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
index 473fe5b8323e53c91a16cd580369e3c58491bed4..1b4a5125202b158b8c5556c88a95250b2aee2311 100644 (file)
@@ -44,7 +44,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
index 4d66e471a54cc81cd0c40c46157e99cc3623ab3f..f18426e74c27198299694c1d3f3d0a25a64f2f71 100644 (file)
@@ -44,7 +44,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
index 8883570dc5337e12f260a911aee4abc6574c0a71..e8c7d49b99cf740e77a0a73bad98a255a0031138 100644 (file)
@@ -42,7 +42,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
index 0ad7016687933751700142803659ed5e10b292f3..4dc5702fb6cdcc97160e82a4f7030222de61c99e 100644 (file)
@@ -44,7 +44,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
index 8477e4a2a03832427ff237aa60d20db081a5fdd0..51339e2a20e1cd22df3b3bcbb0cf00d6cfe30733 100644 (file)
@@ -92,7 +92,7 @@ def on_startup():
 
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
-    db_hero = Hero.from_orm(hero)
+    db_hero = Hero.model_validate(hero)
     session.add(db_hero)
     session.commit()
     session.refresh(db_hero)
@@ -125,7 +125,7 @@ def update_hero(
     db_hero = session.get(Hero, hero_id)
     if not db_hero:
         raise HTTPException(status_code=404, detail="Hero not found")
-    hero_data = hero.dict(exclude_unset=True)
+    hero_data = hero.model_dump(exclude_unset=True)
     for key, value in hero_data.items():
         setattr(db_hero, key, value)
     session.add(db_hero)
@@ -146,7 +146,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
 
 @app.post("/teams/", response_model=TeamRead)
 def create_team(*, session: Session = Depends(get_session), team: TeamCreate):
-    db_team = Team.from_orm(team)
+    db_team = Team.model_validate(team)
     session.add(db_team)
     session.commit()
     session.refresh(db_team)
@@ -182,7 +182,7 @@ def update_team(
     db_team = session.get(Team, team_id)
     if not db_team:
         raise HTTPException(status_code=404, detail="Team not found")
-    team_data = team.dict(exclude_unset=True)
+    team_data = team.model_dump(exclude_unset=True)
     for key, value in team_data.items():
         setattr(db_team, key, value)
     session.add(db_team)
index bec6a6f2e280380c0a2d494d6d4e1989d7d18a4e..35257bd51318dd7bd7d8bbf3f8f375c9228f2d8a 100644 (file)
@@ -90,7 +90,7 @@ def on_startup():
 
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
-    db_hero = Hero.from_orm(hero)
+    db_hero = Hero.model_validate(hero)
     session.add(db_hero)
     session.commit()
     session.refresh(db_hero)
@@ -123,7 +123,7 @@ def update_hero(
     db_hero = session.get(Hero, hero_id)
     if not db_hero:
         raise HTTPException(status_code=404, detail="Hero not found")
-    hero_data = hero.dict(exclude_unset=True)
+    hero_data = hero.model_dump(exclude_unset=True)
     for key, value in hero_data.items():
         setattr(db_hero, key, value)
     session.add(db_hero)
@@ -144,7 +144,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
 
 @app.post("/teams/", response_model=TeamRead)
 def create_team(*, session: Session = Depends(get_session), team: TeamCreate):
-    db_team = Team.from_orm(team)
+    db_team = Team.model_validate(team)
     session.add(db_team)
     session.commit()
     session.refresh(db_team)
@@ -180,7 +180,7 @@ def update_team(
     db_team = session.get(Team, team_id)
     if not db_team:
         raise HTTPException(status_code=404, detail="Team not found")
-    team_data = team.dict(exclude_unset=True)
+    team_data = team.model_dump(exclude_unset=True)
     for key, value in team_data.items():
         setattr(db_team, key, value)
     session.add(db_team)
index 3893905519a0394ad710e502e85e062d99655147..6ceae130a323630126b80f0b33a022e0cc75eeda 100644 (file)
@@ -92,7 +92,7 @@ def on_startup():
 
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
-    db_hero = Hero.from_orm(hero)
+    db_hero = Hero.model_validate(hero)
     session.add(db_hero)
     session.commit()
     session.refresh(db_hero)
@@ -125,7 +125,7 @@ def update_hero(
     db_hero = session.get(Hero, hero_id)
     if not db_hero:
         raise HTTPException(status_code=404, detail="Hero not found")
-    hero_data = hero.dict(exclude_unset=True)
+    hero_data = hero.model_dump(exclude_unset=True)
     for key, value in hero_data.items():
         setattr(db_hero, key, value)
     session.add(db_hero)
@@ -146,7 +146,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
 
 @app.post("/teams/", response_model=TeamRead)
 def create_team(*, session: Session = Depends(get_session), team: TeamCreate):
-    db_team = Team.from_orm(team)
+    db_team = Team.model_validate(team)
     session.add(db_team)
     session.commit()
     session.refresh(db_team)
@@ -182,7 +182,7 @@ def update_team(
     db_team = session.get(Team, team_id)
     if not db_team:
         raise HTTPException(status_code=404, detail="Team not found")
-    team_data = team.dict(exclude_unset=True)
+    team_data = team.model_dump(exclude_unset=True)
     for key, value in team_data.items():
         setattr(db_team, key, value)
     session.add(db_team)
index 3f0602e4b4f9b32aeeafebd20c137ffb78fb003a..7014a73918117b532e1ccb7d3716d0a9a64c4530 100644 (file)
@@ -54,7 +54,7 @@ def on_startup():
 
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
-    db_hero = Hero.from_orm(hero)
+    db_hero = Hero.model_validate(hero)
     session.add(db_hero)
     session.commit()
     session.refresh(db_hero)
@@ -87,7 +87,7 @@ def update_hero(
     db_hero = session.get(Hero, hero_id)
     if not db_hero:
         raise HTTPException(status_code=404, detail="Hero not found")
-    hero_data = hero.dict(exclude_unset=True)
+    hero_data = hero.model_dump(exclude_unset=True)
     for key, value in hero_data.items():
         setattr(db_hero, key, value)
     session.add(db_hero)
index e8615d91df474c637c281efd805b6932dc3b5ea4..cf1bbb713080c9da506fa510c2009f91ac46e6b7 100644 (file)
@@ -52,7 +52,7 @@ def on_startup():
 
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
-    db_hero = Hero.from_orm(hero)
+    db_hero = Hero.model_validate(hero)
     session.add(db_hero)
     session.commit()
     session.refresh(db_hero)
@@ -85,7 +85,7 @@ def update_hero(
     db_hero = session.get(Hero, hero_id)
     if not db_hero:
         raise HTTPException(status_code=404, detail="Hero not found")
-    hero_data = hero.dict(exclude_unset=True)
+    hero_data = hero.model_dump(exclude_unset=True)
     for key, value in hero_data.items():
         setattr(db_hero, key, value)
     session.add(db_hero)
index 9816e70eb075a3d618ff5777588a945dac1bb02c..9f428ab3e852a4800451e5e6e10bbde4a7c452f6 100644 (file)
@@ -54,7 +54,7 @@ def on_startup():
 
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
-    db_hero = Hero.from_orm(hero)
+    db_hero = Hero.model_validate(hero)
     session.add(db_hero)
     session.commit()
     session.refresh(db_hero)
@@ -87,7 +87,7 @@ def update_hero(
     db_hero = session.get(Hero, hero_id)
     if not db_hero:
         raise HTTPException(status_code=404, detail="Hero not found")
-    hero_data = hero.dict(exclude_unset=True)
+    hero_data = hero.model_dump(exclude_unset=True)
     for key, value in hero_data.items():
         setattr(db_hero, key, value)
     session.add(db_hero)
index 1da0dad8a26b3fd5a8413016a2ff4dc2b366f9a4..785c5259184eca641ff11ea170cd7652ef52d0a1 100644 (file)
@@ -83,7 +83,7 @@ def on_startup():
 
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
-    db_hero = Hero.from_orm(hero)
+    db_hero = Hero.model_validate(hero)
     session.add(db_hero)
     session.commit()
     session.refresh(db_hero)
@@ -116,7 +116,7 @@ def update_hero(
     db_hero = session.get(Hero, hero_id)
     if not db_hero:
         raise HTTPException(status_code=404, detail="Hero not found")
-    hero_data = hero.dict(exclude_unset=True)
+    hero_data = hero.model_dump(exclude_unset=True)
     for key, value in hero_data.items():
         setattr(db_hero, key, value)
     session.add(db_hero)
@@ -137,7 +137,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
 
 @app.post("/teams/", response_model=TeamRead)
 def create_team(*, session: Session = Depends(get_session), team: TeamCreate):
-    db_team = Team.from_orm(team)
+    db_team = Team.model_validate(team)
     session.add(db_team)
     session.commit()
     session.refresh(db_team)
@@ -173,7 +173,7 @@ def update_team(
     db_team = session.get(Team, team_id)
     if not db_team:
         raise HTTPException(status_code=404, detail="Team not found")
-    team_data = team.dict(exclude_unset=True)
+    team_data = team.model_dump(exclude_unset=True)
     for key, value in team_data.items():
         setattr(db_team, key, value)
     session.add(db_team)
index a9a527df737bbf37531e5d126bd0b9f1cccc2275..dea4bd8a9bc70028ad9166f9fcbdd2b0a381a8a9 100644 (file)
@@ -81,7 +81,7 @@ def on_startup():
 
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
-    db_hero = Hero.from_orm(hero)
+    db_hero = Hero.model_validate(hero)
     session.add(db_hero)
     session.commit()
     session.refresh(db_hero)
@@ -114,7 +114,7 @@ def update_hero(
     db_hero = session.get(Hero, hero_id)
     if not db_hero:
         raise HTTPException(status_code=404, detail="Hero not found")
-    hero_data = hero.dict(exclude_unset=True)
+    hero_data = hero.model_dump(exclude_unset=True)
     for key, value in hero_data.items():
         setattr(db_hero, key, value)
     session.add(db_hero)
@@ -135,7 +135,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
 
 @app.post("/teams/", response_model=TeamRead)
 def create_team(*, session: Session = Depends(get_session), team: TeamCreate):
-    db_team = Team.from_orm(team)
+    db_team = Team.model_validate(team)
     session.add(db_team)
     session.commit()
     session.refresh(db_team)
@@ -171,7 +171,7 @@ def update_team(
     db_team = session.get(Team, team_id)
     if not db_team:
         raise HTTPException(status_code=404, detail="Team not found")
-    team_data = team.dict(exclude_unset=True)
+    team_data = team.model_dump(exclude_unset=True)
     for key, value in team_data.items():
         setattr(db_team, key, value)
     session.add(db_team)
index 1a3642899442050a7f341fbb97984a2dc1667815..cc6429adcfc246109d134dbf5c310f0e63495a7d 100644 (file)
@@ -83,7 +83,7 @@ def on_startup():
 
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
-    db_hero = Hero.from_orm(hero)
+    db_hero = Hero.model_validate(hero)
     session.add(db_hero)
     session.commit()
     session.refresh(db_hero)
@@ -116,7 +116,7 @@ def update_hero(
     db_hero = session.get(Hero, hero_id)
     if not db_hero:
         raise HTTPException(status_code=404, detail="Hero not found")
-    hero_data = hero.dict(exclude_unset=True)
+    hero_data = hero.model_dump(exclude_unset=True)
     for key, value in hero_data.items():
         setattr(db_hero, key, value)
     session.add(db_hero)
@@ -137,7 +137,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
 
 @app.post("/teams/", response_model=TeamRead)
 def create_team(*, session: Session = Depends(get_session), team: TeamCreate):
-    db_team = Team.from_orm(team)
+    db_team = Team.model_validate(team)
     session.add(db_team)
     session.commit()
     session.refresh(db_team)
@@ -173,7 +173,7 @@ def update_team(
     db_team = session.get(Team, team_id)
     if not db_team:
         raise HTTPException(status_code=404, detail="Team not found")
-    team_data = team.dict(exclude_unset=True)
+    team_data = team.model_dump(exclude_unset=True)
     for key, value in team_data.items():
         setattr(db_team, key, value)
     session.add(db_team)
index bb98efd58148110e5776f9aef270bbbd793920ba..5639638d5c45466b6ca7f09a77f0077830044a4c 100644 (file)
@@ -50,7 +50,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
@@ -79,7 +79,7 @@ def update_hero(hero_id: int, hero: HeroUpdate):
         db_hero = session.get(Hero, hero_id)
         if not db_hero:
             raise HTTPException(status_code=404, detail="Hero not found")
-        hero_data = hero.dict(exclude_unset=True)
+        hero_data = hero.model_dump(exclude_unset=True)
         for key, value in hero_data.items():
             setattr(db_hero, key, value)
         session.add(db_hero)
index 79069181fb07530980c4bf84aff0b2f713d812ba..4faf266f84e197fe0083807a4f3fa6552b32dc15 100644 (file)
@@ -48,7 +48,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
@@ -77,7 +77,7 @@ def update_hero(hero_id: int, hero: HeroUpdate):
         db_hero = session.get(Hero, hero_id)
         if not db_hero:
             raise HTTPException(status_code=404, detail="Hero not found")
-        hero_data = hero.dict(exclude_unset=True)
+        hero_data = hero.model_dump(exclude_unset=True)
         for key, value in hero_data.items():
             setattr(db_hero, key, value)
         session.add(db_hero)
index c788eb1c7ae49e17e0751ebb5f5ba77ad78f6651..b0daa87880d012f51e2ac35a7616419ba9253050 100644 (file)
@@ -50,7 +50,7 @@ def on_startup():
 @app.post("/heroes/", response_model=HeroRead)
 def create_hero(hero: HeroCreate):
     with Session(engine) as session:
-        db_hero = Hero.from_orm(hero)
+        db_hero = Hero.model_validate(hero)
         session.add(db_hero)
         session.commit()
         session.refresh(db_hero)
@@ -79,7 +79,7 @@ def update_hero(hero_id: int, hero: HeroUpdate):
         db_hero = session.get(Hero, hero_id)
         if not db_hero:
             raise HTTPException(status_code=404, detail="Hero not found")
-        hero_data = hero.dict(exclude_unset=True)
+        hero_data = hero.model_dump(exclude_unset=True)
         for key, value in hero_data.items():
             setattr(db_hero, key, value)
         session.add(db_hero)
index 24a6c5c22e2b675c62645dca38e2dd22e0343fd7..10d73793d29e18a741aff26302f18ce88bf0b1d3 100644 (file)
@@ -34,7 +34,7 @@ classifiers = [
 [tool.poetry.dependencies]
 python = "^3.7"
 SQLAlchemy = ">=2.0.0,<2.1.0"
-pydantic = "^1.9.0"
+pydantic = ">=1.10.13,<3.0.0"
 
 [tool.poetry.group.dev.dependencies]
 pytest = "^7.0.1"
@@ -50,6 +50,8 @@ fastapi = "^0.103.2"
 ruff = "^0.1.2"
 # For FastAPI tests
 httpx = "0.24.1"
+# TODO: upgrade when deprecating Python 3.7
+dirty-equals = "^0.6.0"
 typer-cli = "^0.0.13"
 mkdocs-markdownextradata-plugin = ">=0.1.7,<0.3.0"
 
diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py
new file mode 100644 (file)
index 0000000..2a2caca
--- /dev/null
@@ -0,0 +1,554 @@
+import types
+from contextlib import contextmanager
+from contextvars import ContextVar
+from dataclasses import dataclass
+from typing import (
+    TYPE_CHECKING,
+    AbstractSet,
+    Any,
+    Dict,
+    ForwardRef,
+    Generator,
+    Mapping,
+    Optional,
+    Set,
+    Type,
+    TypeVar,
+    Union,
+)
+
+from pydantic import VERSION as PYDANTIC_VERSION
+from pydantic.fields import FieldInfo
+from typing_extensions import get_args, get_origin
+
+IS_PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.")
+
+
+if TYPE_CHECKING:
+    from .main import RelationshipInfo, SQLModel
+
+UnionType = getattr(types, "UnionType", Union)
+NoneType = type(None)
+T = TypeVar("T")
+InstanceOrType = Union[T, Type[T]]
+_TSQLModel = TypeVar("_TSQLModel", bound="SQLModel")
+
+
+class FakeMetadata:
+    max_length: Optional[int] = None
+    max_digits: Optional[int] = None
+    decimal_places: Optional[int] = None
+
+
+@dataclass
+class ObjectWithUpdateWrapper:
+    obj: Any
+    update: Dict[str, Any]
+
+    def __getattribute__(self, __name: str) -> Any:
+        if __name in self.update:
+            return self.update[__name]
+        return getattr(self.obj, __name)
+
+
+def _is_union_type(t: Any) -> bool:
+    return t is UnionType or t is Union
+
+
+finish_init: ContextVar[bool] = ContextVar("finish_init", default=True)
+
+
+@contextmanager
+def partial_init() -> Generator[None, None, None]:
+    token = finish_init.set(False)
+    yield
+    finish_init.reset(token)
+
+
+if IS_PYDANTIC_V2:
+    from pydantic import ConfigDict as BaseConfig
+    from pydantic._internal._fields import PydanticMetadata
+    from pydantic._internal._model_construction import ModelMetaclass
+    from pydantic._internal._repr import Representation as Representation
+    from pydantic_core import PydanticUndefined as Undefined
+    from pydantic_core import PydanticUndefinedType as UndefinedType
+
+    # Dummy for types, to make it importable
+    class ModelField:
+        pass
+
+    class SQLModelConfig(BaseConfig, total=False):
+        table: Optional[bool]
+        registry: Optional[Any]
+
+    def get_config_value(
+        *, model: InstanceOrType["SQLModel"], parameter: str, default: Any = None
+    ) -> Any:
+        return model.model_config.get(parameter, default)
+
+    def set_config_value(
+        *,
+        model: InstanceOrType["SQLModel"],
+        parameter: str,
+        value: Any,
+    ) -> None:
+        model.model_config[parameter] = value  # type: ignore[literal-required]
+
+    def get_model_fields(model: InstanceOrType["SQLModel"]) -> Dict[str, "FieldInfo"]:
+        return model.model_fields
+
+    def set_fields_set(
+        new_object: InstanceOrType["SQLModel"], fields: Set["FieldInfo"]
+    ) -> None:
+        object.__setattr__(new_object, "__pydantic_fields_set__", fields)
+
+    def get_annotations(class_dict: Dict[str, Any]) -> Dict[str, Any]:
+        return class_dict.get("__annotations__", {})
+
+    def is_table_model_class(cls: Type[Any]) -> bool:
+        config = getattr(cls, "model_config", {})
+        if config:
+            return config.get("table", False) or False
+        return False
+
+    def get_relationship_to(
+        name: str,
+        rel_info: "RelationshipInfo",
+        annotation: Any,
+    ) -> Any:
+        origin = get_origin(annotation)
+        use_annotation = annotation
+        # Direct relationships (e.g. 'Team' or Team) have None as an origin
+        if origin is None:
+            if isinstance(use_annotation, ForwardRef):
+                use_annotation = use_annotation.__forward_arg__
+            else:
+                return use_annotation
+        # If Union (e.g. Optional), get the real field
+        elif _is_union_type(origin):
+            use_annotation = get_args(annotation)
+            if len(use_annotation) > 2:
+                raise ValueError(
+                    "Cannot have a (non-optional) union as a SQLAlchemy field"
+                )
+            arg1, arg2 = use_annotation
+            if arg1 is NoneType and arg2 is not NoneType:
+                use_annotation = arg2
+            elif arg2 is NoneType and arg1 is not NoneType:
+                use_annotation = arg1
+            else:
+                raise ValueError(
+                    "Cannot have a Union of None and None as a SQLAlchemy field"
+                )
+
+        # If a list, then also get the real field
+        elif origin is list:
+            use_annotation = get_args(annotation)[0]
+
+        return get_relationship_to(
+            name=name, rel_info=rel_info, annotation=use_annotation
+        )
+
+    def is_field_noneable(field: "FieldInfo") -> bool:
+        if getattr(field, "nullable", Undefined) is not Undefined:
+            return field.nullable  # type: ignore
+        origin = get_origin(field.annotation)
+        if origin is not None and _is_union_type(origin):
+            args = get_args(field.annotation)
+            if any(arg is NoneType for arg in args):
+                return True
+        if not field.is_required():
+            if field.default is Undefined:
+                return False
+            if field.annotation is None or field.annotation is NoneType:  # type: ignore[comparison-overlap]
+                return True
+            return False
+        return False
+
+    def get_type_from_field(field: Any) -> Any:
+        type_: Any = field.annotation
+        # Resolve Optional fields
+        if type_ is None:
+            raise ValueError("Missing field type")
+        origin = get_origin(type_)
+        if origin is None:
+            return type_
+        if _is_union_type(origin):
+            bases = get_args(type_)
+            if len(bases) > 2:
+                raise ValueError(
+                    "Cannot have a (non-optional) union as a SQLAlchemy field"
+                )
+            # Non optional unions are not allowed
+            if bases[0] is not NoneType and bases[1] is not NoneType:
+                raise ValueError(
+                    "Cannot have a (non-optional) union as a SQLlchemy field"
+                )
+            # Optional unions are allowed
+            return bases[0] if bases[0] is not NoneType else bases[1]
+        return origin
+
+    def get_field_metadata(field: Any) -> Any:
+        for meta in field.metadata:
+            if isinstance(meta, PydanticMetadata):
+                return meta
+        return FakeMetadata()
+
+    def post_init_field_info(field_info: FieldInfo) -> None:
+        return None
+
+    # Dummy to make it importable
+    def _calculate_keys(
+        self: "SQLModel",
+        include: Optional[Mapping[Union[int, str], Any]],
+        exclude: Optional[Mapping[Union[int, str], Any]],
+        exclude_unset: bool,
+        update: Optional[Dict[str, Any]] = None,
+    ) -> Optional[AbstractSet[str]]:  # pragma: no cover
+        return None
+
+    def sqlmodel_table_construct(
+        *,
+        self_instance: _TSQLModel,
+        values: Dict[str, Any],
+        _fields_set: Union[Set[str], None] = None,
+    ) -> _TSQLModel:
+        # Copy from Pydantic's BaseModel.construct()
+        # Ref: https://github.com/pydantic/pydantic/blob/v2.5.2/pydantic/main.py#L198
+        # Modified to not include everything, only the model fields, and to
+        # set relationships
+        # SQLModel override to get class SQLAlchemy __dict__ attributes and
+        # set them back in after creating the object
+        # new_obj = cls.__new__(cls)
+        cls = type(self_instance)
+        old_dict = self_instance.__dict__.copy()
+        # End SQLModel override
+
+        fields_values: Dict[str, Any] = {}
+        defaults: Dict[
+            str, Any
+        ] = {}  # keeping this separate from `fields_values` helps us compute `_fields_set`
+        for name, field in cls.model_fields.items():
+            if field.alias and field.alias in values:
+                fields_values[name] = values.pop(field.alias)
+            elif name in values:
+                fields_values[name] = values.pop(name)
+            elif not field.is_required():
+                defaults[name] = field.get_default(call_default_factory=True)
+        if _fields_set is None:
+            _fields_set = set(fields_values.keys())
+        fields_values.update(defaults)
+
+        _extra: Union[Dict[str, Any], None] = None
+        if cls.model_config.get("extra") == "allow":
+            _extra = {}
+            for k, v in values.items():
+                _extra[k] = v
+        # SQLModel override, do not include everything, only the model fields
+        # else:
+        #     fields_values.update(values)
+        # End SQLModel override
+        # SQLModel override
+        # Do not set __dict__, instead use setattr to trigger SQLAlchemy
+        # object.__setattr__(new_obj, "__dict__", fields_values)
+        # instrumentation
+        for key, value in {**old_dict, **fields_values}.items():
+            setattr(self_instance, key, value)
+        # End SQLModel override
+        object.__setattr__(self_instance, "__pydantic_fields_set__", _fields_set)
+        if not cls.__pydantic_root_model__:
+            object.__setattr__(self_instance, "__pydantic_extra__", _extra)
+
+        if cls.__pydantic_post_init__:
+            self_instance.model_post_init(None)
+        elif not cls.__pydantic_root_model__:
+            # Note: if there are any private attributes, cls.__pydantic_post_init__ would exist
+            # Since it doesn't, that means that `__pydantic_private__` should be set to None
+            object.__setattr__(self_instance, "__pydantic_private__", None)
+        # SQLModel override, set relationships
+        # Get and set any relationship objects
+        for key in self_instance.__sqlmodel_relationships__:
+            value = values.get(key, Undefined)
+            if value is not Undefined:
+                setattr(self_instance, key, value)
+        # End SQLModel override
+        return self_instance
+
+    def sqlmodel_validate(
+        cls: Type[_TSQLModel],
+        obj: Any,
+        *,
+        strict: Union[bool, None] = None,
+        from_attributes: Union[bool, None] = None,
+        context: Union[Dict[str, Any], None] = None,
+        update: Union[Dict[str, Any], None] = None,
+    ) -> _TSQLModel:
+        if not is_table_model_class(cls):
+            new_obj: _TSQLModel = cls.__new__(cls)
+        else:
+            # If table, create the new instance normally to make SQLAlchemy create
+            # the _sa_instance_state attribute
+            # The wrapper of this function should use with _partial_init()
+            with partial_init():
+                new_obj = cls()
+        # SQLModel Override to get class SQLAlchemy __dict__ attributes and
+        # set them back in after creating the object
+        old_dict = new_obj.__dict__.copy()
+        use_obj = obj
+        if isinstance(obj, dict) and update:
+            use_obj = {**obj, **update}
+        elif update:
+            use_obj = ObjectWithUpdateWrapper(obj=obj, update=update)
+        cls.__pydantic_validator__.validate_python(
+            use_obj,
+            strict=strict,
+            from_attributes=from_attributes,
+            context=context,
+            self_instance=new_obj,
+        )
+        # Capture fields set to restore it later
+        fields_set = new_obj.__pydantic_fields_set__.copy()
+        if not is_table_model_class(cls):
+            # If not table, normal Pydantic code, set __dict__
+            new_obj.__dict__ = {**old_dict, **new_obj.__dict__}
+        else:
+            # Do not set __dict__, instead use setattr to trigger SQLAlchemy
+            # instrumentation
+            for key, value in {**old_dict, **new_obj.__dict__}.items():
+                setattr(new_obj, key, value)
+        # Restore fields set
+        object.__setattr__(new_obj, "__pydantic_fields_set__", fields_set)
+        # Get and set any relationship objects
+        if is_table_model_class(cls):
+            for key in new_obj.__sqlmodel_relationships__:
+                value = getattr(use_obj, key, Undefined)
+                if value is not Undefined:
+                    setattr(new_obj, key, value)
+        return new_obj
+
+    def sqlmodel_init(*, self: "SQLModel", data: Dict[str, Any]) -> None:
+        old_dict = self.__dict__.copy()
+        if not is_table_model_class(self.__class__):
+            self.__pydantic_validator__.validate_python(
+                data,
+                self_instance=self,
+            )
+        else:
+            sqlmodel_table_construct(
+                self_instance=self,
+                values=data,
+            )
+        object.__setattr__(
+            self,
+            "__dict__",
+            {**old_dict, **self.__dict__},
+        )
+
+else:
+    from pydantic import BaseConfig as BaseConfig  # type: ignore[assignment]
+    from pydantic.errors import ConfigError
+    from pydantic.fields import (  # type: ignore[attr-defined, no-redef]
+        SHAPE_SINGLETON,
+        ModelField,
+    )
+    from pydantic.fields import (  # type: ignore[attr-defined, no-redef]
+        Undefined as Undefined,  # noqa
+    )
+    from pydantic.fields import (  # type: ignore[attr-defined, no-redef]
+        UndefinedType as UndefinedType,
+    )
+    from pydantic.main import (  # type: ignore[no-redef]
+        ModelMetaclass as ModelMetaclass,
+    )
+    from pydantic.main import validate_model
+    from pydantic.typing import resolve_annotations
+    from pydantic.utils import ROOT_KEY, ValueItems
+    from pydantic.utils import (  # type: ignore[no-redef]
+        Representation as Representation,
+    )
+
+    class SQLModelConfig(BaseConfig):  # type: ignore[no-redef]
+        table: Optional[bool] = None  # type: ignore[misc]
+        registry: Optional[Any] = None  # type: ignore[misc]
+
+    def get_config_value(
+        *, model: InstanceOrType["SQLModel"], parameter: str, default: Any = None
+    ) -> Any:
+        return getattr(model.__config__, parameter, default)  # type: ignore[union-attr]
+
+    def set_config_value(
+        *,
+        model: InstanceOrType["SQLModel"],
+        parameter: str,
+        value: Any,
+    ) -> None:
+        setattr(model.__config__, parameter, value)  # type: ignore
+
+    def get_model_fields(model: InstanceOrType["SQLModel"]) -> Dict[str, "FieldInfo"]:
+        return model.__fields__  # type: ignore
+
+    def set_fields_set(
+        new_object: InstanceOrType["SQLModel"], fields: Set["FieldInfo"]
+    ) -> None:
+        object.__setattr__(new_object, "__fields_set__", fields)
+
+    def get_annotations(class_dict: Dict[str, Any]) -> Dict[str, Any]:
+        return resolve_annotations(  # type: ignore[no-any-return]
+            class_dict.get("__annotations__", {}),
+            class_dict.get("__module__", None),
+        )
+
+    def is_table_model_class(cls: Type[Any]) -> bool:
+        config = getattr(cls, "__config__", None)
+        if config:
+            return getattr(config, "table", False)
+        return False
+
+    def get_relationship_to(
+        name: str,
+        rel_info: "RelationshipInfo",
+        annotation: Any,
+    ) -> Any:
+        temp_field = ModelField.infer(  # type: ignore[attr-defined]
+            name=name,
+            value=rel_info,
+            annotation=annotation,
+            class_validators=None,
+            config=SQLModelConfig,
+        )
+        relationship_to = temp_field.type_
+        if isinstance(temp_field.type_, ForwardRef):
+            relationship_to = temp_field.type_.__forward_arg__
+        return relationship_to
+
+    def is_field_noneable(field: "FieldInfo") -> bool:
+        if not field.required:  # type: ignore[attr-defined]
+            # Taken from [Pydantic](https://github.com/samuelcolvin/pydantic/blob/v1.8.2/pydantic/fields.py#L946-L947)
+            return field.allow_none and (  # type: ignore[attr-defined]
+                field.shape != SHAPE_SINGLETON or not field.sub_fields  # type: ignore[attr-defined]
+            )
+        return field.allow_none  # type: ignore[no-any-return, attr-defined]
+
+    def get_type_from_field(field: Any) -> Any:
+        if isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON:
+            return field.type_
+        raise ValueError(f"The field {field.name} has no matching SQLAlchemy type")
+
+    def get_field_metadata(field: Any) -> Any:
+        metadata = FakeMetadata()
+        metadata.max_length = field.field_info.max_length
+        metadata.max_digits = getattr(field.type_, "max_digits", None)
+        metadata.decimal_places = getattr(field.type_, "decimal_places", None)
+        return metadata
+
+    def post_init_field_info(field_info: FieldInfo) -> None:
+        field_info._validate()  # type: ignore[attr-defined]
+
+    def _calculate_keys(
+        self: "SQLModel",
+        include: Optional[Mapping[Union[int, str], Any]],
+        exclude: Optional[Mapping[Union[int, str], Any]],
+        exclude_unset: bool,
+        update: Optional[Dict[str, Any]] = None,
+    ) -> Optional[AbstractSet[str]]:
+        if include is None and exclude is None and not exclude_unset:
+            # Original in Pydantic:
+            # return None
+            # Updated to not return SQLAlchemy attributes
+            # Do not include relationships as that would easily lead to infinite
+            # recursion, or traversing the whole database
+            return (
+                self.__fields__.keys()  # noqa
+            )  # | self.__sqlmodel_relationships__.keys()
+
+        keys: AbstractSet[str]
+        if exclude_unset:
+            keys = self.__fields_set__.copy()  # noqa
+        else:
+            # Original in Pydantic:
+            # keys = self.__dict__.keys()
+            # Updated to not return SQLAlchemy attributes
+            # Do not include relationships as that would easily lead to infinite
+            # recursion, or traversing the whole database
+            keys = (
+                self.__fields__.keys()  # noqa
+            )  # | self.__sqlmodel_relationships__.keys()
+        if include is not None:
+            keys &= include.keys()
+
+        if update:
+            keys -= update.keys()
+
+        if exclude:
+            keys -= {k for k, v in exclude.items() if ValueItems.is_true(v)}
+
+        return keys
+
+    def sqlmodel_validate(
+        cls: Type[_TSQLModel],
+        obj: Any,
+        *,
+        strict: Union[bool, None] = None,
+        from_attributes: Union[bool, None] = None,
+        context: Union[Dict[str, Any], None] = None,
+        update: Union[Dict[str, Any], None] = None,
+    ) -> _TSQLModel:
+        # This was SQLModel's original from_orm() for Pydantic v1
+        # Duplicated from Pydantic
+        if not cls.__config__.orm_mode:  # type: ignore[attr-defined] # noqa
+            raise ConfigError(
+                "You must have the config attribute orm_mode=True to use from_orm"
+            )
+        if not isinstance(obj, Mapping):
+            obj = (
+                {ROOT_KEY: obj}
+                if cls.__custom_root_type__  # type: ignore[attr-defined] # noqa
+                else cls._decompose_class(obj)  # type: ignore[attr-defined] # noqa
+            )
+        # SQLModel, support update dict
+        if update is not None:
+            obj = {**obj, **update}
+        # End SQLModel support dict
+        if not getattr(cls.__config__, "table", False):  # noqa
+            # If not table, normal Pydantic code
+            m: _TSQLModel = cls.__new__(cls)
+        else:
+            # If table, create the new instance normally to make SQLAlchemy create
+            # the _sa_instance_state attribute
+            m = cls()
+        values, fields_set, validation_error = validate_model(cls, obj)
+        if validation_error:
+            raise validation_error
+        # Updated to trigger SQLAlchemy internal handling
+        if not getattr(cls.__config__, "table", False):  # noqa
+            object.__setattr__(m, "__dict__", values)
+        else:
+            for key, value in values.items():
+                setattr(m, key, value)
+        # Continue with standard Pydantic logic
+        object.__setattr__(m, "__fields_set__", fields_set)
+        m._init_private_attributes()  # type: ignore[attr-defined] # noqa
+        return m
+
+    def sqlmodel_init(*, self: "SQLModel", data: Dict[str, Any]) -> None:
+        values, fields_set, validation_error = validate_model(self.__class__, data)
+        # Only raise errors if not a SQLModel model
+        if (
+            not is_table_model_class(self.__class__)  # noqa
+            and validation_error
+        ):
+            raise validation_error
+        if not is_table_model_class(self.__class__):
+            object.__setattr__(self, "__dict__", values)
+        else:
+            # Do not set values as in Pydantic, pass them through setattr, so
+            # SQLAlchemy can handle them
+            for key, value in values.items():
+                setattr(self, key, value)
+        object.__setattr__(self, "__fields_set__", fields_set)
+        non_pydantic_keys = data.keys() - values.keys()
+
+        if is_table_model_class(self.__class__):
+            for key in non_pydantic_keys:
+                if key in self.__sqlmodel_relationships__:
+                    setattr(self, key, data[key])
index c30af5779f3f07b5789e476e629963ab87a93c2b..10064c71165f415b1de4ec89e82164c0632e370c 100644 (file)
@@ -11,7 +11,6 @@ from typing import (
     Callable,
     ClassVar,
     Dict,
-    ForwardRef,
     List,
     Mapping,
     Optional,
@@ -25,13 +24,8 @@ from typing import (
     overload,
 )
 
-from pydantic import BaseConfig, BaseModel
-from pydantic.errors import ConfigError, DictError
-from pydantic.fields import SHAPE_SINGLETON, ModelField, Undefined, UndefinedType
+from pydantic import BaseModel
 from pydantic.fields import FieldInfo as PydanticFieldInfo
-from pydantic.main import ModelMetaclass, validate_model
-from pydantic.typing import NoArgAnyCallable, resolve_annotations
-from pydantic.utils import ROOT_KEY, Representation
 from sqlalchemy import (
     Boolean,
     Column,
@@ -57,11 +51,38 @@ from sqlalchemy.orm.decl_api import DeclarativeMeta
 from sqlalchemy.orm.instrumentation import is_instrumented
 from sqlalchemy.sql.schema import MetaData
 from sqlalchemy.sql.sqltypes import LargeBinary, Time
-from typing_extensions import get_origin
-
+from typing_extensions import Literal, deprecated, get_origin
+
+from ._compat import (  # type: ignore[attr-defined]
+    IS_PYDANTIC_V2,
+    BaseConfig,
+    ModelField,
+    ModelMetaclass,
+    Representation,
+    SQLModelConfig,
+    Undefined,
+    UndefinedType,
+    _calculate_keys,
+    finish_init,
+    get_annotations,
+    get_config_value,
+    get_field_metadata,
+    get_model_fields,
+    get_relationship_to,
+    get_type_from_field,
+    is_field_noneable,
+    is_table_model_class,
+    post_init_field_info,
+    set_config_value,
+    set_fields_set,
+    sqlmodel_init,
+    sqlmodel_validate,
+)
 from .sql.sqltypes import GUID, AutoString
 
 _T = TypeVar("_T")
+NoArgAnyCallable = Callable[[], Any]
+IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any], None]
 
 
 def __dataclass_transform__(
@@ -321,7 +342,7 @@ def Field(
         sa_column_kwargs=sa_column_kwargs,
         **current_schema_extra,
     )
-    field_info._validate()
+    post_init_field_info(field_info)
     return field_info
 
 
@@ -341,7 +362,7 @@ def Relationship(
     *,
     back_populates: Optional[str] = None,
     link_model: Optional[Any] = None,
-    sa_relationship: Optional[RelationshipProperty] = None,  # type: ignore
+    sa_relationship: Optional[RelationshipProperty[Any]] = None,
 ) -> Any:
     ...
 
@@ -350,7 +371,7 @@ def Relationship(
     *,
     back_populates: Optional[str] = None,
     link_model: Optional[Any] = None,
-    sa_relationship: Optional[RelationshipProperty] = None,  # type: ignore
+    sa_relationship: Optional[RelationshipProperty[Any]] = None,
     sa_relationship_args: Optional[Sequence[Any]] = None,
     sa_relationship_kwargs: Optional[Mapping[str, Any]] = None,
 ) -> Any:
@@ -367,18 +388,20 @@ def Relationship(
 @__dataclass_transform__(kw_only_default=True, field_descriptors=(Field, FieldInfo))
 class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
     __sqlmodel_relationships__: Dict[str, RelationshipInfo]
-    __config__: Type[BaseConfig]
-    __fields__: Dict[str, ModelField]
+    model_config: SQLModelConfig
+    model_fields: Dict[str, FieldInfo]
+    __config__: Type[SQLModelConfig]
+    __fields__: Dict[str, ModelField]  # type: ignore[assignment]
 
     # Replicate SQLAlchemy
     def __setattr__(cls, name: str, value: Any) -> None:
-        if getattr(cls.__config__, "table", False):
+        if is_table_model_class(cls):
             DeclarativeMeta.__setattr__(cls, name, value)
         else:
             super().__setattr__(name, value)
 
     def __delattr__(cls, name: str) -> None:
-        if getattr(cls.__config__, "table", False):
+        if is_table_model_class(cls):
             DeclarativeMeta.__delattr__(cls, name)
         else:
             super().__delattr__(name)
@@ -393,9 +416,7 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
     ) -> Any:
         relationships: Dict[str, RelationshipInfo] = {}
         dict_for_pydantic = {}
-        original_annotations = resolve_annotations(
-            class_dict.get("__annotations__", {}), class_dict.get("__module__", None)
-        )
+        original_annotations = get_annotations(class_dict)
         pydantic_annotations = {}
         relationship_annotations = {}
         for k, v in class_dict.items():
@@ -424,10 +445,8 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
                 key.startswith("__") and key.endswith("__")
             )  # skip dunder methods and attributes
         }
-        pydantic_kwargs = kwargs.copy()
         config_kwargs = {
-            key: pydantic_kwargs.pop(key)
-            for key in pydantic_kwargs.keys() & allowed_config_kwargs
+            key: kwargs[key] for key in kwargs.keys() & allowed_config_kwargs
         }
         new_cls = super().__new__(cls, name, bases, dict_used, **config_kwargs)
         new_cls.__annotations__ = {
@@ -437,7 +456,9 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
         }
 
         def get_config(name: str) -> Any:
-            config_class_value = getattr(new_cls.__config__, name, Undefined)
+            config_class_value = get_config_value(
+                model=new_cls, parameter=name, default=Undefined
+            )
             if config_class_value is not Undefined:
                 return config_class_value
             kwarg_value = kwargs.get(name, Undefined)
@@ -448,22 +469,27 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
         config_table = get_config("table")
         if config_table is True:
             # If it was passed by kwargs, ensure it's also set in config
-            new_cls.__config__.table = config_table
-            for k, v in new_cls.__fields__.items():
+            set_config_value(model=new_cls, parameter="table", value=config_table)
+            for k, v in get_model_fields(new_cls).items():
                 col = get_column_from_field(v)
                 setattr(new_cls, k, col)
             # Set a config flag to tell FastAPI that this should be read with a field
             # in orm_mode instead of preemptively converting it to a dict.
-            # This could be done by reading new_cls.__config__.table in FastAPI, but
+            # This could be done by reading new_cls.model_config['table'] in FastAPI, but
             # that's very specific about SQLModel, so let's have another config that
             # other future tools based on Pydantic can use.
-            new_cls.__config__.read_with_orm_mode = True
+            set_config_value(
+                model=new_cls, parameter="read_from_attributes", value=True
+            )
+            # For compatibility with older versions
+            # TODO: remove this in the future
+            set_config_value(model=new_cls, parameter="read_with_orm_mode", value=True)
 
         config_registry = get_config("registry")
         if config_registry is not Undefined:
             config_registry = cast(registry, config_registry)
             # If it was passed by kwargs, ensure it's also set in config
-            new_cls.__config__.registry = config_table
+            set_config_value(model=new_cls, parameter="registry", value=config_table)
             setattr(new_cls, "_sa_registry", config_registry)  # noqa: B010
             setattr(new_cls, "metadata", config_registry.metadata)  # noqa: B010
             setattr(new_cls, "__abstract__", True)  # noqa: B010
@@ -477,13 +503,8 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
         # this allows FastAPI cloning a SQLModel for the response_model without
         # trying to create a new SQLAlchemy, for a new table, with the same name, that
         # triggers an error
-        base_is_table = False
-        for base in bases:
-            config = getattr(base, "__config__")  # noqa: B009
-            if config and getattr(config, "table", False):
-                base_is_table = True
-                break
-        if getattr(cls.__config__, "table", False) and not base_is_table:
+        base_is_table = any(is_table_model_class(base) for base in bases)
+        if is_table_model_class(cls) and not base_is_table:
             for rel_name, rel_info in cls.__sqlmodel_relationships__.items():
                 if rel_info.sa_relationship:
                     # There's a SQLAlchemy relationship declared, that takes precedence
@@ -500,16 +521,9 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
                     # handled well by SQLAlchemy without Mapped, so, wrap the
                     # annotations in Mapped here
                     cls.__annotations__[rel_name] = Mapped[ann]  # type: ignore[valid-type]
-                temp_field = ModelField.infer(
-                    name=rel_name,
-                    value=rel_info,
-                    annotation=ann,
-                    class_validators=None,
-                    config=BaseConfig,
+                relationship_to = get_relationship_to(
+                    name=rel_name, rel_info=rel_info, annotation=ann
                 )
-                relationship_to = temp_field.type_
-                if isinstance(temp_field.type_, ForwardRef):
-                    relationship_to = temp_field.type_.__forward_arg__
                 rel_kwargs: Dict[str, Any] = {}
                 if rel_info.back_populates:
                     rel_kwargs["back_populates"] = rel_info.back_populates
@@ -537,77 +551,89 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
             ModelMetaclass.__init__(cls, classname, bases, dict_, **kw)
 
 
-def get_sqlalchemy_type(field: ModelField) -> Any:
-    sa_type = getattr(field.field_info, "sa_type", Undefined)  # noqa: B009
+def get_sqlalchemy_type(field: Any) -> Any:
+    if IS_PYDANTIC_V2:
+        field_info = field
+    else:
+        field_info = field.field_info
+    sa_type = getattr(field_info, "sa_type", Undefined)  # noqa: B009
     if sa_type is not Undefined:
         return sa_type
-    if isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON:
-        # Check enums first as an enum can also be a str, needed by Pydantic/FastAPI
-        if issubclass(field.type_, Enum):
-            return sa_Enum(field.type_)
-        if issubclass(field.type_, str):
-            if field.field_info.max_length:
-                return AutoString(length=field.field_info.max_length)
-            return AutoString
-        if issubclass(field.type_, float):
-            return Float
-        if issubclass(field.type_, bool):
-            return Boolean
-        if issubclass(field.type_, int):
-            return Integer
-        if issubclass(field.type_, datetime):
-            return DateTime
-        if issubclass(field.type_, date):
-            return Date
-        if issubclass(field.type_, timedelta):
-            return Interval
-        if issubclass(field.type_, time):
-            return Time
-        if issubclass(field.type_, bytes):
-            return LargeBinary
-        if issubclass(field.type_, Decimal):
-            return Numeric(
-                precision=getattr(field.type_, "max_digits", None),
-                scale=getattr(field.type_, "decimal_places", None),
-            )
-        if issubclass(field.type_, ipaddress.IPv4Address):
-            return AutoString
-        if issubclass(field.type_, ipaddress.IPv4Network):
-            return AutoString
-        if issubclass(field.type_, ipaddress.IPv6Address):
-            return AutoString
-        if issubclass(field.type_, ipaddress.IPv6Network):
-            return AutoString
-        if issubclass(field.type_, Path):
-            return AutoString
-        if issubclass(field.type_, uuid.UUID):
-            return GUID
-    raise ValueError(f"The field {field.name} has no matching SQLAlchemy type")
-
-
-def get_column_from_field(field: ModelField) -> Column:  # type: ignore
-    sa_column = getattr(field.field_info, "sa_column", Undefined)
+
+    type_ = get_type_from_field(field)
+    metadata = get_field_metadata(field)
+
+    # Check enums first as an enum can also be a str, needed by Pydantic/FastAPI
+    if issubclass(type_, Enum):
+        return sa_Enum(type_)
+    if issubclass(type_, str):
+        max_length = getattr(metadata, "max_length", None)
+        if max_length:
+            return AutoString(length=max_length)
+        return AutoString
+    if issubclass(type_, float):
+        return Float
+    if issubclass(type_, bool):
+        return Boolean
+    if issubclass(type_, int):
+        return Integer
+    if issubclass(type_, datetime):
+        return DateTime
+    if issubclass(type_, date):
+        return Date
+    if issubclass(type_, timedelta):
+        return Interval
+    if issubclass(type_, time):
+        return Time
+    if issubclass(type_, bytes):
+        return LargeBinary
+    if issubclass(type_, Decimal):
+        return Numeric(
+            precision=getattr(metadata, "max_digits", None),
+            scale=getattr(metadata, "decimal_places", None),
+        )
+    if issubclass(type_, ipaddress.IPv4Address):
+        return AutoString
+    if issubclass(type_, ipaddress.IPv4Network):
+        return AutoString
+    if issubclass(type_, ipaddress.IPv6Address):
+        return AutoString
+    if issubclass(type_, ipaddress.IPv6Network):
+        return AutoString
+    if issubclass(type_, Path):
+        return AutoString
+    if issubclass(type_, uuid.UUID):
+        return GUID
+    raise ValueError(f"{type_} has no matching SQLAlchemy type")
+
+
+def get_column_from_field(field: Any) -> Column:  # type: ignore
+    if IS_PYDANTIC_V2:
+        field_info = field
+    else:
+        field_info = field.field_info
+    sa_column = getattr(field_info, "sa_column", Undefined)
     if isinstance(sa_column, Column):
         return sa_column
     sa_type = get_sqlalchemy_type(field)
-    primary_key = getattr(field.field_info, "primary_key", Undefined)
+    primary_key = getattr(field_info, "primary_key", Undefined)
     if primary_key is Undefined:
         primary_key = False
-    index = getattr(field.field_info, "index", Undefined)
+    index = getattr(field_info, "index", Undefined)
     if index is Undefined:
         index = False
-    nullable = not primary_key and _is_field_noneable(field)
+    nullable = not primary_key and is_field_noneable(field)
     # Override derived nullability if the nullable property is set explicitly
     # on the field
-    field_nullable = getattr(field.field_info, "nullable", Undefined)  # noqa: B009
-    if field_nullable != Undefined:
+    field_nullable = getattr(field_info, "nullable", Undefined)  # noqa: B009
+    if field_nullable is not Undefined:
         assert not isinstance(field_nullable, UndefinedType)
         nullable = field_nullable
     args = []
-    foreign_key = getattr(field.field_info, "foreign_key", Undefined)
+    foreign_key = getattr(field_info, "foreign_key", Undefined)
     if foreign_key is Undefined:
         foreign_key = None
-    unique = getattr(field.field_info, "unique", Undefined)
+    unique = getattr(field_info, "unique", Undefined)
     if unique is Undefined:
         unique = False
     if foreign_key:
@@ -620,16 +646,16 @@ def get_column_from_field(field: ModelField) -> Column:  # type: ignore
         "unique": unique,
     }
     sa_default = Undefined
-    if field.field_info.default_factory:
-        sa_default = field.field_info.default_factory
-    elif field.field_info.default is not Undefined:
-        sa_default = field.field_info.default
+    if field_info.default_factory:
+        sa_default = field_info.default_factory
+    elif field_info.default is not Undefined:
+        sa_default = field_info.default
     if sa_default is not Undefined:
         kwargs["default"] = sa_default
-    sa_column_args = getattr(field.field_info, "sa_column_args", Undefined)
+    sa_column_args = getattr(field_info, "sa_column_args", Undefined)
     if sa_column_args is not Undefined:
         args.extend(list(cast(Sequence[Any], sa_column_args)))
-    sa_column_kwargs = getattr(field.field_info, "sa_column_kwargs", Undefined)
+    sa_column_kwargs = getattr(field_info, "sa_column_kwargs", Undefined)
     if sa_column_kwargs is not Undefined:
         kwargs.update(cast(Dict[Any, Any], sa_column_kwargs))
     return Column(sa_type, *args, **kwargs)  # type: ignore
@@ -639,13 +665,6 @@ class_registry = weakref.WeakValueDictionary()  # type: ignore
 
 default_registry = registry()
 
-
-def _value_items_is_true(v: Any) -> bool:
-    # Re-implement Pydantic's ValueItems.is_true() as it hasn't been released as of
-    # the current latest, Pydantic 1.8.2
-    return v is True or v is ...
-
-
 _TSQLModel = TypeVar("_TSQLModel", bound="SQLModel")
 
 
@@ -653,13 +672,17 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry
     # SQLAlchemy needs to set weakref(s), Pydantic will set the other slots values
     __slots__ = ("__weakref__",)
     __tablename__: ClassVar[Union[str, Callable[..., str]]]
-    __sqlmodel_relationships__: ClassVar[Dict[str, RelationshipProperty]]  # type: ignore
+    __sqlmodel_relationships__: ClassVar[Dict[str, RelationshipProperty[Any]]]
     __name__: ClassVar[str]
     metadata: ClassVar[MetaData]
     __allow_unmapped__ = True  # https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-20-step-six
 
-    class Config:
-        orm_mode = True
+    if IS_PYDANTIC_V2:
+        model_config = SQLModelConfig(from_attributes=True)
+    else:
+
+        class Config:
+            orm_mode = True
 
     def __new__(cls, *args: Any, **kwargs: Any) -> Any:
         new_object = super().__new__(cls)
@@ -668,31 +691,28 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry
         # Set __fields_set__ here, that would have been set when calling __init__
         # in the Pydantic model so that when SQLAlchemy sets attributes that are
         # added (e.g. when querying from DB) to the __fields_set__, this already exists
-        object.__setattr__(new_object, "__fields_set__", set())
+        set_fields_set(new_object, set())
         return new_object
 
     def __init__(__pydantic_self__, **data: Any) -> None:
         # Uses something other than `self` the first arg to allow "self" as a
         # settable attribute
-        values, fields_set, validation_error = validate_model(
-            __pydantic_self__.__class__, data
-        )
-        # Only raise errors if not a SQLModel model
-        if (
-            not getattr(__pydantic_self__.__config__, "table", False)
-            and validation_error
-        ):
-            raise validation_error
-        # Do not set values as in Pydantic, pass them through setattr, so SQLAlchemy
-        # can handle them
-        # object.__setattr__(__pydantic_self__, '__dict__', values)
-        for key, value in values.items():
-            setattr(__pydantic_self__, key, value)
-        object.__setattr__(__pydantic_self__, "__fields_set__", fields_set)
-        non_pydantic_keys = data.keys() - values.keys()
-        for key in non_pydantic_keys:
-            if key in __pydantic_self__.__sqlmodel_relationships__:
-                setattr(__pydantic_self__, key, data[key])
+
+        # SQLAlchemy does very dark black magic and modifies the __init__ method in
+        # sqlalchemy.orm.instrumentation._generate_init()
+        # so, to make SQLAlchemy work, it's needed to explicitly call __init__ to
+        # trigger all the SQLAlchemy logic, it doesn't work using cls.__new__, setting
+        # attributes obj.__dict__, etc. The __init__ method has to be called. But
+        # there are cases where calling all the default logic is not ideal, e.g.
+        # when calling Model.model_validate(), as the validation is done outside
+        # of instance creation.
+        # At the same time, __init__ is what users would normally call, by creating
+        # a new instance, which should have validation and all the default logic.
+        # So, to be able to set up the internal SQLAlchemy logic alone without
+        # executing the rest, and support things like Model.model_validate(), we
+        # use a contextvar to know if we should execute everything.
+        if finish_init.get():
+            sqlmodel_init(self=__pydantic_self__, data=data)
 
     def __setattr__(self, name: str, value: Any) -> None:
         if name in {"_sa_instance_state"}:
@@ -700,59 +720,13 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry
             return
         else:
             # Set in SQLAlchemy, before Pydantic to trigger events and updates
-            if getattr(self.__config__, "table", False) and is_instrumented(self, name):  # type: ignore
+            if is_table_model_class(self.__class__) and is_instrumented(self, name):  # type: ignore[no-untyped-call]
                 set_attribute(self, name, value)
             # Set in Pydantic model to trigger possible validation changes, only for
             # non relationship values
             if name not in self.__sqlmodel_relationships__:
                 super().__setattr__(name, value)
 
-    @classmethod
-    def from_orm(
-        cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None
-    ) -> _TSQLModel:
-        # Duplicated from Pydantic
-        if not cls.__config__.orm_mode:
-            raise ConfigError(
-                "You must have the config attribute orm_mode=True to use from_orm"
-            )
-        obj = {ROOT_KEY: obj} if cls.__custom_root_type__ else cls._decompose_class(obj)
-        # SQLModel, support update dict
-        if update is not None:
-            obj = {**obj, **update}
-        # End SQLModel support dict
-        if not getattr(cls.__config__, "table", False):
-            # If not table, normal Pydantic code
-            m: _TSQLModel = cls.__new__(cls)
-        else:
-            # If table, create the new instance normally to make SQLAlchemy create
-            # the _sa_instance_state attribute
-            m = cls()
-        values, fields_set, validation_error = validate_model(cls, obj)
-        if validation_error:
-            raise validation_error
-        # Updated to trigger SQLAlchemy internal handling
-        if not getattr(cls.__config__, "table", False):
-            object.__setattr__(m, "__dict__", values)
-        else:
-            for key, value in values.items():
-                setattr(m, key, value)
-        # Continue with standard Pydantic logic
-        object.__setattr__(m, "__fields_set__", fields_set)
-        m._init_private_attributes()
-        return m
-
-    @classmethod
-    def parse_obj(
-        cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None
-    ) -> _TSQLModel:
-        obj = cls._enforce_dict_if_root(obj)
-        # SQLModel, support update dict
-        if update is not None:
-            obj = {**obj, **update}
-        # End SQLModel support dict
-        return super().parse_obj(obj)
-
     def __repr_args__(self) -> Sequence[Tuple[Optional[str], Any]]:
         # Don't show SQLAlchemy private attributes
         return [
@@ -761,33 +735,126 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry
             if not (isinstance(k, str) and k.startswith("_sa_"))
         ]
 
-    # From Pydantic, override to enforce validation with dict
+    @declared_attr  # type: ignore
+    def __tablename__(cls) -> str:
+        return cls.__name__.lower()
+
     @classmethod
-    def validate(cls: Type[_TSQLModel], value: Any) -> _TSQLModel:
-        if isinstance(value, cls):
-            return value.copy() if cls.__config__.copy_on_model_validation else value
-
-        value = cls._enforce_dict_if_root(value)
-        if isinstance(value, dict):
-            values, fields_set, validation_error = validate_model(cls, value)
-            if validation_error:
-                raise validation_error
-            model = cls(**value)
-            # Reset fields set, this would have been done in Pydantic in __init__
-            object.__setattr__(model, "__fields_set__", fields_set)
-            return model
-        elif cls.__config__.orm_mode:
-            return cls.from_orm(value)
-        elif cls.__custom_root_type__:
-            return cls.parse_obj(value)
+    def model_validate(
+        cls: Type[_TSQLModel],
+        obj: Any,
+        *,
+        strict: Union[bool, None] = None,
+        from_attributes: Union[bool, None] = None,
+        context: Union[Dict[str, Any], None] = None,
+        update: Union[Dict[str, Any], None] = None,
+    ) -> _TSQLModel:
+        return sqlmodel_validate(
+            cls=cls,
+            obj=obj,
+            strict=strict,
+            from_attributes=from_attributes,
+            context=context,
+            update=update,
+        )
+
+    # TODO: remove when deprecating Pydantic v1, only for compatibility
+    def model_dump(
+        self,
+        *,
+        mode: Union[Literal["json", "python"], str] = "python",
+        include: IncEx = None,
+        exclude: IncEx = None,
+        by_alias: bool = False,
+        exclude_unset: bool = False,
+        exclude_defaults: bool = False,
+        exclude_none: bool = False,
+        round_trip: bool = False,
+        warnings: bool = True,
+    ) -> Dict[str, Any]:
+        if IS_PYDANTIC_V2:
+            return super().model_dump(
+                mode=mode,
+                include=include,
+                exclude=exclude,
+                by_alias=by_alias,
+                exclude_unset=exclude_unset,
+                exclude_defaults=exclude_defaults,
+                exclude_none=exclude_none,
+                round_trip=round_trip,
+                warnings=warnings,
+            )
         else:
-            try:
-                value_as_dict = dict(value)
-            except (TypeError, ValueError) as e:
-                raise DictError() from e
-            return cls(**value_as_dict)
+            return super().dict(
+                include=include,
+                exclude=exclude,
+                by_alias=by_alias,
+                exclude_unset=exclude_unset,
+                exclude_defaults=exclude_defaults,
+                exclude_none=exclude_none,
+            )
+
+    @deprecated(
+        """
+        🚨 `obj.dict()` was deprecated in SQLModel 0.0.14, you should
+        instead use `obj.model_dump()`.
+        """
+    )
+    def dict(
+        self,
+        *,
+        include: IncEx = None,
+        exclude: IncEx = None,
+        by_alias: bool = False,
+        exclude_unset: bool = False,
+        exclude_defaults: bool = False,
+        exclude_none: bool = False,
+    ) -> Dict[str, Any]:
+        return self.model_dump(
+            include=include,
+            exclude=exclude,
+            by_alias=by_alias,
+            exclude_unset=exclude_unset,
+            exclude_defaults=exclude_defaults,
+            exclude_none=exclude_none,
+        )
+
+    @classmethod
+    @deprecated(
+        """
+        🚨 `obj.from_orm(data)` was deprecated in SQLModel 0.0.14, you should
+        instead use `obj.model_validate(data)`.
+        """
+    )
+    def from_orm(
+        cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None
+    ) -> _TSQLModel:
+        return cls.model_validate(obj, update=update)
+
+    @classmethod
+    @deprecated(
+        """
+        🚨 `obj.parse_obj(data)` was deprecated in SQLModel 0.0.14, you should
+        instead use `obj.model_validate(data)`.
+        """
+    )
+    def parse_obj(
+        cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None
+    ) -> _TSQLModel:
+        if not IS_PYDANTIC_V2:
+            obj = cls._enforce_dict_if_root(obj)  # type: ignore[attr-defined] # noqa
+        return cls.model_validate(obj, update=update)
 
     # From Pydantic, override to only show keys from fields, omit SQLAlchemy attributes
+    @deprecated(
+        """
+        🚨 You should not access `obj._calculate_keys()` directly.
+
+        It is only useful for Pydantic v1.X, you should probably upgrade to
+        Pydantic v2.X.
+        """,
+        category=None,
+    )
     def _calculate_keys(
         self,
         include: Optional[Mapping[Union[int, str], Any]],
@@ -795,44 +862,10 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry
         exclude_unset: bool,
         update: Optional[Dict[str, Any]] = None,
     ) -> Optional[AbstractSet[str]]:
-        if include is None and exclude is None and not exclude_unset:
-            # Original in Pydantic:
-            # return None
-            # Updated to not return SQLAlchemy attributes
-            # Do not include relationships as that would easily lead to infinite
-            # recursion, or traversing the whole database
-            return self.__fields__.keys()  # | self.__sqlmodel_relationships__.keys()
-
-        keys: AbstractSet[str]
-        if exclude_unset:
-            keys = self.__fields_set__.copy()
-        else:
-            # Original in Pydantic:
-            # keys = self.__dict__.keys()
-            # Updated to not return SQLAlchemy attributes
-            # Do not include relationships as that would easily lead to infinite
-            # recursion, or traversing the whole database
-            keys = self.__fields__.keys()  # | self.__sqlmodel_relationships__.keys()
-        if include is not None:
-            keys &= include.keys()
-
-        if update:
-            keys -= update.keys()
-
-        if exclude:
-            keys -= {k for k, v in exclude.items() if _value_items_is_true(v)}
-
-        return keys
-
-    @declared_attr  # type: ignore
-    def __tablename__(cls) -> str:
-        return cls.__name__.lower()
-
-
-def _is_field_noneable(field: ModelField) -> bool:
-    if not field.required:
-        # Taken from [Pydantic](https://github.com/samuelcolvin/pydantic/blob/v1.8.2/pydantic/fields.py#L946-L947)
-        return field.allow_none and (
-            field.shape != SHAPE_SINGLETON or not field.sub_fields
+        return _calculate_keys(
+            self,
+            include=include,
+            exclude=exclude,
+            exclude_unset=exclude_unset,
+            update=update,
         )
-    return False
index 7b2cfcd6d54a6efbb880d6be065d6b061a8f977f..e273e235389d833b4ee1c8540ea48f5ac398049f 100644 (file)
@@ -7,6 +7,7 @@ from typing import Any, Callable, Dict, List, Union
 import pytest
 from pydantic import BaseModel
 from sqlmodel import SQLModel
+from sqlmodel._compat import IS_PYDANTIC_V2
 from sqlmodel.main import default_registry
 
 top_level_path = Path(__file__).resolve().parent.parent
@@ -56,12 +57,12 @@ def get_testing_print_function(
         data = []
         for arg in args:
             if isinstance(arg, BaseModel):
-                data.append(arg.dict())
+                data.append(arg.model_dump())
             elif isinstance(arg, list):
                 new_list = []
                 for item in arg:
                     if isinstance(item, BaseModel):
-                        new_list.append(item.dict())
+                        new_list.append(item.model_dump())
                 data.append(new_list)
             else:
                 data.append(arg)
@@ -70,6 +71,9 @@ def get_testing_print_function(
     return new_print
 
 
+needs_pydanticv2 = pytest.mark.skipif(not IS_PYDANTIC_V2, reason="requires Pydantic v2")
+needs_pydanticv1 = pytest.mark.skipif(IS_PYDANTIC_V2, reason="requires Pydantic v1")
+
 needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+")
 needs_py310 = pytest.mark.skipif(
     sys.version_info < (3, 10), reason="requires python3.10+"
diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py
new file mode 100644 (file)
index 0000000..ef66c91
--- /dev/null
@@ -0,0 +1,30 @@
+import pytest
+from sqlmodel import SQLModel
+
+
+class Item(SQLModel):
+    name: str
+
+
+class SubItem(Item):
+    password: str
+
+
+def test_deprecated_from_orm_inheritance():
+    new_item = SubItem(name="Hello", password="secret")
+    with pytest.warns(DeprecationWarning):
+        item = Item.from_orm(new_item)
+    assert item.name == "Hello"
+    assert not hasattr(item, "password")
+
+
+def test_deprecated_parse_obj():
+    with pytest.warns(DeprecationWarning):
+        item = Item.parse_obj({"name": "Hello"})
+    assert item.name == "Hello"
+
+
+def test_deprecated_dict():
+    with pytest.warns(DeprecationWarning):
+        data = Item(name="Hello").dict()
+    assert data == {"name": "Hello"}
index 194bdefea16856f705f93a77b317a6b21e566d01..f0543e90f1d8d32ecc4817ad03a65254121caece 100644 (file)
@@ -5,6 +5,8 @@ from sqlalchemy import create_mock_engine
 from sqlalchemy.sql.type_api import TypeEngine
 from sqlmodel import Field, SQLModel
 
+from .conftest import needs_pydanticv1, needs_pydanticv2
+
 """
 Tests related to Enums
 
@@ -72,7 +74,8 @@ def test_sqlite_ddl_sql(capsys):
     assert "CREATE TYPE" not in captured.out
 
 
-def test_json_schema_flat_model():
+@needs_pydanticv1
+def test_json_schema_flat_model_pydantic_v1():
     assert FlatModel.schema() == {
         "title": "FlatModel",
         "type": "object",
@@ -92,7 +95,8 @@ def test_json_schema_flat_model():
     }
 
 
-def test_json_schema_inherit_model():
+@needs_pydanticv1
+def test_json_schema_inherit_model_pydantic_v1():
     assert InheritModel.schema() == {
         "title": "InheritModel",
         "type": "object",
@@ -110,3 +114,35 @@ def test_json_schema_inherit_model():
             }
         },
     }
+
+
+@needs_pydanticv2
+def test_json_schema_flat_model_pydantic_v2():
+    assert FlatModel.model_json_schema() == {
+        "title": "FlatModel",
+        "type": "object",
+        "properties": {
+            "id": {"title": "Id", "type": "string", "format": "uuid"},
+            "enum_field": {"$ref": "#/$defs/MyEnum1"},
+        },
+        "required": ["id", "enum_field"],
+        "$defs": {
+            "MyEnum1": {"enum": ["A", "B"], "title": "MyEnum1", "type": "string"}
+        },
+    }
+
+
+@needs_pydanticv2
+def test_json_schema_inherit_model_pydantic_v2():
+    assert InheritModel.model_json_schema() == {
+        "title": "InheritModel",
+        "type": "object",
+        "properties": {
+            "id": {"title": "Id", "type": "string", "format": "uuid"},
+            "enum_field": {"$ref": "#/$defs/MyEnum2"},
+        },
+        "required": ["id", "enum_field"],
+        "$defs": {
+            "MyEnum2": {"enum": ["C", "D"], "title": "MyEnum2", "type": "string"}
+        },
+    }
index 7606fd86d800665ce4118d615148905631717c2e..022a100a78901d25eacd0302f9fb6f49d1a146d1 100644 (file)
@@ -6,7 +6,7 @@ from sqlmodel import Field, Relationship, SQLModel
 
 
 def test_sa_relationship_no_args() -> None:
-    with pytest.raises(RuntimeError):
+    with pytest.raises(RuntimeError):  # pragma: no cover
 
         class Team(SQLModel, table=True):
             id: Optional[int] = Field(default=None, primary_key=True)
@@ -30,7 +30,7 @@ def test_sa_relationship_no_args() -> None:
 
 
 def test_sa_relationship_no_kwargs() -> None:
-    with pytest.raises(RuntimeError):
+    with pytest.raises(RuntimeError):  # pragma: no cover
 
         class Team(SQLModel, table=True):
             id: Optional[int] = Field(default=None, primary_key=True)
index 14d560628b930c190c426d550e97b3a11cfa816e..5c8ad77531d538e8daff92b5011bfc5878c30929 100644 (file)
@@ -1,19 +1,16 @@
 from typing import Optional
 
-from sqlalchemy import create_engine, select
-from sqlalchemy.orm import Session
-from sqlmodel import Field, SQLModel
+import pytest
+from pydantic import ValidationError
+from sqlmodel import Field, Session, SQLModel, create_engine, select
 
 
 def test_allow_instantiation_without_arguments(clear_sqlmodel):
-    class Item(SQLModel):
+    class Item(SQLModel, table=True):
         id: Optional[int] = Field(default=None, primary_key=True)
         name: str
         description: Optional[str] = None
 
-        class Config:
-            table = True
-
     engine = create_engine("sqlite:///:memory:")
     SQLModel.metadata.create_all(engine)
     with Session(engine) as db:
@@ -21,7 +18,18 @@ def test_allow_instantiation_without_arguments(clear_sqlmodel):
         item.name = "Rick"
         db.add(item)
         db.commit()
-        result = db.execute(select(Item)).scalars().all()
+        statement = select(Item)
+        result = db.exec(statement).all()
     assert len(result) == 1
     assert isinstance(item.id, int)
     SQLModel.metadata.clear()
+
+
+def test_not_allow_instantiation_without_arguments_if_not_table():
+    class Item(SQLModel):
+        id: Optional[int] = Field(default=None, primary_key=True)
+        name: str
+        description: Optional[str] = None
+
+    with pytest.raises(ValidationError):
+        Item()
index bdbcdeb76dfa9b50034c89a6f3f32cb10152a593..60d5c40ebb5b623da3571c7a80d57f7082df0377 100644 (file)
@@ -91,7 +91,6 @@ def test_should_raise_exception_when_try_to_duplicate_row_if_unique_constraint_i
         with Session(engine) as session:\r
             session.add(hero_2)\r
             session.commit()\r
-            session.refresh(hero_2)\r
 \r
 \r
 def test_sa_relationship_property(clear_sqlmodel):\r
index 2185fa43e9b4a9f58e866402b3459f27ce15c13f..ac4aa42e05287a85a89a2ccd728eefff0def44a5 100644 (file)
@@ -1,17 +1,18 @@
 from typing import Optional
 
 import pytest
+from pydantic import BaseModel
 from sqlmodel import Field, SQLModel
 
 
 def test_missing_sql_type():
-    class CustomType:
+    class CustomType(BaseModel):
         @classmethod
         def __get_validators__(cls):
             yield cls.validate
 
         @classmethod
-        def validate(cls, v):
+        def validate(cls, v):  # pragma: no cover
             return v
 
     with pytest.raises(ValueError):
index 1c8b37b2189e16b51a55c095854e283607712196..a40bb5b5f08d78dd113b7fe3d126c94e354ef64c 100644 (file)
@@ -58,7 +58,7 @@ def test_nullable_fields(clear_sqlmodel, caplog):
     ][0]
     assert "primary_key INTEGER NOT NULL," in create_table_log
     assert "required_value VARCHAR NOT NULL," in create_table_log
-    assert "optional_default_ellipsis VARCHAR NOT NULL," in create_table_log
+    assert "optional_default_ellipsis VARCHAR," in create_table_log
     assert "optional_default_none VARCHAR," in create_table_log
     assert "optional_non_nullable VARCHAR NOT NULL," in create_table_log
     assert "optional_nullable VARCHAR," in create_table_log
index abca97253bc0d6a9f4601f91222bc26a58f71749..88517b92feca5c56549741e68cbf49f424fcff42 100644 (file)
@@ -1,5 +1,6 @@
 from typing import Optional
 
+import pytest
 from sqlmodel import Field, Session, SQLModel, create_engine
 
 
@@ -21,6 +22,7 @@ def test_query(clear_sqlmodel):
         session.refresh(hero_1)
 
     with Session(engine) as session:
-        query_hero = session.query(Hero).first()
+        with pytest.warns(DeprecationWarning):
+            query_hero = session.query(Hero).first()
         assert query_hero
         assert query_hero.name == hero_1.name
index 6a55d6cb986bb934fc51c95b49fec78bd97befaa..706cc8aed779797c573cc8edca714fd3934603fa 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -284,7 +285,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -294,7 +304,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
@@ -302,9 +321,36 @@ def test_tutorial(clear_sqlmodel):
                         "title": "HeroUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "secret_name": IsDict(
+                                {
+                                    "title": "Secret Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Secret Name", "type": "string"}
+                            ),
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index 133b287630e19888987fc1b416df3601b1645501..46c8c42dd34dde689aa23c45ec88645420e3038d 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -287,7 +288,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -297,7 +307,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
@@ -305,9 +324,36 @@ def test_tutorial(clear_sqlmodel):
                         "title": "HeroUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "secret_name": IsDict(
+                                {
+                                    "title": "Secret Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Secret Name", "type": "string"}
+                            ),
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index 5aac8cb11f887a5157eab5fbf2d8658588d0c887..e2874c109579f3cdef06429291dadffd185acb8f 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -287,7 +288,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -297,7 +307,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
@@ -305,9 +324,36 @@ def test_tutorial(clear_sqlmodel):
                         "title": "HeroUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "secret_name": IsDict(
+                                {
+                                    "title": "Secret Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Secret Name", "type": "string"}
+                            ),
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index 2709231504a8b57a847f0321e60633ae26c5dc93..d177c80c4cd4f1035e7df84a586c624684cc75b9 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -217,7 +218,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -227,7 +237,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
index ee0d89ac55ce70b2eea66fd4d54220b1be7e951b..03086996ca7d3f24b017b287e745e29437a6151c 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -220,7 +221,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -230,7 +240,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
index f4ef44abc58dbc53dff7cf46b72f7c44b94fdd01..f7e42e4e205f49f85300e2daf4ea58891f237b90 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -220,7 +221,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -230,7 +240,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
index 7444f8858da11fda7355cd4f8630b557852794e8..2ebfc0c0d0caa0d51543c26a4057443997fe0a95 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlalchemy import inspect
 from sqlalchemy.engine.reflection import Inspector
@@ -53,11 +54,10 @@ def test_tutorial(clear_sqlmodel):
         assert data[1]["id"] != hero2_data["id"]
 
         response = client.get("/openapi.json")
-        data = response.json()
 
         assert response.status_code == 200, response.text
 
-        assert data == {
+        assert response.json() == {
             "openapi": "3.1.0",
             "info": {"title": "FastAPI", "version": "0.1.0"},
             "paths": {
@@ -142,7 +142,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -153,7 +162,16 @@ def test_tutorial(clear_sqlmodel):
                             "id": {"title": "Id", "type": "integer"},
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index 080a907e0e40c470fda7298dfc87b8e78f5bb96a..c17e4829215ed77e0b99c493679884eece81b159 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlalchemy import inspect
 from sqlalchemy.engine.reflection import Inspector
@@ -56,11 +57,9 @@ def test_tutorial(clear_sqlmodel):
         assert data[1]["id"] != hero2_data["id"]
 
         response = client.get("/openapi.json")
-        data = response.json()
-
         assert response.status_code == 200, response.text
 
-        assert data == {
+        assert response.json() == {
             "openapi": "3.1.0",
             "info": {"title": "FastAPI", "version": "0.1.0"},
             "paths": {
@@ -145,7 +144,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -156,7 +164,16 @@ def test_tutorial(clear_sqlmodel):
                             "id": {"title": "Id", "type": "integer"},
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index 7c320093ae25530084b01b39706dcd46bd0f9a1b..258b3a4e54050fcee08deb9b6050e7a3ca7ff54f 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlalchemy import inspect
 from sqlalchemy.engine.reflection import Inspector
@@ -56,11 +57,10 @@ def test_tutorial(clear_sqlmodel):
         assert data[1]["id"] != hero2_data["id"]
 
         response = client.get("/openapi.json")
-        data = response.json()
 
         assert response.status_code == 200, response.text
 
-        assert data == {
+        assert response.json() == {
             "openapi": "3.1.0",
             "info": {"title": "FastAPI", "version": "0.1.0"},
             "paths": {
@@ -145,7 +145,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -156,7 +165,16 @@ def test_tutorial(clear_sqlmodel):
                             "id": {"title": "Id", "type": "integer"},
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index 4a6bb7499e8c28c2ca694c8c747679999500de48..47f2e64155dc08148ded753dcf77142242b09b9d 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlalchemy import inspect
 from sqlalchemy.engine.reflection import Inspector
@@ -53,11 +54,10 @@ def test_tutorial(clear_sqlmodel):
         assert data[1]["id"] != hero2_data["id"]
 
         response = client.get("/openapi.json")
-        data = response.json()
 
         assert response.status_code == 200, response.text
 
-        assert data == {
+        assert response.json() == {
             "openapi": "3.1.0",
             "info": {"title": "FastAPI", "version": "0.1.0"},
             "paths": {
@@ -142,7 +142,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -152,7 +161,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
index 20195c6fdf4631906593d00a66516a323ce974e9..c09b15bd5396ad896ee79468eeb527cff4177999 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlalchemy import inspect
 from sqlalchemy.engine.reflection import Inspector
@@ -56,11 +57,10 @@ def test_tutorial(clear_sqlmodel):
         assert data[1]["id"] != hero2_data["id"]
 
         response = client.get("/openapi.json")
-        data = response.json()
 
         assert response.status_code == 200, response.text
 
-        assert data == {
+        assert response.json() == {
             "openapi": "3.1.0",
             "info": {"title": "FastAPI", "version": "0.1.0"},
             "paths": {
@@ -145,7 +145,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -155,7 +164,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
index 45b061b401e28de925f7194df00a5e23339e20ac..8ad0f271e1c10c4aae3b3397b425a2814105c82d 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlalchemy import inspect
 from sqlalchemy.engine.reflection import Inspector
@@ -56,11 +57,10 @@ def test_tutorial(clear_sqlmodel):
         assert data[1]["id"] != hero2_data["id"]
 
         response = client.get("/openapi.json")
-        data = response.json()
 
         assert response.status_code == 200, response.text
 
-        assert data == {
+        assert response.json() == {
             "openapi": "3.1.0",
             "info": {"title": "FastAPI", "version": "0.1.0"},
             "paths": {
@@ -145,7 +145,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -155,7 +164,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
index 5d2327095eaf7fe8d82bf8b33ebf80717e979d3c..62fbb25a9c24d4a8d82d582c46bccf35ff8768de 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -38,11 +39,10 @@ def test_tutorial(clear_sqlmodel):
         assert response.status_code == 404, response.text
 
         response = client.get("/openapi.json")
-        data = response.json()
 
         assert response.status_code == 200, response.text
 
-        assert data == {
+        assert response.json() == {
             "openapi": "3.1.0",
             "info": {"title": "FastAPI", "version": "0.1.0"},
             "paths": {
@@ -163,7 +163,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -173,7 +182,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
index 2e0a97e78052c4d68b448f8c0c5b2a39016be58f..913d098882179c57aa92ad4a8ce88f758c646a26 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -41,11 +42,10 @@ def test_tutorial(clear_sqlmodel):
         assert response.status_code == 404, response.text
 
         response = client.get("/openapi.json")
-        data = response.json()
 
         assert response.status_code == 200, response.text
 
-        assert data == {
+        assert response.json() == {
             "openapi": "3.1.0",
             "info": {"title": "FastAPI", "version": "0.1.0"},
             "paths": {
@@ -166,7 +166,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -176,7 +185,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
index a663eccac39f7323cf33ebd56314cf2df3792ce1..9bedf5c62dbc98cda5ef48e2ba8799340fa313ca 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -41,11 +42,10 @@ def test_tutorial(clear_sqlmodel):
         assert response.status_code == 404, response.text
 
         response = client.get("/openapi.json")
-        data = response.json()
 
         assert response.status_code == 200, response.text
 
-        assert data == {
+        assert response.json() == {
             "openapi": "3.1.0",
             "info": {"title": "FastAPI", "version": "0.1.0"},
             "paths": {
@@ -166,7 +166,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -176,7 +185,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
index 2c60ce6d04f0324ec3a8e29ce065c62a5d6a474b..b301697dbfa36cc17a3ed1e6b824b4a0178fe4e0 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -531,8 +532,26 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -542,8 +561,26 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
@@ -554,20 +591,85 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
-                            "team": {"$ref": "#/components/schemas/TeamRead"},
+                            "team": IsDict(
+                                {
+                                    "anyOf": [
+                                        {"$ref": "#/components/schemas/TeamRead"},
+                                        {"type": "null"},
+                                    ]
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"$ref": "#/components/schemas/TeamRead"}
+                            ),
                         },
                     },
                     "HeroUpdate": {
                         "title": "HeroUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "secret_name": IsDict(
+                                {
+                                    "title": "Secret Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Secret Name", "type": "string"}
+                            ),
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                         },
                     },
                     "TeamCreate": {
@@ -609,9 +711,36 @@ def test_tutorial(clear_sqlmodel):
                         "title": "TeamUpdate",
                         "type": "object",
                         "properties": {
-                            "id": {"title": "Id", "type": "integer"},
-                            "name": {"title": "Name", "type": "string"},
-                            "headquarters": {"title": "Headquarters", "type": "string"},
+                            "id": IsDict(
+                                {
+                                    "title": "Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Id", "type": "integer"}
+                            ),
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "headquarters": IsDict(
+                                {
+                                    "title": "Headquarters",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Headquarters", "type": "string"}
+                            ),
                         },
                     },
                     "ValidationError": {
index 045a66ba5d927161d73580e96aeb4546833d854c..4d310a87e52d0c9933af1e72761861d8ff66de99 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -534,8 +535,26 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -545,8 +564,26 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
@@ -557,20 +594,85 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
-                            "team": {"$ref": "#/components/schemas/TeamRead"},
+                            "team": IsDict(
+                                {
+                                    "anyOf": [
+                                        {"$ref": "#/components/schemas/TeamRead"},
+                                        {"type": "null"},
+                                    ]
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"$ref": "#/components/schemas/TeamRead"}
+                            ),
                         },
                     },
                     "HeroUpdate": {
                         "title": "HeroUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "secret_name": IsDict(
+                                {
+                                    "title": "Secret Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Secret Name", "type": "string"}
+                            ),
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                         },
                     },
                     "TeamCreate": {
@@ -612,9 +714,36 @@ def test_tutorial(clear_sqlmodel):
                         "title": "TeamUpdate",
                         "type": "object",
                         "properties": {
-                            "id": {"title": "Id", "type": "integer"},
-                            "name": {"title": "Name", "type": "string"},
-                            "headquarters": {"title": "Headquarters", "type": "string"},
+                            "id": IsDict(
+                                {
+                                    "title": "Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Id", "type": "integer"}
+                            ),
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "headquarters": IsDict(
+                                {
+                                    "title": "Headquarters",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Headquarters", "type": "string"}
+                            ),
                         },
                     },
                     "ValidationError": {
index 924d0b90afb72546c85955b304a85aa8334311b9..0603739c4145516dd5e372e1e8360c9a46768540 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -534,8 +535,26 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -545,8 +564,26 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
@@ -557,20 +594,85 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
-                            "team": {"$ref": "#/components/schemas/TeamRead"},
+                            "team": IsDict(
+                                {
+                                    "anyOf": [
+                                        {"$ref": "#/components/schemas/TeamRead"},
+                                        {"type": "null"},
+                                    ]
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"$ref": "#/components/schemas/TeamRead"}
+                            ),
                         },
                     },
                     "HeroUpdate": {
                         "title": "HeroUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "secret_name": IsDict(
+                                {
+                                    "title": "Secret Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Secret Name", "type": "string"}
+                            ),
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                         },
                     },
                     "TeamCreate": {
@@ -612,9 +714,36 @@ def test_tutorial(clear_sqlmodel):
                         "title": "TeamUpdate",
                         "type": "object",
                         "properties": {
-                            "id": {"title": "Id", "type": "integer"},
-                            "name": {"title": "Name", "type": "string"},
-                            "headquarters": {"title": "Headquarters", "type": "string"},
+                            "id": IsDict(
+                                {
+                                    "title": "Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Id", "type": "integer"}
+                            ),
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "headquarters": IsDict(
+                                {
+                                    "title": "Headquarters",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Headquarters", "type": "string"}
+                            ),
                         },
                     },
                     "ValidationError": {
index ca8a41845e100f90e8ad202c048a2c41a034adbe..8f273bbd93ecc430020f6ed5159fc7e4caaf019d 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -31,11 +32,10 @@ def test_tutorial(clear_sqlmodel):
         assert data[0]["secret_name"] == hero_data["secret_name"]
 
         response = client.get("/openapi.json")
-        data = response.json()
 
         assert response.status_code == 200, response.text
 
-        assert data == {
+        assert response.json() == {
             "openapi": "3.1.0",
             "info": {"title": "FastAPI", "version": "0.1.0"},
             "paths": {
@@ -114,10 +114,28 @@ def test_tutorial(clear_sqlmodel):
                         "required": ["name", "secret_name"],
                         "type": "object",
                         "properties": {
-                            "id": {"title": "Id", "type": "integer"},
+                            "id": IsDict(
+                                {
+                                    "title": "Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Id", "type": "integer"}
+                            ),
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index 4acb0068a1b641438e8ff646957720d9af2ffd75..d249cc4e90ede72c99967cbb347af7da0d57a208 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -34,11 +35,10 @@ def test_tutorial(clear_sqlmodel):
         assert data[0]["secret_name"] == hero_data["secret_name"]
 
         response = client.get("/openapi.json")
-        data = response.json()
 
         assert response.status_code == 200, response.text
 
-        assert data == {
+        assert response.json() == {
             "openapi": "3.1.0",
             "info": {"title": "FastAPI", "version": "0.1.0"},
             "paths": {
@@ -117,10 +117,28 @@ def test_tutorial(clear_sqlmodel):
                         "required": ["name", "secret_name"],
                         "type": "object",
                         "properties": {
-                            "id": {"title": "Id", "type": "integer"},
+                            "id": IsDict(
+                                {
+                                    "title": "Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Id", "type": "integer"}
+                            ),
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index 20f3f52313bc8bead709469547610ee14b3b92f8..b9fb2be03f981943209fc27149b7c1685be1352f 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -34,11 +35,10 @@ def test_tutorial(clear_sqlmodel):
         assert data[0]["secret_name"] == hero_data["secret_name"]
 
         response = client.get("/openapi.json")
-        data = response.json()
 
         assert response.status_code == 200, response.text
 
-        assert data == {
+        assert response.json() == {
             "openapi": "3.1.0",
             "info": {"title": "FastAPI", "version": "0.1.0"},
             "paths": {
@@ -117,10 +117,28 @@ def test_tutorial(clear_sqlmodel):
                         "required": ["name", "secret_name"],
                         "type": "object",
                         "properties": {
-                            "id": {"title": "Id", "type": "integer"},
+                            "id": IsDict(
+                                {
+                                    "title": "Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Id", "type": "integer"}
+                            ),
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index 6f97cbf92be18ff79ec528b61610aff1528f23e7..441cc42b282c6dd7655d2f5e9ca679cc22bc5fb0 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -284,7 +285,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -294,7 +304,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
@@ -302,9 +321,36 @@ def test_tutorial(clear_sqlmodel):
                         "title": "HeroUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "secret_name": IsDict(
+                                {
+                                    "title": "Secret Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Secret Name", "type": "string"}
+                            ),
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index f0c5416bdf2f4d9d8023fac67fb0b95471c6a58c..7c427a1c675f55e1bcce0eb2d8ce2e901d4216e0 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -289,7 +290,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -299,7 +309,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
@@ -307,9 +326,36 @@ def test_tutorial(clear_sqlmodel):
                         "title": "HeroUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "secret_name": IsDict(
+                                {
+                                    "title": "Secret Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Secret Name", "type": "string"}
+                            ),
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index 5b911c8462e7749fcc507544cf91835d0358a072..ea63f52c4132edb0dd1d15229959e0095e0491ba 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -289,7 +290,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -299,7 +309,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
@@ -307,9 +326,36 @@ def test_tutorial(clear_sqlmodel):
                         "title": "HeroUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "secret_name": IsDict(
+                                {
+                                    "title": "Secret Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Secret Name", "type": "string"}
+                            ),
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index 2136ed8a1f309e121055cf3ebaf5bb957a50fc1a..9df7e50b814e95a6945296b9a8704974d022c9e3 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -51,11 +52,10 @@ def test_tutorial(clear_sqlmodel):
         assert data[1]["id"] == hero2_data["id"]
 
         response = client.get("/openapi.json")
-        data = response.json()
 
         assert response.status_code == 200, response.text
 
-        assert data == {
+        assert response.json() == {
             "openapi": "3.1.0",
             "info": {"title": "FastAPI", "version": "0.1.0"},
             "paths": {
@@ -120,10 +120,28 @@ def test_tutorial(clear_sqlmodel):
                         "required": ["name", "secret_name"],
                         "type": "object",
                         "properties": {
-                            "id": {"title": "Id", "type": "integer"},
+                            "id": IsDict(
+                                {
+                                    "title": "Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Id", "type": "integer"}
+                            ),
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index d85d9ee5b2b04161373c854e90738168244f9f57..a47513dde246748202b49335891ad53609920ffd 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -54,11 +55,10 @@ def test_tutorial(clear_sqlmodel):
         assert data[1]["id"] == hero2_data["id"]
 
         response = client.get("/openapi.json")
-        data = response.json()
 
         assert response.status_code == 200, response.text
 
-        assert data == {
+        assert response.json() == {
             "openapi": "3.1.0",
             "info": {"title": "FastAPI", "version": "0.1.0"},
             "paths": {
@@ -123,10 +123,28 @@ def test_tutorial(clear_sqlmodel):
                         "required": ["name", "secret_name"],
                         "type": "object",
                         "properties": {
-                            "id": {"title": "Id", "type": "integer"},
+                            "id": IsDict(
+                                {
+                                    "title": "Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Id", "type": "integer"}
+                            ),
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index a1be7b094a5cb3aafd7125f7617e06318e26fd44..a532625d420f2ff12ce07833d2b816665d72e98d 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -518,8 +519,26 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -529,8 +548,26 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
@@ -538,10 +575,46 @@ def test_tutorial(clear_sqlmodel):
                         "title": "HeroUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "secret_name": IsDict(
+                                {
+                                    "title": "Secret Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Secret Name", "type": "string"}
+                            ),
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                         },
                     },
                     "TeamCreate": {
@@ -567,8 +640,26 @@ def test_tutorial(clear_sqlmodel):
                         "title": "TeamUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "headquarters": {"title": "Headquarters", "type": "string"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "headquarters": IsDict(
+                                {
+                                    "title": "Headquarters",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Headquarters", "type": "string"}
+                            ),
                         },
                     },
                     "ValidationError": {
index 882fcc796b6ebcec26e6f3a2aa0b37853593b303..33029f6b603c63327b150d56ce0481f45549d16e 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -521,8 +522,26 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -532,8 +551,26 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
@@ -541,10 +578,46 @@ def test_tutorial(clear_sqlmodel):
                         "title": "HeroUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "secret_name": IsDict(
+                                {
+                                    "title": "Secret Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Secret Name", "type": "string"}
+                            ),
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                         },
                     },
                     "TeamCreate": {
@@ -570,8 +643,26 @@ def test_tutorial(clear_sqlmodel):
                         "title": "TeamUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "headquarters": {"title": "Headquarters", "type": "string"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "headquarters": IsDict(
+                                {
+                                    "title": "Headquarters",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Headquarters", "type": "string"}
+                            ),
                         },
                     },
                     "ValidationError": {
index 12791b269c1351b49cb4995f6f55098130147022..66705e17cc5291afb533d5a2a939d699ac1437e0 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -521,8 +522,26 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -532,8 +551,26 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
@@ -541,10 +578,46 @@ def test_tutorial(clear_sqlmodel):
                         "title": "HeroUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
-                            "team_id": {"title": "Team Id", "type": "integer"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "secret_name": IsDict(
+                                {
+                                    "title": "Secret Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Secret Name", "type": "string"}
+                            ),
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
+                            "team_id": IsDict(
+                                {
+                                    "title": "Team Id",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Team Id", "type": "integer"}
+                            ),
                         },
                     },
                     "TeamCreate": {
@@ -570,8 +643,26 @@ def test_tutorial(clear_sqlmodel):
                         "title": "TeamUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "headquarters": {"title": "Headquarters", "type": "string"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "headquarters": IsDict(
+                                {
+                                    "title": "Headquarters",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Headquarters", "type": "string"}
+                            ),
                         },
                     },
                     "ValidationError": {
index a4573ef11bc51555f7dccebbd42bb7032bbea48e..973ab2db04df2b478c10674c83da07a4321d7cde 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -263,7 +264,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -273,7 +283,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
@@ -281,9 +300,36 @@ def test_tutorial(clear_sqlmodel):
                         "title": "HeroUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "secret_name": IsDict(
+                                {
+                                    "title": "Secret Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Secret Name", "type": "string"}
+                            ),
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index cf56e3cb0121449e3c339970dbcb1ccec9eb7c67..090af8c60f5a4930bf394d3028824cc2874f3b93 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -266,7 +267,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -276,7 +286,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
@@ -284,9 +303,36 @@ def test_tutorial(clear_sqlmodel):
                         "title": "HeroUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "secret_name": IsDict(
+                                {
+                                    "title": "Secret Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Secret Name", "type": "string"}
+                            ),
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index b301ca3bf15d4206a3b479a6832e77a58b18b202..22dfb8f2688e62b8ecbaaf74caac9f0937d815a4 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 from sqlmodel import create_engine
 from sqlmodel.pool import StaticPool
@@ -266,7 +267,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "HeroRead": {
@@ -276,7 +286,16 @@ def test_tutorial(clear_sqlmodel):
                         "properties": {
                             "name": {"title": "Name", "type": "string"},
                             "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                             "id": {"title": "Id", "type": "integer"},
                         },
                     },
@@ -284,9 +303,36 @@ def test_tutorial(clear_sqlmodel):
                         "title": "HeroUpdate",
                         "type": "object",
                         "properties": {
-                            "name": {"title": "Name", "type": "string"},
-                            "secret_name": {"title": "Secret Name", "type": "string"},
-                            "age": {"title": "Age", "type": "integer"},
+                            "name": IsDict(
+                                {
+                                    "title": "Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Name", "type": "string"}
+                            ),
+                            "secret_name": IsDict(
+                                {
+                                    "title": "Secret Name",
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Secret Name", "type": "string"}
+                            ),
+                            "age": IsDict(
+                                {
+                                    "title": "Age",
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Age", "type": "integer"}
+                            ),
                         },
                     },
                     "ValidationError": {
index ad60fcb945e285cfc7044a9cdd8ecd1cc083a3ed..3265922070bb1b2d0ec5d1b77d55ad7df5fae8f3 100644 (file)
@@ -1,12 +1,14 @@
 from typing import Optional
 
 import pytest
-from pydantic import validator
 from pydantic.error_wrappers import ValidationError
 from sqlmodel import SQLModel
 
+from .conftest import needs_pydanticv1, needs_pydanticv2
 
-def test_validation(clear_sqlmodel):
+
+@needs_pydanticv1
+def test_validation_pydantic_v1(clear_sqlmodel):
     """Test validation of implicit and explicit None values.
 
     # For consistency with pydantic, validators are not to be called on
@@ -16,6 +18,7 @@ def test_validation(clear_sqlmodel):
     https://github.com/samuelcolvin/pydantic/issues/1223
 
     """
+    from pydantic import validator
 
     class Hero(SQLModel):
         name: Optional[str] = None
@@ -31,3 +34,32 @@ def test_validation(clear_sqlmodel):
 
     with pytest.raises(ValidationError):
         Hero.validate({"name": None, "age": 25})
+
+
+@needs_pydanticv2
+def test_validation_pydantic_v2(clear_sqlmodel):
+    """Test validation of implicit and explicit None values.
+
+    # For consistency with pydantic, validators are not to be called on
+    # arguments that are not explicitly provided.
+
+    https://github.com/tiangolo/sqlmodel/issues/230
+    https://github.com/samuelcolvin/pydantic/issues/1223
+
+    """
+    from pydantic import field_validator
+
+    class Hero(SQLModel):
+        name: Optional[str] = None
+        secret_name: Optional[str] = None
+        age: Optional[int] = None
+
+        @field_validator("name", "secret_name", "age")
+        def reject_none(cls, v):
+            assert v is not None
+            return v
+
+    Hero.model_validate({"age": 25})
+
+    with pytest.raises(ValidationError):
+        Hero.model_validate({"name": None, "age": 25})