diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9f2688dff..ade60f255 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/docs/tutorial/fastapi/multiple-models.md b/docs/tutorial/fastapi/multiple-models.md index d57d1bd9b..3995daa65 100644 --- a/docs/tutorial/fastapi/multiple-models.md +++ b/docs/tutorial/fastapi/multiple-models.md @@ -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 👆 diff --git a/docs/tutorial/fastapi/update.md b/docs/tutorial/fastapi/update.md index 27c413f38..cfcf8a98e 100644 --- a/docs/tutorial/fastapi/update.md +++ b/docs/tutorial/fastapi/update.md @@ -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**. 😎 diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 1e78c9c4f..9b0939b0c 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -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` diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py index 3f0602e4b..7014a7391 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py @@ -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) diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py index e8615d91d..cf1bbb713 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py @@ -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) diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py index 9816e70eb..9f428ab3e 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py @@ -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) diff --git a/docs_src/tutorial/fastapi/delete/tutorial001.py b/docs_src/tutorial/fastapi/delete/tutorial001.py index 3069fc5e8..532817360 100644 --- a/docs_src/tutorial/fastapi/delete/tutorial001.py +++ b/docs_src/tutorial/fastapi/delete/tutorial001.py @@ -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) diff --git a/docs_src/tutorial/fastapi/delete/tutorial001_py310.py b/docs_src/tutorial/fastapi/delete/tutorial001_py310.py index 5b2da0a0b..45e2e1d51 100644 --- a/docs_src/tutorial/fastapi/delete/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/delete/tutorial001_py310.py @@ -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) diff --git a/docs_src/tutorial/fastapi/delete/tutorial001_py39.py b/docs_src/tutorial/fastapi/delete/tutorial001_py39.py index 5f498cf13..12f6bc3f9 100644 --- a/docs_src/tutorial/fastapi/delete/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/delete/tutorial001_py39.py @@ -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) diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py index 2b8739ca7..2352f3902 100644 --- a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py +++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py @@ -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) diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py index 874a6e843..ad8ff95e3 100644 --- a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py @@ -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) diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py index b63fa753f..b1f7cdcb6 100644 --- a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py @@ -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) diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001.py index df2012333..7f59ac6a1 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial001.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial001.py @@ -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) diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py index 13129f383..ff12eff55 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py @@ -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) diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py index 41a51f448..977a1ac8d 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py @@ -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) diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002.py index 392c2c582..fffbe7249 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial002.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial002.py @@ -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) diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py index 3eda88b19..7373edff5 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py @@ -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) diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py index 473fe5b83..1b4a51252 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py @@ -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) diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001.py b/docs_src/tutorial/fastapi/read_one/tutorial001.py index 4d66e471a..f18426e74 100644 --- a/docs_src/tutorial/fastapi/read_one/tutorial001.py +++ b/docs_src/tutorial/fastapi/read_one/tutorial001.py @@ -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) diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py b/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py index 8883570dc..e8c7d49b9 100644 --- a/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py @@ -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) diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py b/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py index 0ad701668..4dc5702fb 100644 --- a/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py @@ -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) diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001.py b/docs_src/tutorial/fastapi/relationships/tutorial001.py index 8477e4a2a..51339e2a2 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001.py @@ -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) diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py b/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py index bec6a6f2e..35257bd51 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py @@ -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) diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py b/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py index 389390551..6ceae130a 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py @@ -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) diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py index 3f0602e4b..7014a7391 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py @@ -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) diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py index e8615d91d..cf1bbb713 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py @@ -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) diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py index 9816e70eb..9f428ab3e 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py @@ -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) diff --git a/docs_src/tutorial/fastapi/teams/tutorial001.py b/docs_src/tutorial/fastapi/teams/tutorial001.py index 1da0dad8a..785c52591 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001.py @@ -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) diff --git a/docs_src/tutorial/fastapi/teams/tutorial001_py310.py b/docs_src/tutorial/fastapi/teams/tutorial001_py310.py index a9a527df7..dea4bd8a9 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001_py310.py @@ -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) diff --git a/docs_src/tutorial/fastapi/teams/tutorial001_py39.py b/docs_src/tutorial/fastapi/teams/tutorial001_py39.py index 1a3642899..cc6429adc 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001_py39.py @@ -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) diff --git a/docs_src/tutorial/fastapi/update/tutorial001.py b/docs_src/tutorial/fastapi/update/tutorial001.py index bb98efd58..5639638d5 100644 --- a/docs_src/tutorial/fastapi/update/tutorial001.py +++ b/docs_src/tutorial/fastapi/update/tutorial001.py @@ -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) diff --git a/docs_src/tutorial/fastapi/update/tutorial001_py310.py b/docs_src/tutorial/fastapi/update/tutorial001_py310.py index 79069181f..4faf266f8 100644 --- a/docs_src/tutorial/fastapi/update/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/update/tutorial001_py310.py @@ -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) diff --git a/docs_src/tutorial/fastapi/update/tutorial001_py39.py b/docs_src/tutorial/fastapi/update/tutorial001_py39.py index c788eb1c7..b0daa8788 100644 --- a/docs_src/tutorial/fastapi/update/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/update/tutorial001_py39.py @@ -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) diff --git a/pyproject.toml b/pyproject.toml index 24a6c5c22..10d73793d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 index 000000000..2a2caca3e --- /dev/null +++ b/sqlmodel/_compat.py @@ -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]) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index c30af5779..10064c711 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -11,7 +11,6 @@ Callable, ClassVar, Dict, - ForwardRef, List, Mapping, Optional, @@ -25,13 +24,8 @@ 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.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 @@ def __new__( ) -> 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 @@ def __new__( 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 @@ def __new__( } 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 @@ def get_config(name: str) -> Any: 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 @@ def __init__( # 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 @@ def __init__( # 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 @@ def __init__( 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 @@ def get_column_from_field(field: ModelField) -> Column: # 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 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Any: # 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 @@ def __setattr__(self, name: str, value: Any) -> None: 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 @@ def __repr_args__(self) -> Sequence[Tuple[Optional[str], Any]]: 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 @@ def _calculate_keys( 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 diff --git a/tests/conftest.py b/tests/conftest.py index 7b2cfcd6d..e273e2353 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ 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 new_print(*args): 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 new_print(*args): 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 index 000000000..ef66c91b5 --- /dev/null +++ b/tests/test_deprecations.py @@ -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"} diff --git a/tests/test_enums.py b/tests/test_enums.py index 194bdefea..f0543e90f 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -5,6 +5,8 @@ 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"} + }, + } diff --git a/tests/test_field_sa_relationship.py b/tests/test_field_sa_relationship.py index 7606fd86d..022a100a7 100644 --- a/tests/test_field_sa_relationship.py +++ b/tests/test_field_sa_relationship.py @@ -6,7 +6,7 @@ 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 @@ class Hero(SQLModel, table=True): 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) diff --git a/tests/test_instance_no_args.py b/tests/test_instance_no_args.py index 14d560628..5c8ad7753 100644 --- a/tests/test_instance_no_args.py +++ b/tests/test_instance_no_args.py @@ -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 @@ class Config: 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() diff --git a/tests/test_main.py b/tests/test_main.py index bdbcdeb76..60d5c40eb 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -91,7 +91,6 @@ class Hero(SQLModel, table=True): with Session(engine) as session: session.add(hero_2) session.commit() - session.refresh(hero_2) def test_sa_relationship_property(clear_sqlmodel): diff --git a/tests/test_missing_type.py b/tests/test_missing_type.py index 2185fa43e..ac4aa42e0 100644 --- a/tests/test_missing_type.py +++ b/tests/test_missing_type.py @@ -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): diff --git a/tests/test_nullable.py b/tests/test_nullable.py index 1c8b37b21..a40bb5b5f 100644 --- a/tests/test_nullable.py +++ b/tests/test_nullable.py @@ -58,7 +58,7 @@ class Hero(SQLModel, table=True): ][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 diff --git a/tests/test_query.py b/tests/test_query.py index abca97253..88517b92f 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,5 +1,6 @@ from typing import Optional +import pytest from sqlmodel import Field, Session, SQLModel, create_engine @@ -21,6 +22,7 @@ class Hero(SQLModel, table=True): 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 diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py index 6a55d6cb9..706cc8aed 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py index 133b28763..46c8c42dd 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py index 5aac8cb11..e2874c109 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py index 270923150..d177c80c4 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py @@ -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"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py index ee0d89ac5..03086996c 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py @@ -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"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py index f4ef44abc..f7e42e4e2 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py @@ -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"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py index 7444f8858..2ebfc0c0d 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py index 080a907e0..c17e48292 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py index 7c320093a..258b3a4e5 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py index 4a6bb7499..47f2e6415 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py @@ -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"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py index 20195c6fd..c09b15bd5 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py @@ -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"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py index 45b061b40..8ad0f271e 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py @@ -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"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py index 5d2327095..62fbb25a9 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py @@ -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"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py index 2e0a97e78..913d09888 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py @@ -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"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py index a663eccac..9bedf5c62 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py @@ -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"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py index 2c60ce6d0..b301697db 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py index 045a66ba5..4d310a87e 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py index 924d0b90a..0603739c4 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py index ca8a41845..8f273bbd9 100644 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py index 4acb0068a..d249cc4e9 100644 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py index 20f3f5231..b9fb2be03 100644 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py index 6f97cbf92..441cc42b2 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py index f0c5416bd..7c427a1c6 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py index 5b911c846..ea63f52c4 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py index 2136ed8a1..9df7e50b8 100644 --- a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py index d85d9ee5b..a47513dde 100644 --- a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py index a1be7b094..a532625d4 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py index 882fcc796..33029f6b6 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py index 12791b269..66705e17c 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py index a4573ef11..973ab2db0 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py index cf56e3cb0..090af8c60 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py @@ -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": { diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py index b301ca3bf..22dfb8f26 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py @@ -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": { diff --git a/tests/test_validation.py b/tests/test_validation.py index ad60fcb94..326592207 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -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 reject_none(cls, v): 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})