├── .docker └── Dockerfile ├── .flake8 ├── .github ├── dependabot.yaml └── workflows │ ├── ci.yaml │ ├── docs.yaml │ └── publish.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docker-compose.yaml ├── docs ├── css │ └── extra.css ├── images │ ├── 2x │ │ └── starlite-icon@2x.png │ ├── starlite-banner.svg │ └── starlite-favicon.ico ├── index.md └── usage │ ├── 0-build-methods.md │ ├── 1-nest-models.md │ ├── 2-dataclasses.md │ ├── 3-configuration.md │ ├── 4-defining-factory-fields.md │ ├── 5-persistence.md │ ├── 6-extensions.md │ ├── 7-handling-custom-types.md │ └── 8-pytest-fixtures.md ├── mkdocs.yml ├── mypy.ini ├── poetry.lock ├── pydantic_factories ├── __init__.py ├── constraints │ ├── __init__.py │ ├── collection.py │ ├── date.py │ ├── decimal.py │ ├── float.py │ ├── integer.py │ └── strings.py ├── exceptions.py ├── extensions │ ├── __init__.py │ ├── beanie_odm.py │ ├── odmantic_odm.py │ └── ormar_orm.py ├── factory.py ├── fields.py ├── plugins │ ├── __init__.py │ └── pytest_plugin.py ├── protocols.py ├── py.typed ├── utils.py └── value_generators │ ├── __init__.py │ ├── complex_types.py │ ├── constrained_number.py │ ├── primitives.py │ └── regex.py ├── pyproject.toml ├── sonar-project.properties └── tests ├── __init__.py ├── conftest.py ├── constraints ├── test_byte_constraints.py ├── test_date_constraints.py ├── test_decimal_constraints.py ├── test_float_constraints.py ├── test_frozen_set_constraints.py ├── test_int_constraints.py ├── test_list_constraints.py ├── test_set_constraints.py └── test_string_constraints.py ├── extensions ├── test_beanie_extension.py ├── test_odmantic_extension.py └── test_ormar_extension.py ├── models.py ├── plugins └── test_pytest_plugin.py ├── test_complex_types.py ├── test_constrained_attribute_parsing.py ├── test_data_parsing.py ├── test_dataclass.py ├── test_dicts.py ├── test_discriminated_unions.py ├── test_factory_auto_registration.py ├── test_factory_build.py ├── test_factory_child_models.py ├── test_factory_fields.py ├── test_factory_options.py ├── test_generics.py ├── test_new_types.py ├── test_protocols.py ├── test_random_seed.py ├── test_refex_factory.py ├── test_typeddict.py ├── test_utils.py └── typing_test_strict.py /.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # this file is used to create an image for running the docs. 2 | FROM squidfunk/mkdocs-material 3 | 4 | RUN pip install --upgrade pip && pip install --no-cache-dir mkdocstrings[python] black 5 | 6 | ENTRYPOINT ["mkdocs"] 7 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | max-complexity = 12 4 | ignore = E501, W503 5 | per-file-ignores = 6 | **/test_*:SCS108 7 | type-checking-pydantic-enabled = true 8 | type-checking-fastapi-enabled = true 9 | classmethod-decorators = 10 | classmethod 11 | validator 12 | root_validator 13 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | validate: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-python@v4 13 | with: 14 | python-version: "3.11" 15 | - name: Install Pre-Commit 16 | run: python -m pip install pre-commit && pre-commit install 17 | - name: Load cached Pre-Commit Dependencies 18 | id: cached-poetry-dependencies 19 | uses: actions/cache@v3 20 | with: 21 | path: ~/.cache/pre-commit/ 22 | key: pre-commit-4|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} 23 | - name: Execute Pre-Commit 24 | run: pre-commit run --show-diff-on-failure --color=always --all-files 25 | test: 26 | runs-on: ubuntu-latest 27 | strategy: 28 | fail-fast: true 29 | matrix: 30 | python-version: ["3.8", "3.9", "3.10", "3.11"] 31 | steps: 32 | - name: Start MongoDB 33 | uses: supercharge/mongodb-github-action@1.9.0 34 | - name: Check out repository 35 | uses: actions/checkout@v3 36 | - name: Set up python ${{ matrix.python-version }} 37 | uses: actions/setup-python@v4 38 | with: 39 | python-version: ${{ matrix.python-version }} 40 | - name: Install Poetry 41 | uses: snok/install-poetry@v1 42 | with: 43 | virtualenvs-create: true 44 | virtualenvs-in-project: true 45 | installer-parallel: true 46 | - name: Load cached venv 47 | id: cached-poetry-dependencies 48 | uses: actions/cache@v3 49 | with: 50 | path: .venv 51 | key: v1-venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} 52 | restore-keys: | 53 | v1-venv-${{ runner.os }}-${{ matrix.python-version }} 54 | v1-venv-${{ runner.os }} 55 | - name: Install dependencies 56 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 57 | run: poetry install --no-interaction --no-root && poetry add asyncpg beanie ormar && pip install odmantic 58 | - name: Set pythonpath 59 | run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV 60 | - name: Test 61 | if: matrix.python-version != '3.11' 62 | run: poetry run pytest 63 | - name: Test with Coverage 64 | if: matrix.python-version == '3.11' 65 | run: poetry run pytest --cov=. --cov-report=xml 66 | - uses: actions/upload-artifact@v3 67 | if: matrix.python-version == '3.11' 68 | with: 69 | name: coverage-xml 70 | path: coverage.xml 71 | sonar: 72 | needs: 73 | - test 74 | - validate 75 | if: github.event.pull_request.head.repo.fork == false 76 | runs-on: ubuntu-latest 77 | steps: 78 | - name: Check out repository 79 | uses: actions/checkout@v3 80 | - name: Download Artifacts 81 | uses: actions/download-artifact@v3 82 | with: 83 | name: coverage-xml 84 | - name: Fix coverage file for sonarcloud 85 | run: sed -i "s/home\/runner\/work\/starlite\/starlite/github\/workspace/g" coverage.xml 86 | - name: SonarCloud Scan 87 | uses: sonarsource/sonarcloud-github-action@master 88 | env: 89 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 90 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 91 | codeql: 92 | needs: 93 | - test 94 | - validate 95 | runs-on: ubuntu-latest 96 | permissions: 97 | security-events: write 98 | steps: 99 | - name: Checkout repository 100 | uses: actions/checkout@v3 101 | - name: Initialize CodeQL With Dependencies 102 | if: github.event_name == 'push' && github.ref_name == 'main' 103 | uses: github/codeql-action/init@v2 104 | - name: Initialize CodeQL Without Dependencies 105 | if: github.event_name == 'pull_request' 106 | uses: github/codeql-action/init@v2 107 | with: 108 | setup-python-dependencies: false 109 | - name: Perform CodeQL Analysis 110 | uses: github/codeql-action/analyze@v2 111 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | workflow_run: 4 | workflows: ["ci"] 5 | branches: [main] 6 | types: 7 | - completed 8 | jobs: 9 | docs: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: "3.10" 17 | - run: pip install mkdocs-material 18 | - run: mkdocs gh-deploy --force 19 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish-release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check out repository 12 | uses: actions/checkout@v3 13 | - name: Set up python 3.11 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: "3.11" 17 | - name: Install Poetry 18 | uses: snok/install-poetry@v1 19 | - name: Install dependencies 20 | run: poetry install --no-interaction --no-root --no-dev 21 | - name: publish 22 | shell: bash 23 | run: | 24 | poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} 25 | poetry publish --build --no-interaction 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # folders 2 | .pytest_cache/ 3 | .hypothesis/ 4 | .mypy_cache/ 5 | .idea/ 6 | __pycache__/ 7 | dist/ 8 | 9 | # files 10 | .DS_Store 11 | coverage.* 12 | .coverage 13 | .python-version 14 | *.iml 15 | *.mod 16 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: "3.11" 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.4.0 6 | hooks: 7 | - id: check-ast 8 | - id: check-case-conflict 9 | - id: check-merge-conflict 10 | - id: check-toml 11 | - id: debug-statements 12 | - id: end-of-file-fixer 13 | - id: mixed-line-ending 14 | - id: trailing-whitespace 15 | - repo: https://github.com/asottile/pyupgrade 16 | rev: v3.3.1 17 | hooks: 18 | - id: pyupgrade 19 | args: ["--py38-plus"] 20 | - repo: https://github.com/hadialqattan/pycln 21 | rev: v2.1.3 22 | hooks: 23 | - id: pycln 24 | args: [--config=pyproject.toml] 25 | - repo: https://github.com/pycqa/isort 26 | rev: 5.12.0 27 | hooks: 28 | - id: isort 29 | - repo: https://github.com/psf/black 30 | rev: 23.1.0 31 | hooks: 32 | - id: black 33 | args: [--config=./pyproject.toml] 34 | - repo: https://github.com/codespell-project/codespell 35 | rev: v2.2.2 36 | hooks: 37 | - id: codespell 38 | - repo: https://github.com/pre-commit/mirrors-prettier 39 | rev: "v3.0.0-alpha.4" 40 | hooks: 41 | - id: prettier 42 | - repo: https://github.com/pycqa/bandit 43 | rev: 1.7.4 44 | hooks: 45 | - id: bandit 46 | exclude: "test_*" 47 | args: ["-iii", "-ll", "-s=B308,B703"] 48 | - repo: https://github.com/PyCQA/flake8 49 | rev: 6.0.0 50 | hooks: 51 | - id: flake8 52 | additional_dependencies: 53 | [ 54 | "flake8-bugbear", 55 | "flake8-comprehensions", 56 | "flake8-mutable", 57 | "flake8-print", 58 | "flake8-simplify", 59 | "flake8-type-checking", 60 | "flake8-implicit-str-concat", 61 | "flake8-noqa", 62 | "flake8-return", 63 | "flake8-secure-coding-standard", 64 | "flake8-encodings", 65 | "flake8-use-fstring", 66 | "flake8-use-pathlib", 67 | ] 68 | - repo: https://github.com/pycqa/pylint 69 | rev: "v2.16.1" 70 | hooks: 71 | - id: pylint 72 | exclude: "test_*" 73 | args: ["--unsafe-load-any-extension=y"] 74 | additional_dependencies: 75 | [pydantic, faker, pytest, hypothesis, odmantic, ormar, beanie] 76 | - repo: https://github.com/pre-commit/mirrors-mypy 77 | rev: "v1.0.0" 78 | hooks: 79 | - id: mypy 80 | exclude: "extension" 81 | additional_dependencies: 82 | [pydantic, faker, pytest, hypothesis, odmantic, ormar, beanie] 83 | - repo: https://github.com/RobertCraigie/pyright-python 84 | rev: v1.1.293 85 | hooks: 86 | - id: pyright 87 | exclude: "extension" 88 | additional_dependencies: 89 | [pydantic, faker, pytest, hypothesis, odmantic, ormar, beanie] 90 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | [1.17.2] 4 | 5 | - update dependencies and code cleanup. 6 | 7 | [1.17.1] 8 | 9 | - add `deepcopy` to ormar fields. 10 | 11 | [1.17.0] 12 | 13 | - add `GenericModel` to `is_pydantic_model` checks. 14 | - add auto-registration hook. 15 | - improve typing of internal number generator. 16 | 17 | [1.16.0] 18 | 19 | - drop Python 3.7 support. 20 | - fix deprecation warning for `sre_parse` on Python 3.11. 21 | - fix inclusive ranges in `conint`. 22 | - fix partial kwargs detection for deeply nested models. 23 | - improve invalid range detection when `multiple_of` is included into field params. 24 | 25 | [1.15.0] 26 | 27 | - fix `confrozenset`. 28 | - add `NewType` support. 29 | - add unique_items support for `conlist`. 30 | 31 | [1.14.1] 32 | 33 | - fix `ModelFactory` build ignore error when specifying an explicit dict for a nested `Dict` field @anthonyh209. 34 | 35 | [1.14.0] 36 | 37 | - replace `Xeger` with local port to generate regexes. 38 | - update `BeaniePlugin` to support ID. 39 | 40 | [1.13.0] 41 | 42 | - fix `ModelFactory` mistaking model fields with names identical to factory methods to be factory fields. 43 | 44 | [1.12.0] 45 | 46 | - add `TypedDict` support. 47 | 48 | [1.11.1] 49 | 50 | - fix `Any` check. 51 | 52 | [1.11.0] 53 | 54 | - add `Fixture` field type. 55 | 56 | [1.10.0] 57 | 58 | - add support for `ConstrainedDate`. 59 | 60 | [1.9.0] 61 | 62 | - update `ModelFactory` to expose `get_faker()`. 63 | 64 | [1.8.2] 65 | 66 | - fix `pytest` being a required dependency. 67 | 68 | [1.8.1] 69 | 70 | - fix recursion exception with discriminated unions. 71 | 72 | [1.8.0] 73 | 74 | - add support for automatic pytest fixture creation @EltonChou. 75 | - add support for plugins @EltonChou. 76 | 77 | [1.7.1] 78 | 79 | - fix passing dictionaries with nested pydantic models. 80 | 81 | [1.7.0] 82 | 83 | - add Python `3.11` support. 84 | 85 | [1.6.2] 86 | 87 | - fix random-seed doesn't affect UUID generation. 88 | 89 | [1.6.1] 90 | 91 | - updated pydantic version to `1.10.0`. 92 | 93 | [1.6.0] 94 | 95 | - update pydantic version to `1.9.2` and restrict version range to `>=1.9.0`. 96 | 97 | [1.5.4] 98 | 99 | - fix error when building with a parameter that is an optional pydantic model @phbernardes. 100 | 101 | [1.5.3] 102 | 103 | - fix error with decimal validation. 104 | 105 | [1.5.2] 106 | 107 | - fix error when building with a parameter that is an optional pydantic model @phbernardes. 108 | 109 | [1.5.1] 110 | 111 | - fix error when building with a parameter that is a pydantic model @phbernardes. 112 | - update typing and cast calls to use TYPE_CHECKING blocks. 113 | 114 | [1.5.0] 115 | 116 | - handle partial attributes factory for child factories solving issue #50 @phbernardes. 117 | 118 | [1.4.1] 119 | 120 | - fix sampling of `Literal` values. 121 | 122 | [1.4.0] 123 | 124 | - replace `exrex` with `xeger` due to licensing issues. 125 | 126 | [1.3.0] 127 | 128 | - a `PostGenerate` @blagasz. 129 | 130 | [1.2.9] 131 | 132 | - add `allow_population_by_field_name` flag @mrkovalchuk. 133 | - update to pydantic 1.9.1. 134 | 135 | [1.2.8] 136 | 137 | - update random seed to affect exrex @blagasz. 138 | 139 | [1.2.7] 140 | 141 | - fix checking of Union types in python 3.10 using pipe operator @DaanRademaker. 142 | 143 | [1.2.6] 144 | 145 | - fix handling of decimal mac length @DaanRademaker. 146 | 147 | [1.2.5] 148 | 149 | - fix handling of FKs for ormar extension. 150 | - fix handling of choice for ormar extension @mciszczon. 151 | 152 | [1.2.4] 153 | 154 | - update dependencies. 155 | 156 | [1.2.3] 157 | 158 | - fix regression due to lambda function argument. 159 | 160 | [1.2.2] 161 | 162 | - add `Any` to Providers. 163 | 164 | [1.2.1] 165 | 166 | - fix NameError that can occur when calling `update_forward_refs` without access to a localNS. 167 | 168 | [1.2.0] 169 | 170 | - add support for naive classes (including all builtin exceptions). 171 | - fix factory typing and resolve issue with TypeVars not being bounded, @lindycoder. 172 | - fix the `create_model_factory` method to use the current `cls` as the created factory's base by default. 173 | 174 | [1.1.0] 175 | 176 | - add support for constrained frozenset. 177 | - fix compatibilities issues with pydantic 1.8.2. 178 | 179 | [1.0.0] 180 | 181 | - update to support pydantic 1.9.0, including all new types. 182 | 183 | [0.8.0] 184 | 185 | - add random configuration. Thanks to @eviltnan. 186 | 187 | [0.7.0] 188 | 189 | - add support for `factory_use_construct` kwargs, thanks - @danielkatzan. 190 | 191 | [0.6.3] 192 | 193 | - fix backwards compatible import. 194 | 195 | [0.6.2] 196 | 197 | - fix bug with Literal[] values not being recognized. 198 | 199 | [0.6.1] 200 | 201 | - fix bug were nested optionals did not factor in `__allow_none_optionals__` settings. 202 | 203 | [0.6.0] 204 | 205 | - add `__allow_none_optionals__` factory class variable. 206 | - add a new method on `ModelFactory` called `should_set_none_value`, which dictates whether a None value should be set for a given `ModelField`. 207 | - update dependencies. 208 | - update the `ModelFactory.create_factory` method to accept an optional `base` kwarg user defined kwargs. 209 | 210 | [0.5.0] 211 | 212 | - add ormar extension. 213 | 214 | [0.4.6] 215 | 216 | - fix generation of nested constrained fields. 217 | 218 | [0.4.5] 219 | 220 | - fix generation of enum in complex types. 221 | 222 | [0.4.4] 223 | 224 | - update exports to be explicit. 225 | 226 | [0.4.3] 227 | 228 | - fix `py.typed` not placed inside the package. 229 | 230 | [0.4.2] 231 | 232 | - update handling of dataclasses to support randomized optionals. 233 | 234 | [0.4.1] 235 | 236 | - update random return None values for Optional[] marked fields. 237 | 238 | [0.4.0] 239 | 240 | - add support for dataclasses. 241 | 242 | [0.3.5b] 243 | 244 | - update fields. 245 | - update readme. 246 | 247 | [0.3.4b] 248 | 249 | - add Ignore and Require fields. 250 | - add support for ODMantic. 251 | - add support for forward refs. 252 | 253 | [0.3.3b] 254 | 255 | - fix TypeError being raised from issubclass() for python 3.9+. 256 | 257 | [0.3.2b] 258 | 259 | - add beanie extension. 260 | - removed support for python 3.6. 261 | 262 | [0.3.1b] 263 | 264 | - fix issues with decimal parsing. 265 | 266 | [0.3.0b] 267 | 268 | - initial MvP release. 269 | 270 | [0.2.0a] 271 | 272 | - implemented handling of all pydantic constraint fields. 273 | 274 | [0.1.0a] 275 | 276 | - core functionalities, including build and batch methods. 277 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This package is open to contributions big and small. 4 | 5 | To contribute, please follow these steps: 6 | 7 | 1. Fork the upstream repository and clone the fork locally. 8 | 2. Install [poetry](https://python-poetry.org/), and install the project's dependencies with `poetry install`. 9 | 3. Install [pre-commit](https://pre-commit.com/) by running `pre-commit install`. 10 | 4. Make whatever changes and additions you wish and commit these - please try to keep your commit history clean. 11 | 5. Note: 100% tests are mandatory. 12 | 6. Once you are ready, add a PR in the main repo. 13 | 7. Create a pull request to the main repository with an explanation of your changes. 14 | 15 | **NOTE**: The test suite requires having an instance of MongoDB available. You can launch one using the root level 16 | docker-compose config with `docker-compose up --detach`, or by any other means you deem. 17 | 18 | ## Launching the Docs 19 | 20 | To launch the docs locally use docker. First pull the image for the [mkdocs material theme](https://squidfunk.github.io/mkdocs-material/getting-started/) with: 21 | 22 | ```shell 23 | docker pull squidfunk/mkdocs-material 24 | ``` 25 | 26 | And then launch the docs with: 27 | 28 | ```shell 29 | docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material 30 | ``` 31 | 32 | ## Release workflow (Maintainers only) 33 | 34 | 1. Update changelog.md 35 | 2. Increment the version in pyproject.toml. 36 | 3. Commit and push. 37 | 4. In GitHub go to the releases tab 38 | 5. Pick "draft a new release" 39 | 6. Give it a title and a tag, both vX.X.X 40 | 7. Fill in the release description, you can let GitHub do it for you and then edit as needed. 41 | 8. Publish the release. 42 | 9. look under the action pane and make sure the release action runs correctly 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Na'aman Hirschfeld 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | Starlite Logo - Light 4 | Starlite Logo - Dark 5 |

6 | 7 | 8 | 9 |
10 | 11 | ![PyPI - License](https://img.shields.io/pypi/l/pydantic-factories?color=blue) 12 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pydantic-factories) 13 | 14 | [![Discord](https://img.shields.io/discord/919193495116337154?color=202235&label=%20Discord&logo=discord)](https://discord.gg/X3FJqy8d2j) [![Matrix](https://img.shields.io/badge/%5Bm%5D%20Matrix-bridged-blue?color=202235)](https://matrix.to/#/#starlitespace:matrix.org) [![Reddit](https://img.shields.io/reddit/subreddit-subscribers/starlite?label=r%2FStarlite&logo=reddit)](https://reddit.com/r/starlite) 15 | 16 |
17 | 18 | 19 | # ⚠️ 20 | 21 | # The next version of this library is released as [polyfactory](https://pypi.org/project/polyfactory/). Users are encouraged to migrate to it. 22 | 23 | # ⚠️ 24 | 25 | # Pydantic-Factories 26 | 27 | This library offers powerful mock data generation capabilities for [pydantic](https://github.com/samuelcolvin/pydantic) 28 | based models, `dataclasses` and `TypeDict`s. It can also be used with other libraries that use pydantic as a foundation. 29 | 30 | Check out [the documentation 📚](https://starlite-api.github.io/pydantic-factories/). 31 | 32 | ## Installation 33 | 34 | ```shell 35 | pip install pydantic-factories 36 | ``` 37 | 38 | ## Example 39 | 40 | ```python 41 | from datetime import date, datetime 42 | from typing import List, Union 43 | 44 | from pydantic import BaseModel, UUID4 45 | 46 | from pydantic_factories import ModelFactory 47 | 48 | 49 | class Person(BaseModel): 50 | id: UUID4 51 | name: str 52 | hobbies: List[str] 53 | age: Union[float, int] 54 | birthday: Union[datetime, date] 55 | 56 | 57 | class PersonFactory(ModelFactory): 58 | __model__ = Person 59 | 60 | 61 | result = PersonFactory.build() 62 | ``` 63 | 64 | That's it - with almost no work, we are able to create a mock data object fitting the `Person` class model definition. 65 | 66 | This is possible because of the typing information available on the pydantic model and model-fields, which are used as a 67 | source of truth for data generation. 68 | 69 | The factory parses the information stored in the pydantic model and generates a dictionary of kwargs that are passed to 70 | the `Person` class' init method. 71 | 72 | ## Features 73 | 74 | - ✅ supports both built-in and pydantic types 75 | - ✅ supports pydantic field constraints 76 | - ✅ supports complex field types 77 | - ✅ supports custom model fields 78 | - ✅ supports dataclasses 79 | - ✅ supports TypedDicts 80 | 81 | ## Why This Library? 82 | 83 | - 💯 powerful 84 | - 💯 extensible 85 | - 💯 simple 86 | - 💯 rigorously tested 87 | 88 | ## Contributing 89 | 90 | This library is open to contributions - in fact we welcome it. [Please see the contribution guide!](CONTRIBUTING.md) 91 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | docs: 4 | build: 5 | dockerfile: .docker/Dockerfile 6 | context: . 7 | ports: 8 | - 8000:8000 9 | volumes: 10 | - .:/docs 11 | command: 12 | - "serve" 13 | - "--dev-addr=0.0.0.0:8000" 14 | -------------------------------------------------------------------------------- /docs/images/2x/starlite-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litestar-org/pydantic-factories/052b7fd15c7c590a56aa189f6d392ff03f9951db/docs/images/2x/starlite-icon@2x.png -------------------------------------------------------------------------------- /docs/images/starlite-banner.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/images/starlite-favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litestar-org/pydantic-factories/052b7fd15c7c590a56aa189f6d392ff03f9951db/docs/images/starlite-favicon.ico -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | Starlite logo 3 | 4 | 5 | 6 |
7 | 8 | ![PyPI - License](https://img.shields.io/pypi/l/pydantic-factories?color=blue) 9 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pydantic-factories) 10 | 11 | [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/Goldziher/pydantic-factories.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Goldziher/pydantic-factories/context:python) 12 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/Goldziher/pydantic-factories.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Goldziher/pydantic-factories/alerts/) 13 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_pydantic-factories&metric=coverage)](https://sonarcloud.io/summary/new_code?id=starlite-api_pydantic-factories) 14 | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_pydantic-factories&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=starlite-api_pydantic-factories) 15 | [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_pydantic-factories&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=starlite-api_pydantic-factories) 16 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_pydantic-factories&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=starlite-api_pydantic-factories) 17 | 18 | [![Discord](https://img.shields.io/discord/919193495116337154?color=blue&label=chat%20on%20discord&logo=discord)](https://discord.gg/X3FJqy8d2j) 19 | 20 |
21 | 22 | 23 | # Pydantic-Factories 24 | 25 | This library offers powerful mock data generation capabilities for [pydantic](https://github.com/samuelcolvin/pydantic) 26 | based models and `dataclasses`. It can also be used with other libraries that use pydantic as a foundation. 27 | 28 | ## Example 29 | 30 | ```python 31 | from datetime import date, datetime 32 | from typing import List, Union 33 | 34 | from pydantic import BaseModel, UUID4 35 | 36 | from pydantic_factories import ModelFactory 37 | 38 | 39 | class Person(BaseModel): 40 | id: UUID4 41 | name: str 42 | hobbies: List[str] 43 | age: Union[float, int] 44 | birthday: Union[datetime, date] 45 | 46 | 47 | class PersonFactory(ModelFactory): 48 | __model__ = Person 49 | 50 | 51 | result = PersonFactory.build() 52 | ``` 53 | 54 | That's it - with almost no work, we are able to create a mock data object fitting the `Person` class model definition. 55 | 56 | This is possible because of the typing information available on the pydantic model and model-fields, which are used as a 57 | source of truth for data generation. 58 | 59 | The factory parses the information stored in the pydantic model and generates a dictionary of kwargs that are passed to 60 | the `Person` class' init method. 61 | 62 | ## Installation 63 | 64 | ```sh 65 | pip install pydantic-factories 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/usage/0-build-methods.md: -------------------------------------------------------------------------------- 1 | # Build Methods 2 | 3 | The `ModelFactory` class exposes two build methods: 4 | 5 | - `.build(**kwargs)` - builds a single instance of the factory's model 6 | - `.batch(size: int, **kwargs)` - build a list of size n instances 7 | 8 | ```python 9 | from pydantic import BaseModel 10 | 11 | from pydantic_factories import ModelFactory 12 | 13 | 14 | class Person(BaseModel): 15 | ... 16 | 17 | 18 | class PersonFactory(ModelFactory): 19 | __model__ = Person 20 | 21 | 22 | single_result = PersonFactory.build() # a single Person instance 23 | 24 | batch_result = PersonFactory.batch( 25 | size=5 26 | ) # list[Person, Person, Person, Person, Person] 27 | ``` 28 | 29 | Any `kwargs` you pass to `.build`, `.batch` or any of the [persistence methods](./5-persistence.md), will take precedence over 30 | whatever defaults are defined on the factory class itself. 31 | 32 | By default, when building a pydantic class, kwargs are validated, to avoid input validation you can use the `factory_use_construct` param. 33 | 34 | ```python 35 | from pydantic import BaseModel 36 | 37 | from pydantic_factories import ModelFactory 38 | 39 | 40 | class Person(BaseModel): 41 | ... 42 | 43 | 44 | class PersonFactory(ModelFactory): 45 | __model__ = Person 46 | 47 | 48 | PersonFactory.build(id=5) # Raises a validation error 49 | 50 | result = PersonFactory.build( 51 | factory_use_construct=True, id=5 52 | ) # Build a Person with invalid id 53 | ``` 54 | 55 | ## Partial Parameters 56 | 57 | Factories can randomly generate missing parameters for child factories. For example: 58 | 59 | ```python 60 | from pydantic_factories import ModelFactory 61 | from pydantic import BaseModel 62 | 63 | 64 | class Pet(BaseModel): 65 | name: str 66 | age: int 67 | 68 | 69 | class Person(BaseModel): 70 | name: str 71 | pets: list[Pet] 72 | age: int 73 | 74 | 75 | class PersonFactory(ModelFactory[Person]): 76 | __model__ = Person 77 | ``` 78 | 79 | When building a person without specifying the Person and pets ages, all these fields will be randomly generated: 80 | 81 | ```python 82 | from pydantic_factories import ModelFactory 83 | from pydantic import BaseModel 84 | 85 | 86 | class Pet(BaseModel): 87 | name: str 88 | age: int 89 | 90 | 91 | class Person(BaseModel): 92 | name: str 93 | pets: list[Pet] 94 | age: int 95 | 96 | 97 | class PersonFactory(ModelFactory[Person]): 98 | __model__ = Person 99 | 100 | 101 | data = { 102 | "name": "John", 103 | "pets": [ 104 | {"name": "dog"}, 105 | {"name": "cat"}, 106 | ], 107 | } 108 | 109 | person = PersonFactory.build(**data) 110 | 111 | print(person.json(indent=2)) 112 | ``` 113 | 114 | ```json 115 | { 116 | "name": "John", 117 | "pets": [ 118 | { 119 | "name": "dog", 120 | "age": 9005 121 | }, 122 | { 123 | "name": "cat", 124 | "age": 2455 125 | } 126 | ], 127 | "age": 975 128 | } 129 | ``` 130 | -------------------------------------------------------------------------------- /docs/usage/1-nest-models.md: -------------------------------------------------------------------------------- 1 | # Nested Models 2 | 3 | The automatic generation of mock data works for all types supported by pydantic, as well as nested classes that derive 4 | from `BaseModel` (including for 3rd party libraries) and complex types. Let's look at another example: 5 | 6 | ```python 7 | from datetime import date, datetime 8 | from enum import Enum 9 | from pydantic import BaseModel, UUID4 10 | from typing import Any, Dict, List, Union 11 | 12 | from pydantic_factories import ModelFactory 13 | 14 | 15 | class Species(str, Enum): 16 | CAT = "Cat" 17 | DOG = "Dog" 18 | PIG = "Pig" 19 | MONKEY = "Monkey" 20 | 21 | 22 | class Pet(BaseModel): 23 | name: str 24 | sound: str 25 | species: Species 26 | 27 | 28 | class Person(BaseModel): 29 | id: UUID4 30 | name: str 31 | hobbies: List[str] 32 | age: Union[float, int] 33 | birthday: Union[datetime, date] 34 | pets: List[Pet] 35 | assets: List[Dict[str, Dict[str, Any]]] 36 | 37 | 38 | class PersonFactory(ModelFactory): 39 | __model__ = Person 40 | 41 | 42 | result = PersonFactory.build() 43 | ``` 44 | 45 | This example will also work out of the box although no factory was defined for the Pet class, that's not a problem - a 46 | factory will be dynamically generated for it on the fly. 47 | 48 | The complex typing under the `assets` attribute is a bit more tricky, but the factory will generate a python object 49 | fitting this signature, therefore passing validation. 50 | 51 | **Please note**: the one thing factories cannot handle is self referencing models, because this can lead to recursion 52 | errors. In this case you will need to handle the particular field by setting defaults for it. 53 | -------------------------------------------------------------------------------- /docs/usage/2-dataclasses.md: -------------------------------------------------------------------------------- 1 | # Supported Models 2 | 3 | This library works with any class that inherits the pydantic `BaseModel` class, including `GenericModel` and classes 4 | from 3rd party libraries, and also with dataclasses - both those from the python standard library and pydantic's 5 | dataclasses. Finally, it also supports `TypedDict` classes. In fact, you can use them interchangeably as you like: 6 | 7 | ```python 8 | import dataclasses 9 | from typing import Dict, List 10 | 11 | import pydantic 12 | from pydantic_factories import ModelFactory 13 | 14 | 15 | @pydantic.dataclasses.dataclass 16 | class MyPydanticDataClass: 17 | name: str 18 | 19 | 20 | class MyFirstModel(pydantic.BaseModel): 21 | dataclass: MyPydanticDataClass 22 | 23 | 24 | @dataclasses.dataclass() 25 | class MyPythonDataClass: 26 | id: str 27 | complex_type: Dict[str, Dict[int, List[MyFirstModel]]] 28 | 29 | 30 | class MySecondModel(pydantic.BaseModel): 31 | dataclasses: List[MyPythonDataClass] 32 | 33 | 34 | class MyFactory(ModelFactory): 35 | __model__ = MySecondModel 36 | 37 | 38 | result = MyFactory.build() 39 | ``` 40 | 41 | The above example will build correctly. 42 | 43 | ## Note Regarding Nested Optional Types in Dataclasses 44 | 45 | When generating mock values for fields typed as `Optional`, if the factory is defined 46 | with `__allow_none_optionals__ = True`, the field value will be either a value or None - depending on a random decision. 47 | This works even when the `Optional` typing is deeply nested, except for dataclasses - typing is only shallowly evaluated 48 | for dataclasses, and as such they are always assumed to require a value. If you wish to have a None value, in this 49 | particular case, you should do so manually by configured a `Use` callback for the particular field. 50 | -------------------------------------------------------------------------------- /docs/usage/3-configuration.md: -------------------------------------------------------------------------------- 1 | # Factory Configuration 2 | 3 | Configuration of `ModelFactory` is done using class variables: 4 | 5 | - **\_\_model\_\_**: a _required_ variable specifying the model for the factory. It accepts any class that extends _ 6 | pydantic's_ `BaseModel` including classes from other libraries. If this variable is not set, 7 | a `ConfigurationException` will be raised. 8 | 9 | - **\_\_faker\_\_**: an _optional_ variable specifying a user configured instance of faker. If this variable is not set, 10 | the factory will default to using vanilla `faker`. 11 | 12 | - **\_\_sync_persistence\_\_**: an _optional_ variable specifying the handler for synchronously persisting data. If this 13 | is variable is not set, the `.create_sync` and `.create_batch_sync` methods of the factory cannot be used. 14 | See: [persistence methods](./5-persistence.md) 15 | 16 | - **\_\_async_persistence\_\_**: an _optional_ variable specifying the handler for asynchronously persisting data. If 17 | this is variable is not set, the `.create_async` and `.create_batch_async` methods of the factory cannot be used. 18 | See: [persistence methods](./5-persistence.md) 19 | 20 | - **\_\_allow_none_optionals\_\_**: an _optional_ variable specifying whether the factory should randomly set None 21 | values for optional fields, or always set a value for them. This is `True` by default. 22 | 23 | ```python 24 | from faker import Faker 25 | from pydantic_factories import ModelFactory 26 | 27 | from app.models import Person 28 | from .persistence import AsyncPersistenceHandler, SyncPersistenceHandler 29 | 30 | Faker.seed(5) 31 | my_faker = Faker("en-EN") 32 | 33 | 34 | class PersonFactory(ModelFactory): 35 | __model__ = Person 36 | __faker__ = my_faker 37 | __sync_persistence__ = SyncPersistenceHandler 38 | __async_persistence__ = AsyncPersistenceHandler 39 | __allow_none_optionals__ = False 40 | ... 41 | ``` 42 | 43 | ## Generating deterministic objects 44 | 45 | In order to generate deterministic data, use `ModelFactory.seed_random` method. This will pass the seed value to both 46 | Faker and random method calls, guaranteeing data to be the same in between the calls. Especially useful for testing. 47 | -------------------------------------------------------------------------------- /docs/usage/5-persistence.md: -------------------------------------------------------------------------------- 1 | # Persistence 2 | 3 | `ModelFactory` has four persistence methods: 4 | 5 | - `.create_sync(**kwargs)` - builds and persists a single instance of the factory's model synchronously 6 | - `.create_batch_sync(size: int, **kwargs)` - builds and persists a list of size n instances synchronously 7 | - `.create_async(**kwargs)` - builds and persists a single instance of the factory's model asynchronously 8 | - `.create_batch_async(size: int, **kwargs)` - builds and persists a list of size n instances asynchronously 9 | 10 | To use these methods, you must first specify a sync and/or async persistence handlers for the factory: 11 | 12 | ```python 13 | from pydantic_factories import ModelFactory 14 | from typing import TypeVar, List 15 | 16 | from pydantic import BaseModel 17 | from pydantic_factories import SyncPersistenceProtocol, AsyncPersistenceProtocol 18 | 19 | T = TypeVar("T", bound=BaseModel) 20 | 21 | 22 | class SyncPersistenceHandler(SyncPersistenceProtocol[T]): 23 | def save(self, data: T) -> T: 24 | ... # do stuff 25 | 26 | def save_many(self, data: List[T]) -> List[T]: 27 | ... # do stuff 28 | 29 | 30 | class AsyncPersistenceHandler(AsyncPersistenceProtocol[T]): 31 | async def save(self, data: T) -> T: 32 | ... # do stuff 33 | 34 | async def save_many(self, data: List[T]) -> List[T]: 35 | ... # do stuff 36 | 37 | 38 | class PersonFactory(ModelFactory): 39 | __sync_persistence__ = SyncPersistenceHandler 40 | __async_persistence__ = AsyncPersistenceHandler 41 | ... 42 | ``` 43 | 44 | Or create your own base factory and reuse it in your various factories: 45 | 46 | ```python 47 | from pydantic_factories import ModelFactory 48 | from typing import TypeVar, List 49 | 50 | from pydantic import BaseModel 51 | from pydantic_factories import SyncPersistenceProtocol, AsyncPersistenceProtocol 52 | 53 | T = TypeVar("T", bound=BaseModel) 54 | 55 | 56 | class SyncPersistenceHandler(SyncPersistenceProtocol[T]): 57 | def save(self, data: T) -> T: 58 | ... # do stuff 59 | 60 | def save_many(self, data: List[T]) -> List[T]: 61 | ... # do stuff 62 | 63 | 64 | class AsyncPersistenceHandler(AsyncPersistenceProtocol[T]): 65 | async def save(self, data: T) -> T: 66 | ... # do stuff 67 | 68 | async def save_many(self, data: List[T]) -> List[T]: 69 | ... # do stuff 70 | 71 | 72 | class BaseModelFactory(ModelFactory): 73 | __sync_persistence__ = SyncPersistenceHandler 74 | __async_persistence__ = AsyncPersistenceHandler 75 | 76 | 77 | class PersonFactory(BaseModelFactory): 78 | ... 79 | ``` 80 | 81 | With the persistence handlers in place, you can now use all persistence methods. Please note - you do not need to define 82 | any or both persistence handlers. If you will only use sync or async persistence, you only need to define the respective 83 | handler to use these methods. 84 | 85 | ## Create Factory Method 86 | 87 | If you prefer to create a factory imperatively, you can do so using the `ModelFactory.create_factory` method. This method receives the following arguments: 88 | 89 | - model - the model for the factory. 90 | - base - an optional base factory class. Defaults to the factory class on which the method is called. 91 | - kwargs - a dictionary of arguments correlating to the class vars accepted by ModelFactory, e.g. **faker**. 92 | 93 | You could also override the child factory's `__model__` attribute to specify the model to use and the default kwargs as shown as the BuildPet class as shown below: 94 | 95 | ```python 96 | from datetime import date, datetime 97 | from enum import Enum 98 | from pydantic import BaseModel, UUID4 99 | from typing import Any, Dict, List, TypeVar, Union, Generic, Optional 100 | from pydantic_factories import ModelFactory 101 | 102 | 103 | class Species(str, Enum): 104 | CAT = "Cat" 105 | DOG = "Dog" 106 | 107 | 108 | class PetBase(BaseModel): 109 | name: str 110 | species: Species 111 | 112 | 113 | class Pet(PetBase): 114 | id: UUID4 115 | 116 | 117 | class PetCreate(PetBase): 118 | pass 119 | 120 | 121 | class PetUpdate(PetBase): 122 | pass 123 | 124 | 125 | class PersonBase(BaseModel): 126 | name: str 127 | hobbies: List[str] 128 | age: Union[float, int] 129 | birthday: Union[datetime, date] 130 | pets: List[Pet] 131 | assets: List[Dict[str, Dict[str, Any]]] 132 | 133 | 134 | class PersonCreate(PersonBase): 135 | pass 136 | 137 | 138 | class Person(PersonBase): 139 | id: UUID4 140 | 141 | 142 | class PersonUpdate(PersonBase): 143 | pass 144 | 145 | 146 | def test_factory(): 147 | class PersonFactory(ModelFactory): 148 | __model__ = Person 149 | 150 | person = PersonFactory.build() 151 | 152 | assert person.pets != [] 153 | 154 | 155 | ModelType = TypeVar("ModelType", bound=BaseModel) 156 | CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) 157 | UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) 158 | 159 | 160 | class BUILDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): 161 | def __init__( 162 | self, 163 | model: ModelType = None, 164 | create_schema: Optional[CreateSchemaType] = None, 165 | update_schema: Optional[UpdateSchemaType] = None, 166 | ): 167 | self.model = model 168 | self.create_model = create_schema 169 | self.update_model = update_schema 170 | 171 | def build_object(self) -> ModelType: 172 | object_Factory = ModelFactory.create_factory(self.model) 173 | return object_Factory.build() 174 | 175 | def build_create_object(self) -> CreateSchemaType: 176 | object_Factory = ModelFactory.create_factory(self.create_model) 177 | return object_Factory.build() 178 | 179 | def build_update_object(self) -> UpdateSchemaType: 180 | object_Factory = ModelFactory.create_factory(self.update_model) 181 | return object_Factory.build() 182 | 183 | 184 | class BUILDPet(BUILDBase[Pet, PetCreate, PetUpdate]): 185 | def build_object(self) -> Pet: 186 | object_Factory = ModelFactory.create_factory(self.model, name="Fido") 187 | return object_Factory.build() 188 | 189 | def build_create_object(self) -> PetCreate: 190 | object_Factory = ModelFactory.create_factory(self.create_model, name="Rover") 191 | return object_Factory.build() 192 | 193 | def build_update_object(self) -> PetUpdate: 194 | object_Factory = ModelFactory.create_factory(self.update_model, name="Spot") 195 | return object_Factory.build() 196 | 197 | 198 | def test_factory_create(): 199 | person_factory = BUILDBase(Person, PersonCreate, PersonUpdate) 200 | 201 | pet_factory = BUILDPet(Pet, PetCreate, PetUpdate) 202 | 203 | create_person = person_factory.build_create_object() 204 | update_person = person_factory.build_update_object() 205 | 206 | pet = pet_factory.build_object() 207 | create_pet = pet_factory.build_create_object() 208 | update_pet = pet_factory.build_update_object() 209 | 210 | assert create_person is not None 211 | assert update_person is not None 212 | 213 | assert pet.name == "Fido" 214 | assert create_pet.name == "Rover" 215 | assert update_pet.name == "Spot" 216 | ``` 217 | -------------------------------------------------------------------------------- /docs/usage/6-extensions.md: -------------------------------------------------------------------------------- 1 | # Extensions 2 | 3 | Any class that is derived from pydantic's `BaseModel` can be used as the `__model__` of a factory. For most 3rd party 4 | libraries, e.g. [SQLModel](https://sqlmodel.tiangolo.com/), this library will work as is out of the box. 5 | 6 | Currently, this library also includes the following extensions: 7 | 8 | ## ODMantic 9 | 10 | This extension includes a class called `OdmanticModelFactory` and it can be imported from `pydantic_factory.extensions`. 11 | This class is meant to be used with the `Model` and `EmbeddedModel` classes exported by ODMantic, but it will also work 12 | with regular instances of pydantic's `BaseModel`. 13 | 14 | ## Beanie 15 | 16 | This extension includes a class called `BeanieDocumentFactory` as well as an `BeaniePersistenceHandler`. Both of these 17 | can be imported from `pydantic_factory.extensions`. The `BeanieDocumentFactory` is meant to be used with the 18 | Beanie `Document` class, and it includes async persistence build in. 19 | 20 | ## Ormar 21 | 22 | This extension includes a class called `OrmarModelFactory`. This class is meant to be used with the `Model` class 23 | exported by ormar. 24 | -------------------------------------------------------------------------------- /docs/usage/7-handling-custom-types.md: -------------------------------------------------------------------------------- 1 | # Handling Custom Types 2 | 3 | If your model has an attribute that is not supported by `pydantic-factories` and 4 | it depends on third party libraries, you can create your custom extension 5 | subclassing the `ModelFactory`, and overriding the `get_mock_value` method to 6 | add your logic. 7 | 8 | ```python 9 | from typing import Any 10 | from pydantic_factories import ModelFactory 11 | 12 | 13 | class CustomFactory(ModelFactory[Any]): 14 | """Tweak the ModelFactory to add our custom mocks.""" 15 | 16 | @classmethod 17 | def get_mock_value(cls, field_type: Any) -> Any: 18 | """Add our custom mock value.""" 19 | if str(field_type) == "my_super_rare_datetime_field": 20 | return cls.get_faker().date_time_between() 21 | 22 | return super().get_mock_value(field_type) 23 | ``` 24 | 25 | Where `cls.get_faker()` is a `faker` instance that you can use to build your 26 | returned value. 27 | -------------------------------------------------------------------------------- /docs/usage/8-pytest-fixtures.md: -------------------------------------------------------------------------------- 1 | # Using Factories as Fixtures 2 | 3 | Any class from `ModelFactory` can use the decorator to register as a fixture easily. 4 | 5 | The model factory will be registered as a fixture with the name in snake case. 6 | 7 | e.g. `PersonFactory` -> `person_factory` 8 | 9 | The decorator also provides some pytest-like arguments to define the fixture. (`scope`, `autouse`, `name`) 10 | 11 | ```py 12 | from datetime import date, datetime 13 | from typing import List, Union 14 | 15 | from pydantic import UUID4, BaseModel 16 | 17 | from pydantic_factories import ModelFactory 18 | from pydantic_factories.plugins.pytest_plugin import register_fixture 19 | 20 | 21 | class Person(BaseModel): 22 | id: UUID4 23 | name: str 24 | hobbies: List[str] 25 | age: Union[float, int] 26 | birthday: Union[datetime, date] 27 | 28 | 29 | @register_fixture 30 | class PersonFactory(ModelFactory): 31 | """A person factory""" 32 | 33 | __model__ = Person 34 | 35 | 36 | @register_fixture(scope="session", autouse=True, name="cool_guy_factory") 37 | class AnotherPersonFactory(ModelFactory): 38 | """A cool guy factory""" 39 | 40 | __model__ = Person 41 | 42 | 43 | def test_person_factory(person_factory: PersonFactory) -> None: 44 | person = person_factory.build() 45 | assert isinstance(person, Person) 46 | 47 | 48 | def test_cool_guy_factory(cool_guy_factory: AnotherPersonFactory) -> None: 49 | cool_guy = cool_guy_factory.build() 50 | assert isinstance(cool_guy, Person) 51 | ``` 52 | 53 | Use `pytest --fixtures` will show output along these lines: 54 | 55 | ```sh 56 | ------------- fixtures defined from pydantic_factories.plugins.pytest_plugin ------------- 57 | cool_guy_factory [session scope] -- pydantic_factories/plugins/pytest_plugin.py:48 58 | A cool guy factory 59 | 60 | person_factory -- pydantic_factories/plugins/pytest_plugin.py:48 61 | A person factory 62 | 63 | ``` 64 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Pydantic Factories 2 | repo_url: https://github.com/starlite-api/pydantic-factories 3 | repo_name: starlite-api/pydantic-factories 4 | site_url: https://starlite-api.github.io/pydantic-factories 5 | theme: 6 | name: material 7 | palette: 8 | - media: "(prefers-color-scheme: dark)" 9 | scheme: mirage 10 | toggle: 11 | icon: material/toggle-switch 12 | name: Switch to light mode 13 | - media: "(prefers-color-scheme: light)" 14 | scheme: mirage-light 15 | toggle: 16 | icon: material/toggle-switch-off-outline 17 | name: Switch to dark mode 18 | favicon: images/starlite-favicon.ico 19 | logo: images/2x/starlite-icon@2x.png 20 | icon: 21 | repo: fontawesome/brands/github 22 | features: 23 | - navigation.instant 24 | - navigation.tracking 25 | - navigation.tabs 26 | - navigation.tabs.sticky 27 | - toc.integrate 28 | - search.suggest 29 | - search.highlight 30 | - search.share 31 | plugins: 32 | - search: 33 | lang: en 34 | - social: 35 | cards_color: 36 | fill: "#1d2433" 37 | text: "#d6dbe1" 38 | cards_font: Tahoma 39 | extra_css: 40 | - css/extra.css 41 | nav: 42 | - index.md 43 | - Usage: 44 | - usage/0-build-methods.md 45 | - usage/1-nest-models.md 46 | - usage/2-dataclasses.md 47 | - usage/3-configuration.md 48 | - usage/4-defining-factory-fields.md 49 | - usage/5-persistence.md 50 | - usage/6-extensions.md 51 | - usage/7-handling-custom-types.md 52 | - usage/8-pytest-fixtures.md 53 | extra: 54 | social: 55 | - icon: fontawesome/brands/discord 56 | link: https://discord.gg/X3FJqy8d2j 57 | - icon: fontawesome/brands/github 58 | link: https://github.com/starlite-api/pydantic-factories 59 | markdown_extensions: 60 | - admonition 61 | - attr_list 62 | - md_in_html 63 | - pymdownx.highlight: 64 | anchor_linenums: true 65 | - pymdownx.inlinehilite 66 | - pymdownx.snippets 67 | - pymdownx.details 68 | - pymdownx.superfences 69 | plugins: 70 | - search: 71 | lang: en 72 | watch: 73 | - pydantic_factories 74 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | plugins = pydantic.mypy 3 | 4 | warn_unused_ignores = True 5 | warn_redundant_casts = True 6 | warn_unused_configs = True 7 | warn_unreachable = True 8 | warn_return_any = True 9 | strict = True 10 | disallow_untyped_decorators = True 11 | disallow_any_generics = False 12 | implicit_reexport = False 13 | show_error_codes = True 14 | 15 | [pydantic-mypy] 16 | init_forbid_extra = True 17 | init_typed = True 18 | warn_required_dynamic_aliases = True 19 | warn_untyped_fields = True 20 | 21 | [mypy-tests.typing_test_strict] 22 | disallow_any_generics = True 23 | -------------------------------------------------------------------------------- /pydantic_factories/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .exceptions import ConfigurationError 3 | from .extensions import ( 4 | BeanieDocumentFactory, 5 | BeaniePersistenceHandler, 6 | OdmanticModelFactory, 7 | OrmarModelFactory, 8 | ) 9 | from .factory import ModelFactory 10 | from .fields import Ignore, PostGenerated, Require, Use 11 | from .protocols import AsyncPersistenceProtocol, SyncPersistenceProtocol 12 | 13 | __all__ = [ 14 | "AsyncPersistenceProtocol", 15 | "BeanieDocumentFactory", 16 | "BeaniePersistenceHandler", 17 | "ConfigurationError", 18 | "Ignore", 19 | "ModelFactory", 20 | "OdmanticModelFactory", 21 | "OrmarModelFactory", 22 | "Require", 23 | "SyncPersistenceProtocol", 24 | "Use", 25 | "PostGenerated", 26 | ] 27 | -------------------------------------------------------------------------------- /pydantic_factories/constraints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litestar-org/pydantic-factories/052b7fd15c7c590a56aa189f6d392ff03f9951db/pydantic_factories/constraints/__init__.py -------------------------------------------------------------------------------- /pydantic_factories/constraints/collection.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import TYPE_CHECKING, Any, Type, Union, cast 3 | 4 | from pydantic_factories.exceptions import ParameterError 5 | from pydantic_factories.utils import unwrap_new_type_if_needed 6 | from pydantic_factories.value_generators.complex_types import handle_complex_type 7 | 8 | if TYPE_CHECKING: # pragma: no cover 9 | from pydantic import ConstrainedList, ConstrainedSet 10 | from pydantic.fields import ModelField 11 | 12 | from pydantic_factories.factory import ModelFactory 13 | 14 | 15 | def handle_constrained_collection( 16 | collection_type: Union[Type[list], Type[set]], 17 | model_factory: Type["ModelFactory"], 18 | model_field: "ModelField", 19 | ) -> Union[list, set]: 20 | """Generate a constrained list or set.""" 21 | constrained_field = cast( 22 | "Union[ConstrainedList, ConstrainedSet]", unwrap_new_type_if_needed(model_field.outer_type_) 23 | ) # pragma: no cover 24 | min_items = constrained_field.min_items or 0 25 | max_items = constrained_field.max_items if constrained_field.max_items is not None else min_items + 1 26 | unique_items = getattr(constrained_field, "unique_items", False) 27 | 28 | if max_items < min_items: 29 | raise ParameterError("max_items must be longer or equal to min_items") 30 | 31 | if model_field.sub_fields: 32 | handler = lambda: handle_complex_type( # noqa: E731 33 | model_field=random.choice(model_field.sub_fields), model_factory=model_factory # pyright: ignore 34 | ) 35 | else: 36 | t_type = constrained_field.item_type if constrained_field.item_type is not Any else str 37 | handler = lambda: model_factory.get_mock_value(t_type) # noqa: E731 38 | 39 | collection: Union[list, set] = collection_type() 40 | try: 41 | while len(collection) < random.randint(min_items, max_items): 42 | value = handler() # type: ignore 43 | if isinstance(collection, set): 44 | collection.add(value) 45 | else: 46 | if unique_items and value in collection: 47 | continue 48 | collection.append(value) 49 | return collection 50 | except TypeError as e: 51 | raise ParameterError(f"cannot generate a constrained collection of type: {constrained_field.item_type}") from e 52 | -------------------------------------------------------------------------------- /pydantic_factories/constraints/date.py: -------------------------------------------------------------------------------- 1 | from datetime import date, timedelta 2 | from typing import TYPE_CHECKING, Type, cast 3 | 4 | if TYPE_CHECKING: 5 | from faker import Faker 6 | from pydantic import ConstrainedDate 7 | 8 | 9 | def handle_constrained_date(constrained_date: Type["ConstrainedDate"], faker: "Faker") -> date: 10 | """ 11 | Generates a date value fulfilling the expected constraints. 12 | Args: 13 | constrained_date: 14 | faker: 15 | 16 | Returns: 17 | 18 | """ 19 | start_date = date.today() - timedelta(days=100) 20 | if constrained_date.ge: 21 | start_date = constrained_date.ge 22 | elif constrained_date.gt: 23 | start_date = constrained_date.gt + timedelta(days=1) 24 | 25 | end_date = date.today() + timedelta(days=100) 26 | if constrained_date.le: 27 | end_date = constrained_date.le 28 | elif constrained_date.lt: 29 | end_date = constrained_date.lt - timedelta(days=1) 30 | 31 | return cast("date", faker.date_between(start_date=start_date, end_date=end_date)) 32 | -------------------------------------------------------------------------------- /pydantic_factories/constraints/decimal.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from typing import TYPE_CHECKING, Optional, cast 3 | 4 | from pydantic_factories.exceptions import ParameterError 5 | from pydantic_factories.value_generators.constrained_number import ( 6 | generate_constrained_number, 7 | get_constrained_number_range, 8 | ) 9 | from pydantic_factories.value_generators.primitives import create_random_decimal 10 | 11 | if TYPE_CHECKING: 12 | from pydantic import ConstrainedDecimal 13 | 14 | 15 | def validate_max_digits( 16 | max_digits: int, 17 | minimum: Optional[Decimal], 18 | decimal_places: Optional[int], 19 | ) -> None: 20 | """Validates that max digits is greater than minimum and decimal places. 21 | 22 | Args: 23 | max_digits: The maximal number of digits for the decimal. 24 | minimum: Minimal value. 25 | decimal_places: Number of decimal places 26 | 27 | Returns: 28 | 'None' 29 | """ 30 | if max_digits <= 0: 31 | raise ParameterError("max_digits must be greater than 0") 32 | 33 | if minimum is not None: 34 | min_str = str(minimum).split(".")[1] if "." in str(minimum) else str(minimum) 35 | 36 | if max_digits <= len(min_str): 37 | raise ParameterError("minimum is greater than max_digits") 38 | 39 | if decimal_places is not None and max_digits <= decimal_places: 40 | raise ParameterError("max_digits must be greater than decimal places") 41 | 42 | 43 | def handle_decimal_length( 44 | generated_decimal: Decimal, 45 | decimal_places: Optional[int], 46 | max_digits: Optional[int], 47 | ) -> Decimal: 48 | """Handles the length of the decimal.""" 49 | string_number = str(generated_decimal) 50 | sign = "-" if "-" in string_number else "+" 51 | string_number = string_number.replace("-", "") 52 | whole_numbers, decimals = string_number.split(".") 53 | 54 | if max_digits is not None and decimal_places is not None: 55 | if len(whole_numbers) + decimal_places > max_digits: 56 | # max digits determines decimal length 57 | max_decimals = max_digits - len(whole_numbers) 58 | else: 59 | # decimal places determines max decimal length 60 | max_decimals = decimal_places 61 | elif max_digits is not None: 62 | max_decimals = max_digits - len(whole_numbers) 63 | else: 64 | max_decimals = cast("int", decimal_places) 65 | 66 | if max_decimals < 0: 67 | # in this case there are fewer digits than the len of whole_numbers 68 | return Decimal(sign + whole_numbers[:max_decimals]) 69 | 70 | decimals = decimals[:max_decimals] 71 | return Decimal(sign + whole_numbers + "." + decimals[:decimal_places]) 72 | 73 | 74 | def handle_constrained_decimal(field: "ConstrainedDecimal") -> Decimal: 75 | """Handles 'ConstrainedDecimal' instances.""" 76 | multiple_of = cast("Optional[Decimal]", field.multiple_of) 77 | decimal_places = field.decimal_places 78 | max_digits = field.max_digits 79 | 80 | minimum, maximum = get_constrained_number_range( 81 | gt=field.gt, ge=field.ge, lt=field.lt, le=field.le, multiple_of=multiple_of, t_type=Decimal # type: ignore 82 | ) 83 | 84 | if max_digits is not None: 85 | validate_max_digits(max_digits=max_digits, minimum=cast("Decimal", minimum), decimal_places=decimal_places) 86 | 87 | generated_decimal = generate_constrained_number( 88 | minimum=cast("Decimal", minimum), 89 | maximum=cast("Decimal", maximum), 90 | multiple_of=multiple_of, 91 | method=create_random_decimal, 92 | ) 93 | 94 | if max_digits is not None or decimal_places is not None: 95 | return handle_decimal_length( 96 | generated_decimal=generated_decimal, max_digits=max_digits, decimal_places=decimal_places 97 | ) 98 | 99 | return generated_decimal 100 | -------------------------------------------------------------------------------- /pydantic_factories/constraints/float.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from pydantic_factories.value_generators.constrained_number import ( 4 | generate_constrained_number, 5 | get_constrained_number_range, 6 | ) 7 | from pydantic_factories.value_generators.primitives import create_random_float 8 | 9 | if TYPE_CHECKING: 10 | from pydantic import ConstrainedFloat 11 | 12 | 13 | def handle_constrained_float(field: "ConstrainedFloat") -> float: 14 | """Handles 'ConstrainedFloat' instances.""" 15 | multiple_of = field.multiple_of 16 | 17 | minimum, maximum = get_constrained_number_range( 18 | gt=field.gt, ge=field.ge, lt=field.lt, le=field.le, t_type=float, multiple_of=multiple_of 19 | ) 20 | 21 | return generate_constrained_number( 22 | minimum=minimum, 23 | maximum=maximum, 24 | multiple_of=multiple_of, 25 | method=create_random_float, 26 | ) 27 | -------------------------------------------------------------------------------- /pydantic_factories/constraints/integer.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from pydantic_factories.value_generators.constrained_number import ( 4 | generate_constrained_number, 5 | get_constrained_number_range, 6 | ) 7 | from pydantic_factories.value_generators.primitives import create_random_integer 8 | 9 | if TYPE_CHECKING: 10 | from pydantic import ConstrainedInt 11 | 12 | 13 | def handle_constrained_int(field: "ConstrainedInt") -> int: 14 | """Handles 'ConstrainedInt' instances.""" 15 | multiple_of = field.multiple_of 16 | 17 | minimum, maximum = get_constrained_number_range( 18 | gt=field.gt, ge=field.ge, lt=field.lt, le=field.le, t_type=int, multiple_of=multiple_of 19 | ) 20 | return generate_constrained_number( 21 | minimum=minimum, maximum=maximum, multiple_of=multiple_of, method=create_random_integer 22 | ) 23 | -------------------------------------------------------------------------------- /pydantic_factories/constraints/strings.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, Optional, Pattern, Tuple, Union 2 | 3 | from pydantic_factories.exceptions import ParameterError 4 | from pydantic_factories.value_generators.primitives import ( 5 | create_random_bytes, 6 | create_random_string, 7 | ) 8 | from pydantic_factories.value_generators.regex import RegexFactory 9 | 10 | if TYPE_CHECKING: 11 | from pydantic import ConstrainedBytes, ConstrainedStr 12 | 13 | 14 | def parse_constrained_string_or_bytes( 15 | field: Union["ConstrainedStr", "ConstrainedBytes"] 16 | ) -> Tuple[Optional[int], Optional[int], bool]: 17 | """Parses and validates the given field.""" 18 | lower_case = field.to_lower 19 | min_length = field.min_length 20 | max_length = field.max_length 21 | 22 | if min_length is not None and min_length < 0: 23 | raise ParameterError("min_length must be greater or equal to 0") 24 | 25 | if max_length is not None and max_length < 0: 26 | raise ParameterError("max_length must be greater or equal to 0") 27 | 28 | if max_length is not None and min_length is not None and max_length < min_length: 29 | raise ParameterError("max_length must be greater than min_length") 30 | 31 | return min_length, max_length, lower_case 32 | 33 | 34 | def handle_constrained_bytes(field: "ConstrainedBytes") -> bytes: 35 | """Handles ConstrainedStr and Fields with string constraints.""" 36 | min_length, max_length, lower_case = parse_constrained_string_or_bytes(field) 37 | if max_length == 0: 38 | return b"" 39 | return create_random_bytes(min_length=min_length, max_length=max_length, lower_case=lower_case) 40 | 41 | 42 | def handle_constrained_string(field: "ConstrainedStr", random_seed: Optional[int]) -> str: 43 | """Handles ConstrainedStr and Fields with string constraints.""" 44 | regex_factory = RegexFactory(seed=random_seed) 45 | min_length, max_length, lower_case = parse_constrained_string_or_bytes(field) 46 | 47 | if max_length == 0: 48 | return "" 49 | 50 | regex: Any = field.regex 51 | if not regex: 52 | return create_random_string(min_length, max_length, lower_case=lower_case) 53 | 54 | if isinstance(regex, Pattern): 55 | regex = regex.pattern 56 | 57 | result = regex_factory(regex) 58 | if min_length: 59 | while len(result) < min_length: 60 | result += regex_factory(regex) 61 | 62 | if max_length and len(result) > max_length: 63 | result = result[:max_length] 64 | 65 | return result.lower() if lower_case else result 66 | -------------------------------------------------------------------------------- /pydantic_factories/exceptions.py: -------------------------------------------------------------------------------- 1 | class ModelFactoryError(Exception): 2 | pass 3 | 4 | 5 | class ConfigurationError(ModelFactoryError): 6 | pass 7 | 8 | 9 | class ParameterError(ModelFactoryError): 10 | pass 11 | 12 | 13 | class MissingBuildKwargError(ModelFactoryError): 14 | pass 15 | 16 | 17 | class MissingExtensionDependency(ModelFactoryError): 18 | pass 19 | -------------------------------------------------------------------------------- /pydantic_factories/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | from .beanie_odm import BeanieDocumentFactory, BeaniePersistenceHandler 2 | from .odmantic_odm import OdmanticModelFactory 3 | from .ormar_orm import OrmarModelFactory 4 | 5 | __all__ = ["BeanieDocumentFactory", "BeaniePersistenceHandler", "OdmanticModelFactory", "OrmarModelFactory"] 6 | -------------------------------------------------------------------------------- /pydantic_factories/extensions/beanie_odm.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, List, Union 2 | 3 | from pydantic import BaseModel 4 | 5 | from pydantic_factories.factory import ModelFactory 6 | from pydantic_factories.protocols import AsyncPersistenceProtocol 7 | 8 | try: 9 | from beanie import Document 10 | except ImportError: 11 | Document = BaseModel # type: ignore 12 | 13 | if TYPE_CHECKING: 14 | from pydantic.fields import ModelField 15 | 16 | 17 | class BeaniePersistenceHandler(AsyncPersistenceProtocol[Document]): 18 | async def save(self, data: Document) -> Document: 19 | """Persists a single instance in mongoDB.""" 20 | return await data.insert() # type: ignore 21 | 22 | async def save_many(self, data: List[Document]) -> List[Document]: 23 | """Persists multiple instances in mongoDB. 24 | 25 | Note: we cannot use the .insert_many method from Beanie here because it doesn't return the created instances 26 | """ 27 | result = [] 28 | for doc in data: 29 | result.append(await doc.insert()) 30 | return result 31 | 32 | 33 | class BeanieDocumentFactory(ModelFactory[Document]): 34 | """Subclass of ModelFactory for Beanie Documents.""" 35 | 36 | __async_persistence__ = BeaniePersistenceHandler 37 | 38 | @classmethod 39 | def get_field_value(cls, model_field: "ModelField", field_parameters: Union[dict, list, None] = None) -> Any: 40 | """Override to handle the fields created by the beanie Indexed helper function. 41 | 42 | Note: these fields do not have a class we can use, rather they instantiate a private class inside a closure. 43 | Hence, the hacky solution of checking the __name__ property 44 | """ 45 | if hasattr(model_field.type_, "__name__") and "Indexed " in model_field.type_.__name__: 46 | base_type = model_field.outer_type_.__bases__[0] 47 | model_field.outer_type_ = base_type 48 | model_field.type_ = base_type 49 | 50 | if hasattr(model_field.type_, "__name__") and "Link" in model_field.type_.__name__: 51 | link_class = model_field.outer_type_.__args__[0] 52 | model_field.outer_type_ = link_class 53 | model_field.type_ = link_class 54 | 55 | return super().get_field_value(model_field=model_field, field_parameters=field_parameters) 56 | -------------------------------------------------------------------------------- /pydantic_factories/extensions/odmantic_odm.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from pydantic import BaseModel 4 | 5 | from pydantic_factories.factory import ModelFactory 6 | from pydantic_factories.fields import Ignore 7 | 8 | try: 9 | from odmantic import EmbeddedModel, Model 10 | except ImportError: 11 | Model = BaseModel # type: ignore 12 | EmbeddedModel = BaseModel # type: ignore 13 | 14 | 15 | T = TypeVar("T", Model, EmbeddedModel) 16 | 17 | 18 | class OdmanticModelFactory(ModelFactory[T]): 19 | id = Ignore() 20 | -------------------------------------------------------------------------------- /pydantic_factories/extensions/ormar_orm.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import TYPE_CHECKING, Any, Union 3 | 4 | from pydantic import BaseModel 5 | from pydantic.utils import smart_deepcopy 6 | 7 | from pydantic_factories.factory import ModelFactory 8 | from pydantic_factories.utils import is_pydantic_model, is_union 9 | 10 | try: 11 | from ormar import Model 12 | except ImportError: 13 | Model = BaseModel # type: ignore 14 | 15 | if TYPE_CHECKING: 16 | from pydantic.fields import ModelField 17 | 18 | 19 | class OrmarModelFactory(ModelFactory[Model]): # pragma: no cover # type: ignore 20 | @classmethod 21 | def get_field_value(cls, model_field: "ModelField", field_parameters: Union[dict, list, None] = None) -> Any: 22 | """We need to handle here both choices and the fact that ormar sets values to be optional.""" 23 | if not model_field.required: 24 | model_field = smart_deepcopy(model_field) 25 | model_field.required = True 26 | 27 | # check if this is a RelationShip field 28 | if ( 29 | is_union(model_field=model_field) 30 | and model_field.sub_fields 31 | and any("PkOnly" in sf.name for sf in model_field.sub_fields) 32 | ): 33 | return cls.get_field_value( 34 | model_field=[sf for sf in model_field.sub_fields if is_pydantic_model(sf.outer_type_)][0], 35 | field_parameters=field_parameters, 36 | ) 37 | if getattr(model_field.field_info, "choices", False): 38 | return random.choice(list(model_field.field_info.choices)) # type: ignore 39 | return super().get_field_value(model_field=model_field, field_parameters=field_parameters) 40 | -------------------------------------------------------------------------------- /pydantic_factories/fields.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Optional, TypeVar, cast 2 | 3 | from typing_extensions import ParamSpec, TypedDict 4 | 5 | from pydantic_factories.exceptions import ParameterError 6 | 7 | T = TypeVar("T") 8 | P = ParamSpec("P") 9 | 10 | if TYPE_CHECKING: 11 | from pydantic_factories.factory import ModelFactory 12 | 13 | 14 | class WrappedCallable(TypedDict): 15 | value: Callable 16 | 17 | 18 | class Use(Generic[P, T]): 19 | __slots__ = ("fn", "kwargs", "args") 20 | 21 | def __init__(self, fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> None: 22 | """A class used to wrap a callable alongside any args and kwargs. 23 | 24 | The callable will be invoked whenever building the given factory 25 | attribute. 26 | 27 | Args: 28 | fn: A callable. 29 | *args: Args for the callable. 30 | **kwargs: Kwargs for the callable. 31 | """ 32 | self.fn: WrappedCallable = {"value": fn} 33 | self.kwargs = kwargs 34 | self.args = args 35 | 36 | def to_value(self) -> T: 37 | """Invokes the callable. 38 | 39 | Returns: 40 | The output of the callable. 41 | """ 42 | return cast("T", self.fn["value"](*self.args, **self.kwargs)) 43 | 44 | 45 | class PostGenerated: 46 | __slots__ = ("fn", "kwargs", "args") 47 | 48 | def __init__(self, fn: Callable, *args: Any, **kwargs: Any) -> None: 49 | """A class that allows for generating values after other fields are generated. 50 | 51 | Args: 52 | fn: A callable. 53 | *args: Args for the callable. 54 | **kwargs: Kwargs for the callable. 55 | """ 56 | self.fn: WrappedCallable = {"value": fn} 57 | self.kwargs = kwargs 58 | self.args = args 59 | 60 | def to_value(self, name: str, values: Dict[str, Any]) -> Any: 61 | """Invokes the post generation callback. 62 | 63 | Args: 64 | name: Field name. 65 | values: Generated values. 66 | 67 | Returns: 68 | An arbitrary value. 69 | """ 70 | return self.fn["value"](name, values, *self.args, **self.kwargs) 71 | 72 | 73 | class Fixture: 74 | __slots__ = ("fixture", "size", "kwargs") 75 | 76 | def __init__(self, fixture: Callable, size: Optional[int] = None, **kwargs: Any) -> None: 77 | """A class that allows using ModelFactory classes registered as pytest fixtures as factory fields. 78 | 79 | Args: 80 | fixture: A factory that was registered as a fixture. 81 | size: Optional batch size. 82 | **kwargs: Any build kwargs. 83 | """ 84 | self.fixture: WrappedCallable = {"value": fixture} 85 | self.size = size 86 | self.kwargs = kwargs 87 | 88 | def to_value(self) -> Any: 89 | """ 90 | Retries the correct factory for the fixture, calling either its build method - or if size is given, batch. 91 | 92 | Returns: 93 | The build result. 94 | """ 95 | from pydantic_factories.plugins.pytest_plugin import FactoryFixture 96 | 97 | factory = cast("Optional[ModelFactory]", FactoryFixture.factory_class_map.get(self.fixture["value"])) 98 | if not factory: 99 | raise ParameterError("fixture has not been registered using the register_factory decorator") 100 | if self.size: 101 | return factory.batch(self.size, **self.kwargs) 102 | return factory.build(**self.kwargs) 103 | 104 | 105 | class Require: 106 | """A placeholder class used to mark a given factory attribute as a required build-time kwarg.""" 107 | 108 | 109 | class Ignore: 110 | """A placeholder class used to mark a given factory attribute as ignored.""" 111 | -------------------------------------------------------------------------------- /pydantic_factories/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litestar-org/pydantic-factories/052b7fd15c7c590a56aa189f6d392ff03f9951db/pydantic_factories/plugins/__init__.py -------------------------------------------------------------------------------- /pydantic_factories/plugins/pytest_plugin.py: -------------------------------------------------------------------------------- 1 | import re 2 | from inspect import isclass 3 | from typing import Any, Callable, ClassVar, Dict, Literal, Optional, Type, Union 4 | 5 | import pytest 6 | from _pytest.config import Config 7 | from pydantic import validate_arguments 8 | 9 | from pydantic_factories.exceptions import ParameterError 10 | from pydantic_factories.factory import ModelFactory 11 | 12 | Scope = Union[ 13 | Literal["session", "package", "module", "class", "function"], 14 | Callable[[str, Config], Literal["session", "package", "module", "class", "function"]], 15 | ] 16 | 17 | 18 | split_pattern_1 = re.compile(r"([A-Z]+)([A-Z][a-z])") 19 | split_pattern_2 = re.compile(r"([a-z\d])([A-Z])") 20 | 21 | 22 | def _get_fixture_name(name: str) -> str: 23 | """from inflection.underscore.""" 24 | name = re.sub(split_pattern_1, r"\1_\2", name) 25 | name = re.sub(split_pattern_2, r"\1_\2", name) 26 | name = name.replace("-", "_") 27 | return name.lower() 28 | 29 | 30 | class FactoryFixture: 31 | __slots__ = ("scope", "autouse", "name") 32 | 33 | factory_class_map: ClassVar[Dict[Callable, Type["ModelFactory"]]] = {} 34 | 35 | @validate_arguments 36 | def __init__( 37 | self, 38 | scope: "Scope" = "function", 39 | autouse: bool = False, 40 | name: Optional[str] = None, 41 | ): 42 | self.scope = scope 43 | self.autouse = autouse 44 | self.name = name 45 | 46 | def __call__(self, model_factory: Type["ModelFactory"]) -> Any: 47 | if not isclass(model_factory): 48 | raise ParameterError(f"{model_factory.__name__} is not a class.") 49 | if not issubclass(model_factory, ModelFactory): 50 | raise ParameterError(f"{model_factory.__name__} is not a ModelFactory subclass.") 51 | 52 | fixture_name = self.name or _get_fixture_name(model_factory.__name__) 53 | fixture_register = pytest.fixture(scope=self.scope, name=fixture_name, autouse=self.autouse) # pyright: ignore 54 | 55 | def factory_fixture() -> Type["ModelFactory"]: 56 | return model_factory 57 | 58 | factory_fixture.__doc__ = model_factory.__doc__ 59 | marker = fixture_register(factory_fixture) 60 | self.factory_class_map[marker] = model_factory 61 | return marker 62 | 63 | 64 | def register_fixture( 65 | model_factory: Optional[Type["ModelFactory"]] = None, 66 | *, 67 | scope: "Scope" = "function", 68 | autouse: bool = False, 69 | name: Optional[str] = None, 70 | ) -> Any: 71 | """A decorator that allows registering model factories as fixtures. 72 | 73 | Args: 74 | model_factory: An optional model factory class to decorate. 75 | scope: Pytest scope. 76 | autouse: Auto use fixture. 77 | name: Fixture name. 78 | 79 | Returns: 80 | A fixture factory instance. 81 | """ 82 | fixture = FactoryFixture(scope=scope, autouse=autouse, name=name) 83 | return fixture(model_factory) if model_factory else fixture 84 | -------------------------------------------------------------------------------- /pydantic_factories/protocols.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=unnecessary-ellipsis 2 | from typing import Any, Dict, List, Protocol, TypeVar, Union, runtime_checkable 3 | 4 | from pydantic import BaseModel 5 | 6 | 7 | # According to https://github.com/python/cpython/blob/main/Lib/dataclasses.py#L1213 8 | # having __dataclass_fields__ is enough to identity a dataclass. 9 | @runtime_checkable 10 | class DataclassProtocol(Protocol): 11 | __dataclass_fields__: Dict[str, Any] 12 | 13 | 14 | T = TypeVar("T", bound=Union[BaseModel, DataclassProtocol]) 15 | 16 | 17 | @runtime_checkable 18 | class SyncPersistenceProtocol(Protocol[T]): 19 | def save(self, data: T) -> T: 20 | """Persist a single instance synchronously.""" 21 | ... 22 | 23 | def save_many(self, data: List[T]) -> List[T]: 24 | """Persist multiple instances synchronously.""" 25 | ... 26 | 27 | 28 | @runtime_checkable 29 | class AsyncPersistenceProtocol(Protocol[T]): 30 | async def save(self, data: T) -> T: 31 | """Persist a single instance asynchronously.""" 32 | ... 33 | 34 | async def save_many(self, data: List[T]) -> List[T]: 35 | """Persist multiple instances asynchronously.""" 36 | ... 37 | -------------------------------------------------------------------------------- /pydantic_factories/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litestar-org/pydantic-factories/052b7fd15c7c590a56aa189f6d392ff03f9951db/pydantic_factories/py.typed -------------------------------------------------------------------------------- /pydantic_factories/utils.py: -------------------------------------------------------------------------------- 1 | from dataclasses import Field as DataclassField 2 | from dataclasses import fields as get_dataclass_fields 3 | from decimal import Decimal 4 | from inspect import isclass 5 | from typing import TYPE_CHECKING, Any, Optional, Tuple, Type, TypeVar, cast 6 | 7 | from pydantic import BaseModel, create_model 8 | from pydantic.generics import GenericModel 9 | from pydantic.utils import almost_equal_floats 10 | 11 | T = TypeVar("T", int, float, Decimal) 12 | 13 | if TYPE_CHECKING: 14 | from typing import NewType 15 | 16 | from pydantic.fields import ModelField 17 | from typing_extensions import TypeGuard 18 | 19 | from pydantic_factories.protocols import DataclassProtocol 20 | 21 | 22 | def passes_pydantic_multiple_validator(value: T, multiple_of: T) -> bool: 23 | """A function that determines whether a given value passes the pydantic multiple_of validation.""" 24 | if multiple_of == 0: 25 | return True 26 | mod = float(value) / float(multiple_of) % 1 27 | return almost_equal_floats(mod, 0.0) or almost_equal_floats(mod, 1.0) 28 | 29 | 30 | def is_multiply_of_multiple_of_in_range(minimum: Optional[T], maximum: Optional[T], multiple_of: T) -> bool: 31 | """Determines if at least one multiply of `multiple_of` lies in the given range.""" 32 | # if the range has infinity on one of its ends then infinite number of multipliers 33 | # can be found within the range 34 | if minimum is None or maximum is None: 35 | return True 36 | 37 | # if we were given floats and multiple_of is really close to zero then it doesn't make sense 38 | # to continue trying to check the range 39 | if isinstance(minimum, float) and minimum / multiple_of in [float("+inf"), float("-inf")]: 40 | return False 41 | 42 | multiplier = round(minimum / multiple_of) 43 | step = 1 if multiple_of > 0 else -1 44 | # since rounding can go either up or down we may end up in a situation when 45 | # minimum is less or equal to `multiplier * multiple_of` 46 | # or when it is greater than `multiplier * multiple_of` 47 | # (in this case minimum is less than `(multiplier + 1)* multiple_of`). So we need to check 48 | # that any of two values is inside the given range. ASCII graphic below explain this 49 | # 50 | # minimum 51 | # -----------------+-------+-----------------------------------+---------------------------- 52 | # multiplier * multiple_of (multiplier + 1) * multiple_of 53 | # 54 | # 55 | # minimum 56 | # -------------------------+--------+--------------------------+---------------------------- 57 | # multiplier * multiple_of (multiplier + 1) * multiple_of 58 | # 59 | # since `multiple_of` can be a negative number adding +1 to `multiplier` drives `(multiplier + 1) * multiple_of`` 60 | # away from `minumum` to the -infinity. It looks like this: 61 | # minimum 62 | # -----------------------+--------------------------------+------------------------+-------- 63 | # (multiplier + 1) * multiple_of (multiplier) * multiple_of 64 | # 65 | # so for negative `multiple_of` we want to subtract 1 from multiplier 66 | for multiply in [multiplier * multiple_of, (multiplier + step) * multiple_of]: 67 | multiply_float = float(multiply) 68 | if ( 69 | almost_equal_floats(multiply_float, float(minimum)) 70 | or almost_equal_floats(multiply_float, float(maximum)) 71 | or minimum < multiply < maximum 72 | ): 73 | return True 74 | 75 | return False 76 | 77 | 78 | def is_pydantic_model(value: Any) -> "TypeGuard[Type[BaseModel]]": 79 | """A function to determine if a given value is a subclass of BaseModel.""" 80 | try: 81 | return isclass(value) and issubclass(value, (BaseModel, GenericModel)) 82 | except TypeError: # pragma: no cover 83 | # isclass(value) returns True for python 3.9+ typings such as list[str] etc. 84 | # this raises a TypeError in issubclass, and so we need to handle it. 85 | return False 86 | 87 | 88 | def set_model_field_to_required(model_field: "ModelField") -> "ModelField": 89 | """recursively sets the model_field and all sub_fields as required.""" 90 | model_field.required = True 91 | if model_field.sub_fields: 92 | for index, sub_field in enumerate(model_field.sub_fields): 93 | model_field.sub_fields[index] = set_model_field_to_required(model_field=sub_field) 94 | return model_field 95 | 96 | 97 | def create_model_from_dataclass( 98 | dataclass: Type["DataclassProtocol"], 99 | ) -> Type[BaseModel]: 100 | """Creates a subclass of BaseModel from a given dataclass. 101 | 102 | We are limited here because Pydantic does not perform proper field 103 | parsing when going this route - which requires we set the fields as 104 | required and not required independently. We currently do not handle 105 | deeply nested Any and Optional. 106 | """ 107 | dataclass_fields: Tuple[DataclassField, ...] = get_dataclass_fields(dataclass) # pyright: ignore 108 | model = create_model(dataclass.__name__, **{field.name: (field.type, ...) for field in dataclass_fields}) # type: ignore 109 | for field_name, model_field in model.__fields__.items(): 110 | dataclass_field = [field for field in dataclass_fields if field.name == field_name][0] 111 | typing_string = repr(dataclass_field.type) 112 | model_field = set_model_field_to_required(model_field=model_field) 113 | if typing_string.startswith("typing.Optional") or typing_string == "typing.Any": 114 | model_field.required = False 115 | model_field.allow_none = True 116 | model_field.default = None 117 | else: 118 | model_field.required = True 119 | model_field.allow_none = False 120 | setattr(model, field_name, model_field) 121 | return cast("Type[BaseModel]", model) 122 | 123 | 124 | def is_union(model_field: "ModelField") -> bool: 125 | """Determines whether the given model_field is type Union.""" 126 | field_type_repr = repr(model_field.outer_type_) 127 | if field_type_repr.startswith("typing.Union[") or ("|" in field_type_repr) or model_field.discriminator_key: 128 | return True 129 | return False 130 | 131 | 132 | def is_any(model_field: "ModelField") -> bool: 133 | """Determines whether the given model_field is type Any.""" 134 | type_name = cast("Any", getattr(model_field.outer_type_, "_name", None)) 135 | return model_field.type_ is Any or (type_name is not None and "Any" in type_name) 136 | 137 | 138 | def is_optional(model_field: "ModelField") -> bool: 139 | """Determines whether the given model_field is type Optional.""" 140 | return model_field.allow_none and not is_any(model_field=model_field) and not model_field.required 141 | 142 | 143 | def is_literal(model_field: "ModelField") -> bool: 144 | """Determines whether a given model_field is a Literal type.""" 145 | return "typing.Literal" in repr(model_field.outer_type_) or "typing_extensions.Literal" in repr( 146 | model_field.outer_type_ 147 | ) 148 | 149 | 150 | def is_new_type(value: Any) -> "TypeGuard[Type[NewType]]": 151 | """A function to determine if a given value is of NewType.""" 152 | # we have to use hasattr check since in Python 3.9 and below NewType is just a function 153 | return hasattr(value, "__supertype__") 154 | 155 | 156 | def unwrap_new_type_if_needed(value: Type[Any]) -> Type[Any]: 157 | """Returns base type if given value is a type derived with NewType. 158 | 159 | Otherwise it returns value untouched. 160 | """ 161 | unwrap = value 162 | while is_new_type(unwrap): 163 | unwrap = unwrap.__supertype__ 164 | 165 | return unwrap 166 | -------------------------------------------------------------------------------- /pydantic_factories/value_generators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litestar-org/pydantic-factories/052b7fd15c7c590a56aa189f6d392ff03f9951db/pydantic_factories/value_generators/__init__.py -------------------------------------------------------------------------------- /pydantic_factories/value_generators/complex_types.py: -------------------------------------------------------------------------------- 1 | import random 2 | from collections import defaultdict, deque 3 | from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union, cast 4 | 5 | from pydantic.fields import ( 6 | SHAPE_DEFAULTDICT, 7 | SHAPE_DEQUE, 8 | SHAPE_DICT, 9 | SHAPE_FROZENSET, 10 | SHAPE_ITERABLE, 11 | SHAPE_LIST, 12 | SHAPE_MAPPING, 13 | SHAPE_SEQUENCE, 14 | SHAPE_SET, 15 | SHAPE_TUPLE, 16 | SHAPE_TUPLE_ELLIPSIS, 17 | ModelField, 18 | ) 19 | 20 | from pydantic_factories.utils import is_any, is_union 21 | from pydantic_factories.value_generators.primitives import create_random_string 22 | 23 | if TYPE_CHECKING: # pragma: no cover 24 | from pydantic_factories.factory import ModelFactory 25 | 26 | type_mapping = { 27 | "Dict": dict, 28 | "Sequence": list, 29 | "List": list, 30 | "Set": set, 31 | "Deque": deque, 32 | "Mapping": dict, 33 | "Tuple": tuple, 34 | "DefaultDict": defaultdict, 35 | "FrozenSet": frozenset, 36 | "Iterable": list, 37 | } 38 | 39 | shape_mapping = { 40 | SHAPE_LIST: list, 41 | SHAPE_SET: set, 42 | SHAPE_MAPPING: dict, 43 | SHAPE_TUPLE: tuple, 44 | SHAPE_TUPLE_ELLIPSIS: tuple, 45 | SHAPE_SEQUENCE: list, 46 | SHAPE_FROZENSET: frozenset, 47 | SHAPE_ITERABLE: list, 48 | SHAPE_DEQUE: deque, 49 | SHAPE_DICT: dict, 50 | SHAPE_DEFAULTDICT: defaultdict, 51 | } 52 | 53 | 54 | def handle_container_type( 55 | model_field: ModelField, 56 | container_type: Type[Any], 57 | model_factory: Type["ModelFactory"], 58 | field_parameters: Optional[Union[Dict[Any, Any], List[Any]]] = None, 59 | ) -> Any: 60 | """Handles generation of container types, e.g. dict, list etc. 61 | 62 | recursively 63 | """ 64 | is_frozen_set = container_type is frozenset 65 | container = container_type() if not is_frozen_set else set() 66 | if isinstance(container, dict) and field_parameters and isinstance(field_parameters, dict): 67 | key, value = list(field_parameters.items())[0] 68 | container[key] = value 69 | return container 70 | value = None 71 | if model_field.sub_fields: 72 | value = handle_complex_type(model_field=random.choice(model_field.sub_fields), model_factory=model_factory) 73 | if value is not None: 74 | if isinstance(container, dict): 75 | key = handle_complex_type( 76 | model_field=cast("ModelField", model_field.key_field), model_factory=model_factory 77 | ) 78 | container[key] = value 79 | elif isinstance(container, (list, deque)): 80 | container.append(value) 81 | else: 82 | container.add(value) 83 | if is_frozen_set: 84 | return cast("set", frozenset(*container)) 85 | return container 86 | 87 | 88 | def handle_complex_type( 89 | model_field: ModelField, 90 | model_factory: Type["ModelFactory"], 91 | field_parameters: Optional[Union[Dict[Any, Any], List[Any]]] = None, 92 | ) -> Any: 93 | """Recursive type generation based on typing info stored in the graph like structure of pydantic model_fields.""" 94 | container_type: Optional[Type[Any]] = shape_mapping.get(model_field.shape) 95 | if container_type: 96 | if container_type is not tuple: 97 | return handle_container_type( 98 | model_field=model_field, 99 | container_type=container_type, 100 | model_factory=model_factory, 101 | field_parameters=field_parameters, 102 | ) 103 | return tuple( 104 | handle_complex_type(model_field=sub_field, model_factory=model_factory) 105 | for sub_field in (model_field.sub_fields or []) 106 | ) 107 | if is_union(model_field=model_field) and model_field.sub_fields: 108 | return handle_complex_type(model_field=random.choice(model_field.sub_fields), model_factory=model_factory) 109 | if is_any(model_field=model_field): 110 | return create_random_string(min_length=1, max_length=10) 111 | if model_factory.should_set_none_value(model_field): 112 | return None 113 | return model_factory.get_field_value(model_field=model_field) 114 | -------------------------------------------------------------------------------- /pydantic_factories/value_generators/constrained_number.py: -------------------------------------------------------------------------------- 1 | import random 2 | import sys 3 | from decimal import Decimal 4 | from typing import Any, Dict, Optional, Protocol, Tuple, Type, TypeVar, cast 5 | 6 | from pydantic_factories.exceptions import ParameterError 7 | from pydantic_factories.utils import ( 8 | is_multiply_of_multiple_of_in_range, 9 | passes_pydantic_multiple_validator, 10 | ) 11 | 12 | T = TypeVar("T", Decimal, int, float) 13 | 14 | 15 | class NumberGeneratorProtocol(Protocol[T]): 16 | def __call__(self, minimum: Optional[T] = None, maximum: Optional[T] = None) -> T: 17 | ... 18 | 19 | 20 | def get_increment(t_type: Type[T]) -> T: 21 | """Gets a small increment base to add to constrained values, i.e. lt/gt entries. 22 | 23 | Args: 24 | t_type: A value of type T. 25 | 26 | Returns: 27 | An increment T. 28 | """ 29 | values: Dict[Any, Any] = {int: 1, float: sys.float_info.epsilon, Decimal: Decimal("0.001")} 30 | return cast("T", values[t_type]) 31 | 32 | 33 | def get_value_or_none(equal_value: Optional[T], constrained: Optional[T], increment: T) -> Optional[T]: 34 | """helper function to reduce branching in the get_constrained_number_range method if the ge/le value is available, 35 | return that, otherwise return the gt/lt value + an increment or None. 36 | 37 | Args: 38 | equal_value: An GE/LE value. 39 | constrained: An GT/LT value. 40 | increment: increment. 41 | 42 | Returns: 43 | Optional T. 44 | """ 45 | if equal_value is not None: 46 | return equal_value 47 | if constrained is not None: 48 | return constrained + increment 49 | return None 50 | 51 | 52 | def get_constrained_number_range( 53 | lt: Optional[T], 54 | le: Optional[T], 55 | gt: Optional[T], 56 | ge: Optional[T], 57 | t_type: Type[T], 58 | multiple_of: Optional[T] = None, 59 | ) -> Tuple[Optional[T], Optional[T]]: 60 | """Returns the minimum and maximum values given a field's constraints.""" 61 | seed = t_type(random.random() * 10) 62 | minimum = get_value_or_none(equal_value=ge, constrained=gt, increment=get_increment(t_type)) 63 | maximum = get_value_or_none(equal_value=le, constrained=lt, increment=-get_increment(t_type)) # pyright: ignore 64 | 65 | if minimum is not None and maximum is not None and maximum < minimum: 66 | raise ParameterError("maximum value must be greater than minimum value") 67 | 68 | if multiple_of is None: 69 | if minimum is not None and maximum is None: 70 | if minimum == 0: 71 | return minimum, seed # pyright: ignore 72 | return minimum, minimum + seed 73 | if maximum is not None and minimum is None: 74 | return maximum - seed, maximum 75 | else: 76 | if multiple_of == 0.0: 77 | raise ParameterError("multiple_of can not be zero") 78 | if not is_multiply_of_multiple_of_in_range(minimum=minimum, maximum=maximum, multiple_of=multiple_of): 79 | raise ParameterError("given range should include at least one multiply of multiple_of") 80 | 81 | return minimum, maximum 82 | 83 | 84 | def generate_constrained_number( 85 | minimum: Optional[T], 86 | maximum: Optional[T], 87 | multiple_of: Optional[T], 88 | method: NumberGeneratorProtocol[T], 89 | ) -> T: 90 | """Generates a constrained number, output depends on the passed in callbacks.""" 91 | if minimum is not None and maximum is not None: 92 | if multiple_of is None: 93 | return method(minimum, maximum) 94 | if multiple_of >= minimum: 95 | return multiple_of 96 | result = minimum 97 | while not passes_pydantic_multiple_validator(result, multiple_of): 98 | result = round(method(minimum, maximum) / multiple_of) * multiple_of 99 | return result 100 | if multiple_of is not None: 101 | return multiple_of 102 | return method() 103 | -------------------------------------------------------------------------------- /pydantic_factories/value_generators/primitives.py: -------------------------------------------------------------------------------- 1 | import random 2 | from binascii import hexlify 3 | from decimal import Decimal 4 | from os import urandom 5 | from typing import Optional, Union 6 | 7 | 8 | def create_random_float( 9 | minimum: Optional[Union[Decimal, int, float]] = None, maximum: Optional[Union[Decimal, int, float]] = None 10 | ) -> float: 11 | """Generates a random float given the constraints.""" 12 | if minimum is None: 13 | minimum = float(random.randint(0, 100)) if maximum is None else float(maximum) - 100.0 14 | if maximum is None: 15 | maximum = float(minimum) + 1.0 * 2.0 if minimum >= 0 else float(minimum) + 1.0 / 2.0 16 | return random.uniform(float(minimum), float(maximum)) 17 | 18 | 19 | def create_random_integer(minimum: Optional[int] = None, maximum: Optional[int] = None) -> int: 20 | """Generates a random int given the constraints.""" 21 | return int(create_random_float(minimum, maximum)) 22 | 23 | 24 | def create_random_decimal(minimum: Optional[Decimal] = None, maximum: Optional[Decimal] = None) -> Decimal: 25 | """Generates a random Decimal given the constraints.""" 26 | return Decimal(str(create_random_float(minimum, maximum))) 27 | 28 | 29 | def create_random_bytes( 30 | min_length: Optional[int] = None, max_length: Optional[int] = None, lower_case: bool = False 31 | ) -> bytes: 32 | """Generates a random bytes given the constraints.""" 33 | if min_length is None: 34 | min_length = 0 35 | if max_length is None: 36 | max_length = min_length + 1 * 2 37 | length = random.randint(min_length, max_length) 38 | result = hexlify(urandom(length)) 39 | if lower_case: 40 | result = result.lower() 41 | if max_length and len(result) > max_length: 42 | end = random.randint(min_length or 0, max_length) 43 | return result[0:end] 44 | return result 45 | 46 | 47 | def create_random_string( 48 | min_length: Optional[int] = None, max_length: Optional[int] = None, lower_case: bool = False 49 | ) -> str: 50 | """Generates a random string given the constraints.""" 51 | return create_random_bytes(min_length=min_length, max_length=max_length, lower_case=lower_case).decode("utf-8") 52 | 53 | 54 | def create_random_boolean() -> bool: 55 | """Generates a random boolean value.""" 56 | return bool(random.getrandbits(1)) 57 | -------------------------------------------------------------------------------- /pydantic_factories/value_generators/regex.py: -------------------------------------------------------------------------------- 1 | """The code in this files is adapted from https://github.com/crdoconnor/xeger/blob/master/xeger/xeger.py Which in turn 2 | adapted it from https://bitbucket.org/leapfrogdevelopment/rstr/ 3 | 4 | Copyright (C) 2015, Colm O'Connor 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | * Neither the name of the Leapfrog Direct Response, LLC, including 15 | its subsidiaries and affiliates nor the names of its 16 | contributors, may be used to endorse or promote products derived 17 | from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LEAPFROG DIRECT 23 | RESPONSE, LLC, INCLUDING ITS SUBSIDIARIES AND AFFILIATES, BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 27 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 28 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 29 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 30 | IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | """ 32 | 33 | from itertools import chain 34 | from random import Random 35 | from string import ( 36 | ascii_letters, 37 | ascii_lowercase, 38 | ascii_uppercase, 39 | digits, 40 | printable, 41 | punctuation, 42 | whitespace, 43 | ) 44 | from typing import Any, Dict, List, Optional, Pattern, Tuple, Union 45 | 46 | try: # >=3.11 47 | from re._parser import SubPattern, parse # pyright:ignore 48 | except ImportError: # < 3.11 49 | from sre_parse import SubPattern, parse # pylint: disable=deprecated-module 50 | 51 | _alphabets = { 52 | "printable": printable, 53 | "letters": ascii_letters, 54 | "uppercase": ascii_uppercase, 55 | "lowercase": ascii_lowercase, 56 | "digits": digits, 57 | "punctuation": punctuation, 58 | "nondigits": ascii_letters + punctuation, 59 | "nonletters": digits + punctuation, 60 | "whitespace": whitespace, 61 | "nonwhitespace": printable.strip(), 62 | "normal": ascii_letters + digits + " ", 63 | "word": ascii_letters + digits + "_", 64 | "nonword": "".join(set(printable).difference(ascii_letters + digits + "_")), 65 | "postalsafe": ascii_letters + digits + " .-#/", 66 | "urlsafe": ascii_letters + digits + "-._~", 67 | "domainsafe": ascii_letters + digits + "-", 68 | } 69 | 70 | _categories = { 71 | "category_digit": _alphabets["digits"], 72 | "category_not_digit": _alphabets["nondigits"], 73 | "category_space": _alphabets["whitespace"], 74 | "category_not_space": _alphabets["nonwhitespace"], 75 | "category_word": _alphabets["word"], 76 | "category_not_word": _alphabets["nonword"], 77 | } 78 | 79 | 80 | class RegexFactory: 81 | def __init__(self, limit: int = 10, seed: Optional[int] = None) -> None: 82 | self._limit = limit 83 | self._cache: Dict[str, Any] = {} 84 | self._random = Random(x=seed) 85 | 86 | self._cases = { 87 | "literal": chr, 88 | "not_literal": lambda x: self._random.choice(printable.replace(chr(x), "")), 89 | "at": lambda x: "", 90 | "in": self._handle_in, 91 | "any": lambda x: self._random.choice(printable.replace("\n", "")), 92 | "range": lambda x: [chr(i) for i in range(x[0], x[1] + 1)], 93 | "category": lambda x: _categories[str(x).lower()], 94 | "branch": lambda x: "".join(self._handle_state(i) for i in self._random.choice(x[1])), 95 | "subpattern": self._handle_group, 96 | "assert": lambda x: "".join(self._handle_state(i) for i in x[1]), 97 | "assert_not": lambda x: "", 98 | "groupref": lambda x: self._cache[x], 99 | "min_repeat": lambda x: self._handle_repeat(*x), 100 | "max_repeat": lambda x: self._handle_repeat(*x), 101 | "negate": lambda x: [False], 102 | } 103 | 104 | def __call__(self, string_or_regex: Union[str, Pattern]) -> str: 105 | pattern = string_or_regex.pattern if isinstance(string_or_regex, Pattern) else string_or_regex 106 | parsed = parse(pattern) 107 | result = self._build_string(parsed) 108 | self._cache.clear() 109 | return result # noqa: R504 110 | 111 | def _build_string(self, parsed: SubPattern) -> str: 112 | return "".join([self._handle_state(state) for state in parsed]) # pyright:ignore 113 | 114 | def _handle_state(self, state: Tuple[SubPattern, Tuple[Any, ...]]) -> Any: 115 | opcode, value = state 116 | return self._cases[str(opcode).lower()](value) # type: ignore[no-untyped-call] 117 | 118 | def _handle_group(self, value: Tuple[Any, ...]) -> str: 119 | result = "".join(self._handle_state(i) for i in value[3]) 120 | if value[0]: 121 | self._cache[value[0]] = result 122 | return result 123 | 124 | def _handle_in(self, value: Tuple[Any, ...]) -> Any: 125 | candidates = list(chain(*(self._handle_state(i) for i in value))) 126 | if candidates and candidates[0] is False: 127 | candidates = list(set(printable).difference(candidates[1:])) 128 | return self._random.choice(candidates) 129 | return self._random.choice(candidates) 130 | 131 | def _handle_repeat(self, start_range: int, end_range: Any, value: SubPattern) -> str: 132 | result: List[str] = [] 133 | end_range = min(end_range, self._limit) 134 | 135 | for i in range(self._random.randint(start_range, max(start_range, end_range))): 136 | result.append("".join(self._handle_state(i) for i in list(value))) # pyright:ignore 137 | 138 | return "".join(result) 139 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pydantic-factories" 3 | version = "1.17.3" 4 | description = "Mock data generation for pydantic based models and python dataclasses" 5 | authors = ["Na'aman Hirschfeld "] 6 | maintainers = [ 7 | "Na'aman Hirschfeld ", 8 | "Peter Schutt ", 9 | "Cody Fincher ", 10 | "Janek Nouvertné ", 11 | "Konstantin Mikhailov " 12 | ] 13 | license = "MIT" 14 | readme = "README.md" 15 | homepage = "https://github.com/starlite-api/pydantic-factories" 16 | repository = "https://github.com/starlite-api/pydantic-factories" 17 | documentation = "https://github.com/starlite-api/pydantic-factories" 18 | keywords = [ 19 | "dataclasses", 20 | "factory", 21 | "faker", 22 | "mock", 23 | "pydantic", 24 | "pytest", 25 | "starlite", 26 | "tdd", 27 | "testing", 28 | ] 29 | classifiers = [ 30 | "Environment :: Web Environment", 31 | "Framework :: Pytest", 32 | "Intended Audience :: Developers", 33 | "License :: OSI Approved :: MIT License", 34 | "Natural Language :: English", 35 | "Operating System :: OS Independent", 36 | "Programming Language :: Python", 37 | "Programming Language :: Python :: 3.8", 38 | "Programming Language :: Python :: 3.9", 39 | "Programming Language :: Python :: 3.10", 40 | "Programming Language :: Python :: 3.11", 41 | "Topic :: Software Development", 42 | "Topic :: Software Development :: Libraries", 43 | "Topic :: Software Development :: Testing", 44 | "Topic :: Software Development :: Testing :: Unit", 45 | "Topic :: Utilities", 46 | "Typing :: Typed", 47 | ] 48 | include = ["CHANGELOG.md"] 49 | packages = [ 50 | { include = "pydantic_factories" }, 51 | ] 52 | 53 | [tool.poetry.dependencies] 54 | python = ">=3.8,<4.0" 55 | faker = "*" 56 | pydantic = ">=1.10.0" 57 | typing-extensions = "*" 58 | 59 | [tool.poetry.group.dev.dependencies] 60 | email-validator = "*" 61 | hypothesis = "*" 62 | pre-commit = "*" 63 | pytest = "*" 64 | pytest-asyncio = "*" 65 | pytest-cov = "*" 66 | 67 | [build-system] 68 | requires = ["poetry-core>=1.0.0"] 69 | build-backend = "poetry.core.masonry.api" 70 | 71 | [tool.black] 72 | line-length = 120 73 | include = '\.pyi?$' 74 | 75 | [tool.isort] 76 | profile = "black" 77 | multi_line_output = 3 78 | 79 | [tool.pylint.MESSAGE_CONTROL] 80 | disable = [ 81 | "import-outside-toplevel", 82 | "line-too-long", 83 | "missing-class-docstring", 84 | "missing-module-docstring", 85 | "not-callable", 86 | "too-few-public-methods", 87 | "ungrouped-imports", 88 | "unnecessary-lambda-assignment", 89 | ] 90 | enable = "useless-suppression" 91 | extension-pkg-allow-list = ["pydantic"] 92 | 93 | [tool.pylint.REPORTS] 94 | reports = "no" 95 | 96 | [tool.pylint.FORMAT] 97 | max-line-length = "120" 98 | 99 | [tool.pylint.DESIGN] 100 | max-args = 10 101 | max-attributes = 10 102 | max-locals = 12 103 | max-returns = 10 104 | max-public-methods = 21 105 | 106 | [tool.pylint.VARIABLES] 107 | ignored-argument-names = "args|kwargs|_|__" 108 | no-docstring-rgx = "(__.*__|main|test.*|.*test|.*Test|^_.*)$" 109 | 110 | [tool.pylint.BASIC] 111 | good-names = "_,i,e,l,g,mm,yy,T,lt,le,gt,ge,cb,fn" 112 | 113 | [tool.coverage.run] 114 | omit = ["*/tests/*"] 115 | 116 | [tool.pytest.ini_options] 117 | asyncio_mode = "auto" 118 | 119 | [tool.coverage.report] 120 | exclude_lines = [ 121 | 'pragma: no cover', 122 | 'if TYPE_CHECKING:', 123 | 'except ImportError:', 124 | '\.\.\.' 125 | ] 126 | 127 | [tool.pycln] 128 | all = true 129 | 130 | [tool.pydocstyle] 131 | add-ignore = "D100,D104,D105,D106,D202,D205,D415" 132 | add-select = "D401,D404,D417" 133 | convention = "google" 134 | match_dir = "pydantic_factories" 135 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=starlite-api_pydantic-factories 2 | sonar.organization=starlite-api 3 | sonar.python.coverage.reportPaths=coverage.xml 4 | sonar.test.inclusions=tests/test_*.py 5 | sonar.sources=pydantic_factories 6 | sonar.sourceEncoding=UTF-8 7 | sonar.python.version=3.7, 3.8, 3.9, 3.10 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litestar-org/pydantic-factories/052b7fd15c7c590a56aa189f6d392ff03f9951db/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litestar-org/pydantic-factories/052b7fd15c7c590a56aa189f6d392ff03f9951db/tests/conftest.py -------------------------------------------------------------------------------- /tests/constraints/test_byte_constraints.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import pytest 4 | from hypothesis import given 5 | from hypothesis.strategies import booleans, integers 6 | from pydantic import ConstrainedBytes 7 | 8 | from pydantic_factories.constraints.strings import handle_constrained_bytes 9 | from pydantic_factories.exceptions import ParameterError 10 | 11 | 12 | def create_constrained_field( 13 | to_lower: bool, min_length: Optional[int] = None, max_length: Optional[int] = None 14 | ) -> ConstrainedBytes: 15 | field = ConstrainedBytes() 16 | field.max_length = max_length 17 | field.min_length = min_length 18 | field.to_lower = to_lower 19 | return field 20 | 21 | 22 | @given(booleans(), integers(max_value=10000), integers(max_value=10000)) 23 | def test_handle_constrained_bytes_with_min_length_and_max_length( 24 | to_lower: bool, min_length: int, max_length: int 25 | ) -> None: 26 | field = create_constrained_field(to_lower=to_lower, min_length=min_length, max_length=max_length) 27 | if min_length < 0 or max_length < 0 or min_length > max_length: 28 | with pytest.raises(ParameterError): 29 | handle_constrained_bytes(field=field) 30 | else: 31 | result = handle_constrained_bytes(field=field) 32 | if to_lower: 33 | assert result == result.lower() 34 | assert len(result) >= min_length 35 | assert len(result) <= max_length 36 | 37 | 38 | @given(booleans(), integers(max_value=10000)) 39 | def test_handle_constrained_bytes_with_min_length(to_lower: bool, min_length: int) -> None: 40 | field = create_constrained_field(to_lower=to_lower, min_length=min_length) 41 | if min_length < 0: 42 | with pytest.raises(ParameterError): 43 | handle_constrained_bytes(field=field) 44 | else: 45 | result = handle_constrained_bytes(field=field) 46 | if to_lower: 47 | assert result == result.lower() 48 | assert len(result) >= min_length 49 | 50 | 51 | @given(booleans(), integers(max_value=10000)) 52 | def test_handle_constrained_bytes_with_max_length(to_lower: bool, max_length: int) -> None: 53 | field = create_constrained_field(to_lower=to_lower, max_length=max_length) 54 | if max_length < 0: 55 | with pytest.raises(ParameterError): 56 | handle_constrained_bytes(field=field) 57 | else: 58 | result = handle_constrained_bytes(field=field) 59 | if to_lower: 60 | assert result == result.lower() 61 | assert len(result) <= max_length 62 | -------------------------------------------------------------------------------- /tests/constraints/test_date_constraints.py: -------------------------------------------------------------------------------- 1 | from datetime import date, timedelta 2 | from typing import Dict, Optional 3 | 4 | import pytest 5 | from hypothesis import given 6 | from hypothesis.strategies import dates 7 | from pydantic import BaseModel, condate 8 | 9 | from pydantic_factories import ModelFactory 10 | 11 | 12 | @given( 13 | dates(max_value=date.today() - timedelta(days=3)), 14 | dates(min_value=date.today()), 15 | ) 16 | @pytest.mark.parametrize("start, end", ((None, None), ("ge", "le"), ("gt", "lt"), ("ge", "lt"), ("gt", "le"))) 17 | def test_handle_constrained_date( 18 | start: Optional[str], 19 | end: Optional[str], 20 | start_date: date, 21 | end_date: date, 22 | ) -> None: 23 | if start_date != end_date: 24 | kwargs: Dict[str, date] = {} 25 | if start: 26 | kwargs[start] = start_date 27 | if end: 28 | kwargs[end] = end_date 29 | 30 | class MyModel(BaseModel): 31 | value: condate(**kwargs) # type: ignore 32 | 33 | class MyFactory(ModelFactory): 34 | __model__ = MyModel 35 | 36 | result = MyFactory.build() 37 | 38 | assert result.value 39 | -------------------------------------------------------------------------------- /tests/constraints/test_float_constraints.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import pytest 4 | from hypothesis import given 5 | from hypothesis.strategies import floats 6 | from pydantic import ConstrainedFloat 7 | 8 | from pydantic_factories.constraints.float import handle_constrained_float 9 | from pydantic_factories.exceptions import ParameterError 10 | from pydantic_factories.utils import ( 11 | is_multiply_of_multiple_of_in_range, 12 | passes_pydantic_multiple_validator, 13 | ) 14 | 15 | 16 | def create_constrained_field( 17 | gt: Optional[float] = None, 18 | ge: Optional[float] = None, 19 | lt: Optional[float] = None, 20 | le: Optional[float] = None, 21 | multiple_of: Optional[float] = None, 22 | ) -> ConstrainedFloat: 23 | field = ConstrainedFloat() 24 | field.ge = ge 25 | field.gt = gt 26 | field.lt = lt 27 | field.le = le 28 | field.multiple_of = multiple_of 29 | return field 30 | 31 | 32 | def test_handle_constrained_float_without_constraints() -> None: 33 | result = handle_constrained_float(create_constrained_field()) 34 | assert isinstance(result, float) 35 | 36 | 37 | @given(floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000)) 38 | def test_handle_constrained_float_handles_ge(minimum: float) -> None: 39 | result = handle_constrained_float(create_constrained_field(ge=minimum)) 40 | assert result >= minimum 41 | 42 | 43 | @given(floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000)) 44 | def test_handle_constrained_float_handles_gt(minimum: float) -> None: 45 | result = handle_constrained_float(create_constrained_field(gt=minimum)) 46 | assert result > minimum 47 | 48 | 49 | @given(floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000)) 50 | def test_handle_constrained_float_handles_le(maximum: float) -> None: 51 | result = handle_constrained_float(create_constrained_field(le=maximum)) 52 | assert result <= maximum 53 | 54 | 55 | @given(floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000)) 56 | def test_handle_constrained_float_handles_lt(maximum: float) -> None: 57 | result = handle_constrained_float(create_constrained_field(lt=maximum)) 58 | assert result < maximum 59 | 60 | 61 | @given(floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000)) 62 | def test_handle_constrained_float_handles_multiple_of(multiple_of: float) -> None: 63 | if multiple_of != 0.0: 64 | result = handle_constrained_float(create_constrained_field(multiple_of=multiple_of)) 65 | assert passes_pydantic_multiple_validator(result, multiple_of) 66 | else: 67 | with pytest.raises(ParameterError): 68 | handle_constrained_float(create_constrained_field(multiple_of=multiple_of)) 69 | 70 | 71 | @given( 72 | floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000), 73 | floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000), 74 | ) 75 | def test_handle_constrained_float_handles_multiple_of_with_lt(val1: float, val2: float) -> None: 76 | multiple_of, max_value = sorted([val1, val2]) 77 | if multiple_of != 0.0: 78 | result = handle_constrained_float(create_constrained_field(multiple_of=multiple_of, lt=max_value)) 79 | assert passes_pydantic_multiple_validator(result, multiple_of) 80 | else: 81 | with pytest.raises(ParameterError): 82 | handle_constrained_float(create_constrained_field(multiple_of=multiple_of, lt=max_value)) 83 | 84 | 85 | @given( 86 | floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000), 87 | floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000), 88 | ) 89 | def test_handle_constrained_float_handles_multiple_of_with_le(val1: float, val2: float) -> None: 90 | multiple_of, max_value = sorted([val1, val2]) 91 | if multiple_of != 0.0: 92 | result = handle_constrained_float(create_constrained_field(multiple_of=multiple_of, le=max_value)) 93 | assert passes_pydantic_multiple_validator(result, multiple_of) 94 | else: 95 | with pytest.raises(ParameterError): 96 | handle_constrained_float(create_constrained_field(multiple_of=multiple_of, le=max_value)) 97 | 98 | 99 | @given( 100 | floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000), 101 | floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000), 102 | ) 103 | def test_handle_constrained_float_handles_multiple_of_with_ge(val1: float, val2: float) -> None: 104 | min_value, multiple_of = sorted([val1, val2]) 105 | if multiple_of != 0.0: 106 | result = handle_constrained_float(create_constrained_field(multiple_of=multiple_of, ge=min_value)) 107 | assert passes_pydantic_multiple_validator(result, multiple_of) 108 | else: 109 | with pytest.raises(ParameterError): 110 | handle_constrained_float(create_constrained_field(multiple_of=multiple_of, ge=min_value)) 111 | 112 | 113 | @given( 114 | floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000), 115 | floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000), 116 | ) 117 | def test_handle_constrained_float_handles_multiple_of_with_gt(val1: float, val2: float) -> None: 118 | min_value, multiple_of = sorted([val1, val2]) 119 | if multiple_of != 0.0: 120 | result = handle_constrained_float(create_constrained_field(multiple_of=multiple_of, gt=min_value)) 121 | assert passes_pydantic_multiple_validator(result, multiple_of) 122 | else: 123 | with pytest.raises(ParameterError): 124 | handle_constrained_float(create_constrained_field(multiple_of=multiple_of, gt=min_value)) 125 | 126 | 127 | @given( 128 | floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000), 129 | floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000), 130 | floats(allow_nan=False, allow_infinity=False, min_value=-1000000000, max_value=1000000000), 131 | ) 132 | def test_handle_constrained_float_handles_multiple_of_with_ge_and_le(val1: float, val2: float, val3: float) -> None: 133 | min_value, multiple_of, max_value = sorted([val1, val2, val3]) 134 | if multiple_of != 0.0 and is_multiply_of_multiple_of_in_range( 135 | minimum=min_value, maximum=max_value, multiple_of=multiple_of 136 | ): 137 | result = handle_constrained_float(create_constrained_field(multiple_of=multiple_of, ge=min_value, le=max_value)) 138 | assert passes_pydantic_multiple_validator(result, multiple_of) 139 | else: 140 | with pytest.raises(ParameterError): 141 | handle_constrained_float(create_constrained_field(multiple_of=multiple_of, ge=min_value, le=max_value)) 142 | -------------------------------------------------------------------------------- /tests/constraints/test_frozen_set_constraints.py: -------------------------------------------------------------------------------- 1 | from contextlib import suppress 2 | from typing import Any, Optional 3 | 4 | import pytest 5 | from hypothesis import given 6 | from hypothesis.strategies import integers 7 | from pydantic import BaseConfig, ConstrainedFrozenSet 8 | from pydantic.fields import ModelField 9 | 10 | from pydantic_factories import ModelFactory 11 | from pydantic_factories.constraints.collection import handle_constrained_collection 12 | from pydantic_factories.exceptions import ParameterError 13 | 14 | 15 | def create_model_field( 16 | item_type: Any, 17 | min_items: Optional[int] = None, 18 | max_items: Optional[int] = None, 19 | ) -> ModelField: 20 | field = ConstrainedFrozenSet() 21 | field.min_items = min_items 22 | field.max_items = max_items 23 | field.item_type = item_type 24 | model_field = ModelField(name="", class_validators={}, model_config=BaseConfig, type_=item_type) 25 | model_field.outer_type_ = field 26 | return model_field 27 | 28 | 29 | @given( 30 | integers(min_value=0, max_value=10), 31 | integers(min_value=0, max_value=10), 32 | ) 33 | def test_handle_constrained_set_with_min_items_and_max_items(min_items: int, max_items: int) -> None: 34 | if max_items >= min_items: 35 | field = create_model_field(str, min_items=min_items, max_items=max_items) 36 | result = handle_constrained_collection(collection_type=set, model_field=field, model_factory=ModelFactory) 37 | assert len(result) >= min_items 38 | assert len(result) <= max_items 39 | else: 40 | field = create_model_field(str, min_items=min_items, max_items=max_items) 41 | with pytest.raises(ParameterError): 42 | handle_constrained_collection(collection_type=set, model_field=field, model_factory=ModelFactory) 43 | 44 | 45 | @given( 46 | integers(min_value=0, max_value=10), 47 | ) 48 | def test_handle_constrained_set_with_max_items( 49 | max_items: int, 50 | ) -> None: 51 | field = create_model_field(str, max_items=max_items) 52 | result = handle_constrained_collection(collection_type=set, model_field=field, model_factory=ModelFactory) 53 | assert len(result) <= max_items 54 | 55 | 56 | @given( 57 | integers(min_value=0, max_value=10), 58 | ) 59 | def test_handle_constrained_set_with_min_items( 60 | min_items: int, 61 | ) -> None: 62 | field = create_model_field(str, min_items=min_items) 63 | result = handle_constrained_collection(collection_type=set, model_field=field, model_factory=ModelFactory) 64 | assert len(result) >= min_items 65 | 66 | 67 | def test_handle_constrained_set_with_different_types() -> None: 68 | with suppress(ParameterError): 69 | for t_type in ModelFactory.get_provider_map(): 70 | field = create_model_field(t_type, min_items=1) 71 | result = handle_constrained_collection(collection_type=set, model_field=field, model_factory=ModelFactory) 72 | assert len(result) > 0 73 | -------------------------------------------------------------------------------- /tests/constraints/test_int_constraints.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import pytest 4 | from hypothesis import given 5 | from hypothesis.strategies import integers 6 | from pydantic import ConstrainedInt 7 | 8 | from pydantic_factories.constraints.integer import handle_constrained_int 9 | from pydantic_factories.exceptions import ParameterError 10 | from pydantic_factories.utils import ( 11 | is_multiply_of_multiple_of_in_range, 12 | passes_pydantic_multiple_validator, 13 | ) 14 | 15 | 16 | def create_constrained_field( 17 | gt: Optional[int] = None, 18 | ge: Optional[int] = None, 19 | lt: Optional[int] = None, 20 | le: Optional[int] = None, 21 | multiple_of: Optional[int] = None, 22 | ) -> ConstrainedInt: 23 | field = ConstrainedInt() 24 | field.ge = ge 25 | field.gt = gt 26 | field.lt = lt 27 | field.le = le 28 | field.multiple_of = multiple_of 29 | return field 30 | 31 | 32 | def test_handle_constrained_int_without_constraints() -> None: 33 | result = handle_constrained_int(create_constrained_field()) 34 | assert isinstance(result, int) 35 | 36 | 37 | @given(integers(min_value=-1000000000, max_value=1000000000)) 38 | def test_handle_constrained_int_handles_ge(minimum: int) -> None: 39 | result = handle_constrained_int(create_constrained_field(ge=minimum)) 40 | assert result >= minimum 41 | 42 | 43 | @given(integers(min_value=-1000000000, max_value=1000000000)) 44 | def test_handle_constrained_int_handles_gt(minimum: int) -> None: 45 | result = handle_constrained_int(create_constrained_field(gt=minimum)) 46 | assert result > minimum 47 | 48 | 49 | @given(integers(min_value=-1000000000, max_value=1000000000)) 50 | def test_handle_constrained_int_handles_le(maximum: int) -> None: 51 | result = handle_constrained_int(create_constrained_field(le=maximum)) 52 | assert result <= maximum 53 | 54 | 55 | @given(integers(min_value=-1000000000, max_value=1000000000)) 56 | def test_handle_constrained_int_handles_lt(maximum: int) -> None: 57 | result = handle_constrained_int(create_constrained_field(lt=maximum)) 58 | assert result < maximum 59 | 60 | 61 | @given(integers(min_value=-1000000000, max_value=1000000000)) 62 | def test_handle_constrained_int_handles_multiple_of(multiple_of: int) -> None: 63 | if multiple_of != 0: 64 | result = handle_constrained_int(create_constrained_field(multiple_of=multiple_of)) 65 | assert passes_pydantic_multiple_validator(result, multiple_of) 66 | else: 67 | with pytest.raises(ParameterError): 68 | handle_constrained_int(create_constrained_field(multiple_of=multiple_of)) 69 | 70 | 71 | @given( 72 | integers(min_value=-1000000000, max_value=1000000000), 73 | integers(min_value=-1000000000, max_value=1000000000), 74 | ) 75 | def test_handle_constrained_int_handles_multiple_of_with_lt(val1: int, val2: int) -> None: 76 | multiple_of, max_value = sorted([val1, val2]) 77 | if multiple_of != 0: 78 | result = handle_constrained_int(create_constrained_field(multiple_of=multiple_of, lt=max_value)) 79 | assert passes_pydantic_multiple_validator(result, multiple_of) 80 | else: 81 | with pytest.raises(ParameterError): 82 | handle_constrained_int(create_constrained_field(multiple_of=multiple_of, lt=max_value)) 83 | 84 | 85 | @given( 86 | integers(min_value=-1000000000, max_value=1000000000), 87 | integers(min_value=-1000000000, max_value=1000000000), 88 | ) 89 | def test_handle_constrained_int_handles_multiple_of_with_le(val1: int, val2: int) -> None: 90 | multiple_of, max_value = sorted([val1, val2]) 91 | if multiple_of != 0: 92 | result = handle_constrained_int(create_constrained_field(multiple_of=multiple_of, le=max_value)) 93 | assert passes_pydantic_multiple_validator(result, multiple_of) 94 | else: 95 | with pytest.raises(ParameterError): 96 | handle_constrained_int(create_constrained_field(multiple_of=multiple_of, le=max_value)) 97 | 98 | 99 | @given( 100 | integers(min_value=-1000000000, max_value=1000000000), 101 | integers(min_value=-1000000000, max_value=1000000000), 102 | ) 103 | def test_handle_constrained_int_handles_multiple_of_with_ge(val1: int, val2: int) -> None: 104 | min_value, multiple_of = sorted([val1, val2]) 105 | if multiple_of != 0: 106 | result = handle_constrained_int(create_constrained_field(multiple_of=multiple_of, ge=min_value)) 107 | assert passes_pydantic_multiple_validator(result, multiple_of) 108 | else: 109 | with pytest.raises(ParameterError): 110 | handle_constrained_int(create_constrained_field(multiple_of=multiple_of, ge=min_value)) 111 | 112 | 113 | @given( 114 | integers(min_value=-1000000000, max_value=1000000000), 115 | integers(min_value=-1000000000, max_value=1000000000), 116 | ) 117 | def test_handle_constrained_int_handles_multiple_of_with_gt(val1: int, val2: int) -> None: 118 | min_value, multiple_of = sorted([val1, val2]) 119 | if multiple_of != 0: 120 | result = handle_constrained_int(create_constrained_field(multiple_of=multiple_of, gt=min_value)) 121 | assert passes_pydantic_multiple_validator(result, multiple_of) 122 | else: 123 | with pytest.raises(ParameterError): 124 | handle_constrained_int(create_constrained_field(multiple_of=multiple_of, gt=min_value)) 125 | 126 | 127 | @given( 128 | integers(min_value=-1000000000, max_value=1000000000), 129 | integers(min_value=-1000000000, max_value=1000000000), 130 | integers(min_value=-1000000000, max_value=1000000000), 131 | ) 132 | def test_handle_constrained_int_handles_multiple_of_with_ge_and_le(val1: int, val2: int, val3: int) -> None: 133 | min_value, multiple_of, max_value = sorted([val1, val2, val3]) 134 | if multiple_of != 0 and is_multiply_of_multiple_of_in_range( 135 | minimum=min_value, maximum=max_value, multiple_of=multiple_of 136 | ): 137 | result = handle_constrained_int(create_constrained_field(multiple_of=multiple_of, ge=min_value, le=max_value)) 138 | assert passes_pydantic_multiple_validator(result, multiple_of) 139 | else: 140 | with pytest.raises(ParameterError): 141 | handle_constrained_int(create_constrained_field(multiple_of=multiple_of, ge=min_value, le=max_value)) 142 | 143 | 144 | def test_constraint_bounds_handling() -> None: 145 | result = handle_constrained_int(create_constrained_field(ge=100, le=100)) 146 | assert result == 100 147 | 148 | result = handle_constrained_int(create_constrained_field(gt=100, lt=102)) 149 | assert result == 101 150 | 151 | result = handle_constrained_int(create_constrained_field(gt=100, le=101)) 152 | assert result == 101 153 | 154 | with pytest.raises(ParameterError): 155 | result = handle_constrained_int(create_constrained_field(gt=100, lt=101)) 156 | 157 | with pytest.raises(ParameterError): 158 | result = handle_constrained_int(create_constrained_field(ge=100, le=99)) 159 | -------------------------------------------------------------------------------- /tests/constraints/test_list_constraints.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | 3 | import pytest 4 | from hypothesis import given 5 | from hypothesis.strategies import integers 6 | from pydantic import BaseConfig, ConstrainedList 7 | from pydantic.fields import ModelField 8 | 9 | from pydantic_factories import ModelFactory 10 | from pydantic_factories.constraints.collection import handle_constrained_collection 11 | from pydantic_factories.exceptions import ParameterError 12 | 13 | 14 | def create_model_field( 15 | item_type: Any, 16 | min_items: Optional[int] = None, 17 | max_items: Optional[int] = None, 18 | unique_items: Optional[bool] = None, 19 | ) -> ModelField: 20 | field = ConstrainedList() 21 | field.min_items = min_items 22 | field.max_items = max_items 23 | field.item_type = item_type 24 | field.unique_items = unique_items 25 | model_field = ModelField(name="", class_validators={}, model_config=BaseConfig, type_=item_type) 26 | model_field.outer_type_ = field 27 | return model_field 28 | 29 | 30 | @given( 31 | integers(min_value=0, max_value=10), 32 | integers(min_value=0, max_value=10), 33 | ) 34 | def test_handle_constrained_list_with_min_items_and_max_items(min_items: int, max_items: int) -> None: 35 | if max_items >= min_items: 36 | field = create_model_field(str, min_items=min_items, max_items=max_items) 37 | result = handle_constrained_collection(collection_type=list, model_field=field, model_factory=ModelFactory) 38 | assert len(result) >= min_items 39 | assert len(result) <= max_items 40 | else: 41 | field = create_model_field(str, min_items=min_items, max_items=max_items) 42 | with pytest.raises(ParameterError): 43 | handle_constrained_collection(collection_type=list, model_field=field, model_factory=ModelFactory) 44 | 45 | 46 | @given( 47 | integers(min_value=0, max_value=10), 48 | ) 49 | def test_handle_constrained_list_with_max_items( 50 | max_items: int, 51 | ) -> None: 52 | field = create_model_field(str, max_items=max_items) 53 | result = handle_constrained_collection(collection_type=list, model_field=field, model_factory=ModelFactory) 54 | assert len(result) <= max_items 55 | 56 | 57 | @given( 58 | integers(min_value=0, max_value=10), 59 | ) 60 | def test_handle_constrained_list_with_min_items( 61 | min_items: int, 62 | ) -> None: 63 | field = create_model_field(str, min_items=min_items) 64 | result = handle_constrained_collection(collection_type=list, model_field=field, model_factory=ModelFactory) 65 | assert len(result) >= min_items 66 | 67 | 68 | def test_handle_constrained_list_with_different_types() -> None: 69 | for t_type in ModelFactory.get_provider_map(): 70 | field = create_model_field(t_type, min_items=1) 71 | result = handle_constrained_collection(collection_type=list, model_field=field, model_factory=ModelFactory) 72 | assert len(result) > 0 73 | 74 | 75 | def test_handle_unique_items() -> None: 76 | field = create_model_field(bool, min_items=2, unique_items=True) 77 | result = handle_constrained_collection(collection_type=list, model_field=field, model_factory=ModelFactory) 78 | assert len(result) == 2 79 | assert len(set(result)) == 2 80 | -------------------------------------------------------------------------------- /tests/constraints/test_set_constraints.py: -------------------------------------------------------------------------------- 1 | from contextlib import suppress 2 | from typing import Any, Optional 3 | 4 | import pytest 5 | from hypothesis import given 6 | from hypothesis.strategies import integers 7 | from pydantic import BaseConfig, ConstrainedSet 8 | from pydantic.fields import ModelField 9 | 10 | from pydantic_factories import ModelFactory 11 | from pydantic_factories.constraints.collection import handle_constrained_collection 12 | from pydantic_factories.exceptions import ParameterError 13 | 14 | 15 | def create_model_field( 16 | item_type: Any, 17 | min_items: Optional[int] = None, 18 | max_items: Optional[int] = None, 19 | ) -> ModelField: 20 | field = ConstrainedSet() 21 | field.min_items = min_items 22 | field.max_items = max_items 23 | field.item_type = item_type 24 | model_field = ModelField(name="", class_validators={}, model_config=BaseConfig, type_=item_type) 25 | model_field.outer_type_ = field 26 | return model_field 27 | 28 | 29 | @given( 30 | integers(min_value=0, max_value=10), 31 | integers(min_value=0, max_value=10), 32 | ) 33 | def test_handle_constrained_set_with_min_items_and_max_items(min_items: int, max_items: int) -> None: 34 | if max_items >= min_items: 35 | field = create_model_field(str, min_items=min_items, max_items=max_items) 36 | result = handle_constrained_collection(collection_type=set, model_field=field, model_factory=ModelFactory) 37 | assert len(result) >= min_items 38 | assert len(result) <= max_items 39 | else: 40 | field = create_model_field(str, min_items=min_items, max_items=max_items) 41 | with pytest.raises(ParameterError): 42 | handle_constrained_collection(collection_type=set, model_field=field, model_factory=ModelFactory) 43 | 44 | 45 | @given( 46 | integers(min_value=0, max_value=10), 47 | ) 48 | def test_handle_constrained_set_with_max_items( 49 | max_items: int, 50 | ) -> None: 51 | field = create_model_field(str, max_items=max_items) 52 | result = handle_constrained_collection(collection_type=set, model_field=field, model_factory=ModelFactory) 53 | assert len(result) <= max_items 54 | 55 | 56 | @given( 57 | integers(min_value=0, max_value=10), 58 | ) 59 | def test_handle_constrained_set_with_min_items( 60 | min_items: int, 61 | ) -> None: 62 | field = create_model_field(str, min_items=min_items) 63 | result = handle_constrained_collection(collection_type=set, model_field=field, model_factory=ModelFactory) 64 | assert len(result) >= min_items 65 | 66 | 67 | def test_handle_constrained_set_with_different_types() -> None: 68 | with suppress(ParameterError): 69 | for t_type in ModelFactory.get_provider_map(): 70 | field = create_model_field(t_type, min_items=1) 71 | result = handle_constrained_collection(collection_type=set, model_field=field, model_factory=ModelFactory) 72 | assert len(result) > 0 73 | -------------------------------------------------------------------------------- /tests/constraints/test_string_constraints.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Optional 3 | 4 | import pytest 5 | from hypothesis import given, settings 6 | from hypothesis.strategies import booleans, integers 7 | from pydantic import ConstrainedStr 8 | 9 | from pydantic_factories.constraints.strings import handle_constrained_string 10 | from pydantic_factories.exceptions import ParameterError 11 | 12 | 13 | def create_constrained_field( 14 | to_lower: bool, min_length: Optional[int] = None, max_length: Optional[int] = None 15 | ) -> ConstrainedStr: 16 | field = ConstrainedStr() 17 | field.max_length = max_length 18 | field.min_length = min_length 19 | field.to_lower = to_lower 20 | return field 21 | 22 | 23 | REGEXES = [ 24 | r"(a|b|c)xz", 25 | r"a|b", 26 | r"[0-9]{2,4}", 27 | r"a{2,3}", 28 | r"ma?n", 29 | r"ma+n", 30 | r"ma*n", 31 | r"a$", 32 | r"\Athe", 33 | r"\bfoo", 34 | r"foo\b", 35 | r"\Bfoo", 36 | r"foo\B", 37 | ] 38 | 39 | 40 | @settings(deadline=600) 41 | @given(booleans(), integers(min_value=5, max_value=100), integers(min_value=5, max_value=100)) 42 | def test_handle_constrained_string_with_min_length_and_max_length_and_regex( 43 | to_lower: bool, min_length: int, max_length: int 44 | ) -> None: 45 | field = create_constrained_field(to_lower=to_lower, min_length=min_length, max_length=max_length) 46 | if min_length < 0 or max_length < 0 or min_length > max_length: 47 | with pytest.raises(ParameterError): 48 | handle_constrained_string(field=field, random_seed=None) 49 | else: 50 | for regex in REGEXES: 51 | field.regex = regex 52 | result = handle_constrained_string(field=field, random_seed=None) 53 | if to_lower: 54 | assert result == result.lower() 55 | match = re.search(regex, result) 56 | 57 | if match: 58 | assert match.group(0) 59 | assert len(result) >= min_length 60 | assert len(result) <= max_length 61 | 62 | 63 | @given(booleans(), integers(max_value=10000), integers(max_value=10000)) 64 | def test_handle_constrained_string_with_min_length_and_max_length( 65 | to_lower: bool, min_length: int, max_length: int 66 | ) -> None: 67 | field = create_constrained_field(to_lower=to_lower, min_length=min_length, max_length=max_length) 68 | if min_length < 0 or max_length < 0 or min_length > max_length: 69 | with pytest.raises(ParameterError): 70 | handle_constrained_string(field=field, random_seed=None) 71 | else: 72 | result = handle_constrained_string(field=field, random_seed=None) 73 | if to_lower: 74 | assert result == result.lower() 75 | assert len(result) >= min_length 76 | assert len(result) <= max_length 77 | 78 | 79 | @given(booleans(), integers(max_value=10000)) 80 | def test_handle_constrained_string_with_min_length(to_lower: bool, min_length: int) -> None: 81 | field = create_constrained_field(to_lower=to_lower, min_length=min_length) 82 | if min_length < 0: 83 | with pytest.raises(ParameterError): 84 | handle_constrained_string(field=field, random_seed=None) 85 | else: 86 | result = handle_constrained_string(field=field, random_seed=None) 87 | if to_lower: 88 | assert result == result.lower() 89 | assert len(result) >= min_length 90 | 91 | 92 | @given(booleans(), integers(max_value=10000)) 93 | def test_handle_constrained_string_with_max_length(to_lower: bool, max_length: int) -> None: 94 | field = create_constrained_field(to_lower=to_lower, max_length=max_length) 95 | if max_length < 0: 96 | with pytest.raises(ParameterError): 97 | handle_constrained_string(field=field, random_seed=None) 98 | else: 99 | result = handle_constrained_string(field=field, random_seed=None) 100 | if to_lower: 101 | assert result == result.lower() 102 | assert len(result) <= max_length 103 | -------------------------------------------------------------------------------- /tests/extensions/test_beanie_extension.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, List 2 | 3 | import pytest 4 | 5 | try: 6 | import pymongo 7 | from beanie import Document, Link, init_beanie 8 | from beanie.odm.fields import Indexed, PydanticObjectId 9 | from motor.motor_asyncio import AsyncIOMotorClient 10 | 11 | from pydantic_factories.extensions import BeanieDocumentFactory 12 | except ImportError: 13 | pytest.skip(allow_module_level=True) 14 | 15 | # mongo can be run locally or using the docker-compose file at the repository's root 16 | 17 | mongo_dsn = "mongodb://localhost:27017" 18 | 19 | 20 | @pytest.fixture() 21 | def mongo_connection() -> AsyncIOMotorClient: 22 | return AsyncIOMotorClient(mongo_dsn) 23 | 24 | 25 | class MyDocument(Document): 26 | id: PydanticObjectId 27 | name: str 28 | index: Indexed(str, pymongo.DESCENDING) # type: ignore 29 | siblings: List[PydanticObjectId] 30 | 31 | 32 | class MyOtherDocument(Document): 33 | id: PydanticObjectId 34 | document: Link[MyDocument] 35 | 36 | 37 | class MyFactory(BeanieDocumentFactory): 38 | __model__ = MyDocument 39 | 40 | 41 | class MyOtherFactory(BeanieDocumentFactory): 42 | __model__ = MyOtherDocument 43 | 44 | 45 | @pytest.fixture() 46 | async def beanie_init(mongo_connection: AsyncIOMotorClient): 47 | await init_beanie(database=mongo_connection.db_name, document_models=[MyDocument, MyOtherDocument]) 48 | 49 | 50 | @pytest.mark.asyncio() 51 | async def test_handling_of_beanie_types(beanie_init: Callable) -> None: 52 | result = MyFactory.build() 53 | assert result.name 54 | assert result.index 55 | assert isinstance(result.index, str) 56 | 57 | 58 | @pytest.mark.asyncio() 59 | async def test_beanie_persistence_of_single_instance(beanie_init: Callable) -> None: 60 | result = await MyFactory.create_async() 61 | assert result.id 62 | assert result.name 63 | assert result.index 64 | assert isinstance(result.index, str) 65 | 66 | 67 | @pytest.mark.asyncio() 68 | async def test_beanie_persistence_of_multiple_instances(beanie_init: Callable) -> None: 69 | result = await MyFactory.create_batch_async(size=3) 70 | assert len(result) == 3 71 | for instance in result: 72 | assert instance.id 73 | assert instance.name 74 | assert instance.index 75 | assert isinstance(instance.index, str) 76 | 77 | 78 | @pytest.mark.asyncio() 79 | async def test_beanie_links(beanie_init: Callable) -> None: 80 | result = await MyOtherFactory.create_async() 81 | assert isinstance(result.document, MyDocument) 82 | -------------------------------------------------------------------------------- /tests/extensions/test_odmantic_extension.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from uuid import UUID 3 | 4 | import pytest 5 | 6 | try: 7 | from odmantic import AIOEngine, EmbeddedModel, Model 8 | 9 | from pydantic_factories.extensions.odmantic_odm import OdmanticModelFactory 10 | except ImportError: 11 | pytest.skip(allow_module_level=True) 12 | 13 | 14 | class OtherEmbeddedDocument(EmbeddedModel): 15 | name: str 16 | serial: UUID 17 | 18 | 19 | class MyEmbeddedDocument(EmbeddedModel): 20 | name: str 21 | serial: UUID 22 | other_embedded_document: OtherEmbeddedDocument 23 | 24 | 25 | class MyModel(Model): 26 | name: str 27 | embedded: MyEmbeddedDocument 28 | embedded_list: List[MyEmbeddedDocument] 29 | 30 | 31 | @pytest.fixture() 32 | async def odmantic_engine(mongo_connection) -> AIOEngine: 33 | return AIOEngine(motor_client=mongo_connection, database=mongo_connection.db_name) 34 | 35 | 36 | def test_handles_odmantic_models() -> None: 37 | class MyFactory(OdmanticModelFactory): 38 | __model__ = MyModel 39 | 40 | result = MyFactory.build() 41 | 42 | assert result.name 43 | assert result.embedded 44 | assert result.embedded_list 45 | -------------------------------------------------------------------------------- /tests/extensions/test_ormar_extension.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from enum import Enum 3 | from uuid import uuid4 4 | 5 | import pytest 6 | from pydantic import UUID4 7 | 8 | try: 9 | import sqlalchemy 10 | from databases import Database 11 | from ormar import UUID, DateTime 12 | from ormar import Enum as OrmarEnum 13 | from ormar import ForeignKey, Integer, Model, String, Text 14 | from sqlalchemy import func 15 | 16 | from pydantic_factories.extensions import OrmarModelFactory 17 | except ImportError: 18 | pytest.skip(allow_module_level=True) 19 | 20 | postgres_dsn = "postgresql+asyncpg://pydantic-factories:pydantic-factories@postgres:5432/pydantic-factories" 21 | 22 | database = Database(url=postgres_dsn, force_rollback=True) 23 | metadata = sqlalchemy.MetaData() 24 | 25 | 26 | class BaseMeta: 27 | metadata = metadata 28 | database = database 29 | 30 | 31 | class Mood(str, Enum): 32 | HAPPY = "happy" 33 | GRUMPY = "grumpy" 34 | 35 | 36 | class Person(Model): 37 | id: int = Integer(autoincrement=True, primary_key=True) 38 | created_at: datetime = DateTime(timezone=True, server_default=func.now()) 39 | updated_at: datetime = DateTime(timezone=True, server_default=func.now(), onupdate=func.now()) 40 | mood: Mood = String(choices=Mood, max_length=20) # type: ignore 41 | 42 | class Meta(BaseMeta): 43 | pass 44 | 45 | 46 | class Job(Model): 47 | id: int = Integer(autoincrement=True, primary_key=True) 48 | person: Person = ForeignKey(Person) 49 | name: str = String(max_length=20) 50 | 51 | class Meta(BaseMeta): 52 | pass 53 | 54 | 55 | class PersonFactory(OrmarModelFactory): 56 | __model__ = Person 57 | 58 | 59 | class JobFactory(OrmarModelFactory): 60 | __model__ = Job 61 | 62 | 63 | def test_person_factory() -> None: 64 | result = PersonFactory.build() 65 | 66 | assert result.id 67 | assert result.created_at 68 | assert result.updated_at 69 | assert result.mood 70 | 71 | 72 | def test_job_factory() -> None: 73 | job_name: str = "Unemployed" 74 | result = JobFactory.build(name=job_name) 75 | 76 | assert result.id 77 | assert result.name == job_name 78 | assert result.person is not None 79 | 80 | 81 | def test_model_creation_after_factory_build() -> None: 82 | # https://github.com/starlite-api/pydantic-factories/issues/128 83 | class TestModel(Model): 84 | class Meta(BaseMeta): 85 | pass 86 | 87 | id: UUID4 = UUID(primary_key=True, default=uuid4) 88 | text: str = Text() 89 | text2: str = Text(nullable=True) 90 | created_date: datetime = DateTime(default=datetime.now) 91 | mood: Mood = OrmarEnum(enum_class=Mood, default=Mood.HAPPY) 92 | 93 | class TestModelFactory(OrmarModelFactory): 94 | __model__ = TestModel 95 | 96 | TestModelFactory.build() 97 | TestModel(text="qwerty") 98 | -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime 2 | from typing import List, Optional, Union 3 | from uuid import uuid4 4 | 5 | from pydantic import UUID4, BaseModel 6 | 7 | from pydantic_factories import ModelFactory 8 | 9 | 10 | class Pet(BaseModel): 11 | name: str 12 | species: str 13 | color: str 14 | sound: str 15 | age: float 16 | 17 | 18 | class Person(BaseModel): 19 | id: UUID4 20 | name: str 21 | hobbies: Optional[List[str]] 22 | nicks: List[str] 23 | age: Union[float, int] 24 | pets: List[Pet] 25 | birthday: Union[datetime, date] 26 | 27 | 28 | class PersonFactoryWithoutDefaults(ModelFactory): 29 | __model__ = Person 30 | 31 | 32 | class PersonFactoryWithDefaults(PersonFactoryWithoutDefaults): 33 | id = uuid4() 34 | name = "moishe" 35 | hobbies = ["fishing"] 36 | nicks: List[str] = [] 37 | age = 33 38 | pets: List[Pet] = [] 39 | birthday = datetime(2021 - 33, 1, 1) 40 | 41 | 42 | class PetFactory(ModelFactory): 43 | __model__ = Pet 44 | -------------------------------------------------------------------------------- /tests/plugins/test_pytest_plugin.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | from pydantic import BaseModel 5 | 6 | from pydantic_factories import ModelFactory 7 | from pydantic_factories.exceptions import ParameterError 8 | from pydantic_factories.fields import Fixture 9 | from pydantic_factories.plugins.pytest_plugin import register_fixture 10 | from tests.models import Person, PersonFactoryWithoutDefaults 11 | 12 | 13 | @register_fixture 14 | class PersonFactoryFixture(PersonFactoryWithoutDefaults): 15 | """Person Factory Fixture.""" 16 | 17 | 18 | @register_fixture(name="another_fixture") 19 | class AnotherPersonFactoryFixture(PersonFactoryWithoutDefaults): 20 | """Another Person Factory Fixture.""" 21 | 22 | 23 | def test_fixture_register_decorator(person_factory_fixture: PersonFactoryFixture) -> None: 24 | person = person_factory_fixture.build() 25 | assert isinstance(person, Person) 26 | 27 | 28 | def test_custom_naming_fixture_register_decorator(another_fixture: AnotherPersonFactoryFixture) -> None: 29 | person = another_fixture.build() 30 | assert isinstance(person, Person) 31 | 32 | 33 | def test_register_with_function_error() -> None: 34 | with pytest.raises(ParameterError): 35 | 36 | @register_fixture # type: ignore 37 | def foo() -> None: 38 | pass 39 | 40 | 41 | def test_register_with_class_not_model_factory_error() -> None: 42 | with pytest.raises(ParameterError): 43 | 44 | @register_fixture # type: ignore 45 | class Foo: 46 | pass 47 | 48 | 49 | def test_using_a_fixture_as_field_value() -> None: 50 | class MyModel(BaseModel): 51 | best_friend: Person 52 | all_friends: List[Person] 53 | 54 | class MyFactory(ModelFactory[MyModel]): 55 | __model__ = MyModel 56 | 57 | best_friend = Fixture(PersonFactoryFixture, name="mike") 58 | all_friends = Fixture(PersonFactoryFixture, size=5) 59 | 60 | result = MyFactory.build() 61 | assert result.best_friend.name == "mike" 62 | assert len(result.all_friends) == 5 63 | 64 | 65 | def test_using_non_fixture_with_the_fixture_field_raises() -> None: 66 | class MyModel(BaseModel): 67 | best_friend: Person 68 | all_friends: List[Person] 69 | 70 | class MyFactory(ModelFactory[MyModel]): 71 | __model__ = MyModel 72 | 73 | best_friend = Fixture(PersonFactoryFixture, name="mike") 74 | all_friends = Fixture(123) # type: ignore 75 | 76 | with pytest.raises(ParameterError): 77 | MyFactory.build() 78 | -------------------------------------------------------------------------------- /tests/test_complex_types.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import ( 3 | Any, 4 | DefaultDict, 5 | Deque, 6 | Dict, 7 | FrozenSet, 8 | Iterable, 9 | List, 10 | Mapping, 11 | Optional, 12 | Sequence, 13 | Set, 14 | Tuple, 15 | Union, 16 | ) 17 | 18 | import pytest 19 | from pydantic import BaseModel 20 | 21 | from pydantic_factories import ModelFactory 22 | from pydantic_factories.exceptions import ParameterError 23 | from tests.models import Person 24 | 25 | 26 | def test_handles_complex_typing() -> None: 27 | class MyModel(BaseModel): 28 | nested_dict: Dict[str, Dict[Union[int, str], Dict[Any, List[Dict[str, str]]]]] 29 | dict_str_any: Dict[str, Any] 30 | nested_list: List[List[List[Dict[str, List[Any]]]]] 31 | sequence_dict: Sequence[Dict] 32 | iterable_float: Iterable[float] 33 | tuple_ellipsis: Tuple[int, ...] 34 | tuple_str_str: Tuple[str, str] 35 | default_dict: DefaultDict[str, List[Dict[str, int]]] 36 | deque: Deque[List[Dict[str, int]]] 37 | set_union: Set[Union[str, int]] 38 | frozen_set: FrozenSet[str] 39 | 40 | class MyFactory(ModelFactory): 41 | __model__ = MyModel 42 | 43 | result = MyFactory.build() 44 | assert result.nested_dict 45 | assert result.dict_str_any 46 | assert result.nested_list 47 | assert result.sequence_dict 48 | assert result.iterable_float 49 | assert result.tuple_ellipsis 50 | assert result.tuple_str_str 51 | assert result.default_dict 52 | assert result.deque 53 | assert result.set_union 54 | assert result.frozen_set 55 | 56 | 57 | def test_handles_complex_typing_with_embedded_models() -> None: 58 | class MyModel(BaseModel): 59 | person_dict: Dict[str, Person] 60 | person_list: List[Person] 61 | 62 | class MyFactory(ModelFactory): 63 | __model__ = MyModel 64 | 65 | result = MyFactory.build() 66 | 67 | assert result.person_dict 68 | assert result.person_list[0].pets 69 | 70 | 71 | def test_raises_for_user_defined_types() -> None: 72 | class MyClass: 73 | def __init__(self, value: int): 74 | self.value = value 75 | 76 | class MyModel(BaseModel): 77 | my_class_field: Dict[str, MyClass] 78 | 79 | class Config: 80 | arbitrary_types_allowed = True 81 | 82 | class MyFactory(ModelFactory): 83 | __model__ = MyModel 84 | 85 | with pytest.raises(ParameterError): 86 | MyFactory.build() 87 | 88 | 89 | def test_randomizes_optional_returns() -> None: 90 | """this is a flaky test - because it depends on randomness, hence it's been re-ran multiple times.""" 91 | 92 | class MyModel(BaseModel): 93 | optional_1: List[Optional[str]] 94 | optional_2: Dict[str, Optional[str]] 95 | optional_3: Set[Optional[str]] 96 | optional_4: Mapping[int, Optional[str]] 97 | 98 | class MyFactory(ModelFactory): 99 | __model__ = MyModel 100 | 101 | failed = False 102 | for _ in range(5): 103 | try: 104 | result = MyFactory.build() 105 | assert any( 106 | [ 107 | not result.optional_1, 108 | not result.optional_2, 109 | not result.optional_3, 110 | not result.optional_4, 111 | ] 112 | ) 113 | assert any( 114 | [ 115 | bool(result.optional_1), 116 | bool(result.optional_2), 117 | bool(result.optional_3), 118 | bool(result.optional_4), 119 | ] 120 | ) 121 | failed = False 122 | break 123 | except AssertionError: 124 | failed = True 125 | assert not failed 126 | 127 | 128 | def test_complex_typing_with_enum() -> None: 129 | class Animal(str, Enum): 130 | DOG = "Dog" 131 | CAT = "Cat" 132 | MONKEY = "Monkey" 133 | 134 | class MyModel(BaseModel): 135 | animal_list: List[Animal] 136 | 137 | class MyFactory(ModelFactory): 138 | __model__ = MyModel 139 | 140 | result = MyFactory.build() 141 | assert result.animal_list 142 | -------------------------------------------------------------------------------- /tests/test_constrained_attribute_parsing.py: -------------------------------------------------------------------------------- 1 | import re 2 | from decimal import Decimal 3 | from typing import Dict, List, Tuple 4 | 5 | from pydantic import ( 6 | BaseModel, 7 | ConstrainedBytes, 8 | ConstrainedDecimal, 9 | ConstrainedFloat, 10 | ConstrainedInt, 11 | ConstrainedStr, 12 | Field, 13 | conbytes, 14 | condecimal, 15 | confloat, 16 | confrozenset, 17 | conint, 18 | conlist, 19 | conset, 20 | constr, 21 | ) 22 | 23 | from pydantic_factories import ModelFactory 24 | from tests.models import Person 25 | 26 | pattern = r"(a|b|c)zz" 27 | 28 | 29 | def test_constrained_attribute_parsing() -> None: 30 | class ConstrainedModel(BaseModel): 31 | conbytes_field: conbytes() # type: ignore[valid-type] 32 | condecimal_field: condecimal() # type: ignore[valid-type] 33 | confloat_field: confloat() # type: ignore[valid-type] 34 | conint_field: conint() # type: ignore[valid-type] 35 | conlist_field: conlist(str, min_items=5, max_items=10) # type: ignore[valid-type] 36 | conset_field: conset(str, min_items=5, max_items=10) # type: ignore[valid-type] 37 | confrozenset_field: confrozenset(str, min_items=5, max_items=10) # type: ignore[valid-type] 38 | constr_field: constr(to_lower=True) # type: ignore[valid-type] 39 | str_field1: str = Field(min_length=11) 40 | str_field2: str = Field(max_length=11) 41 | str_field3: str = Field(min_length=8, max_length=11, regex=pattern) 42 | int_field: int = Field(gt=1, multiple_of=5) 43 | float_field: float = Field(gt=100, lt=1000) 44 | decimal_field: Decimal = Field(ge=100, le=1000) 45 | list_field: List[str] = Field(min_items=1, max_items=10) 46 | constant_field: int = Field(const=True, default=100) 47 | 48 | class MyFactory(ModelFactory): 49 | __model__ = ConstrainedModel 50 | 51 | result = MyFactory.build() 52 | 53 | assert isinstance(result.conbytes_field, bytes) 54 | assert isinstance(result.conint_field, int) 55 | assert isinstance(result.confloat_field, float) 56 | assert isinstance(result.condecimal_field, Decimal) 57 | assert isinstance(result.conlist_field, list) 58 | assert isinstance(result.conset_field, set) 59 | assert isinstance(result.confrozenset_field, frozenset) 60 | assert isinstance(result.str_field1, str) 61 | assert isinstance(result.constr_field, str) 62 | assert len(result.conlist_field) >= 5 63 | assert len(result.conlist_field) <= 10 64 | assert len(result.conset_field) >= 5 65 | assert len(result.conset_field) <= 10 66 | assert len(result.confrozenset_field) >= 5 67 | assert len(result.confrozenset_field) <= 10 68 | assert result.constr_field.lower() == result.constr_field 69 | assert len(result.str_field1) >= 11 70 | assert len(result.str_field2) <= 11 71 | assert len(result.str_field3) >= 8 72 | assert len(result.str_field3) <= 11 73 | match = re.search(pattern, result.str_field3) 74 | assert match 75 | assert match.group(0) 76 | assert result.int_field >= 1 77 | assert result.int_field % 5 == 0 78 | assert result.float_field > 100 79 | assert result.float_field < 1000 80 | assert result.decimal_field > 100 81 | assert result.decimal_field < 1000 82 | assert len(result.list_field) >= 1 83 | assert len(result.list_field) <= 10 84 | assert all([isinstance(r, str) for r in result.list_field]) 85 | assert result.constant_field == 100 86 | 87 | 88 | def test_complex_constrained_attribute_parsing() -> None: 89 | class MyModel(BaseModel): 90 | conlist_with_model_field: conlist(Person, min_items=3) # type: ignore[valid-type] 91 | conlist_with_complex_type: conlist(Dict[str, Tuple[Person, Person, Person]], min_items=1) # type: ignore[valid-type] 92 | 93 | class MyFactory(ModelFactory): 94 | __model__ = MyModel 95 | 96 | result = MyFactory.build() 97 | 98 | assert len(result.conlist_with_model_field) >= 3 99 | assert all([isinstance(v, Person) for v in result.conlist_with_model_field]) 100 | assert result.conlist_with_complex_type 101 | assert isinstance(result.conlist_with_complex_type[0], dict) 102 | assert isinstance(list(result.conlist_with_complex_type[0].values())[0], tuple) 103 | assert len(list(result.conlist_with_complex_type[0].values())[0]) == 3 104 | assert all([isinstance(v, Person) for v in list(result.conlist_with_complex_type[0].values())[0]]) 105 | 106 | 107 | def test_nested_constrained_attribute_handling() -> None: 108 | # subclassing the constrained fields is not documented by pydantic, but is supported apparently 109 | class MyConstrainedString(ConstrainedStr): 110 | regex = re.compile("^vpc-.*$") 111 | 112 | class MyConstrainedBytes(ConstrainedBytes): 113 | min_length = 11 114 | 115 | class MyConstrainedInt(ConstrainedInt): 116 | ge = 11 117 | 118 | class MyConstrainedFloat(ConstrainedFloat): 119 | ge = 11.0 120 | 121 | class MyConstrainedDecimal(ConstrainedDecimal): 122 | ge = Decimal("11.0") 123 | 124 | class MyModel(BaseModel): 125 | conbytes_list_field: List[conbytes()] # type: ignore[valid-type] 126 | condecimal_list_field: List[condecimal()] # type: ignore[valid-type] 127 | confloat_list_field: List[confloat()] # type: ignore[valid-type] 128 | conint_list_field: List[conint()] # type: ignore[valid-type] 129 | conlist_list_field: List[conlist(str)] # type: ignore[valid-type] 130 | conset_list_field: List[conset(str)] # type: ignore[valid-type] 131 | constr_list_field: List[constr(to_lower=True)] # type: ignore[valid-type] 132 | 133 | my_bytes_list_field: List[MyConstrainedBytes] 134 | my_decimal_list_field: List[MyConstrainedDecimal] 135 | my_float_list_field: List[MyConstrainedFloat] 136 | my_int_list_field: List[MyConstrainedInt] 137 | my_str_list_field: List[MyConstrainedString] 138 | 139 | class MyFactory(ModelFactory): 140 | __model__ = MyModel 141 | 142 | result = MyFactory.build() 143 | 144 | assert result.conbytes_list_field 145 | assert result.condecimal_list_field 146 | assert result.confloat_list_field 147 | assert result.conint_list_field 148 | assert result.conlist_list_field 149 | assert result.conset_list_field 150 | assert result.constr_list_field 151 | 152 | assert result.my_bytes_list_field 153 | assert result.my_decimal_list_field 154 | assert result.my_float_list_field 155 | assert result.my_int_list_field 156 | assert result.my_str_list_field 157 | -------------------------------------------------------------------------------- /tests/test_data_parsing.py: -------------------------------------------------------------------------------- 1 | from collections import Counter, deque 2 | from datetime import date, datetime, time, timedelta 3 | from decimal import Decimal 4 | from enum import Enum 5 | from ipaddress import ( 6 | IPv4Address, 7 | IPv4Interface, 8 | IPv4Network, 9 | IPv6Address, 10 | IPv6Interface, 11 | IPv6Network, 12 | ) 13 | from pathlib import Path 14 | from typing import Callable, Literal 15 | from uuid import UUID 16 | 17 | import pytest 18 | from pydantic import ( 19 | UUID1, 20 | UUID3, 21 | UUID4, 22 | UUID5, 23 | AmqpDsn, 24 | AnyHttpUrl, 25 | AnyUrl, 26 | BaseConfig, 27 | BaseModel, 28 | ByteSize, 29 | DirectoryPath, 30 | EmailStr, 31 | Field, 32 | FilePath, 33 | FutureDate, 34 | HttpUrl, 35 | IPvAnyAddress, 36 | IPvAnyInterface, 37 | IPvAnyNetwork, 38 | Json, 39 | KafkaDsn, 40 | NameEmail, 41 | NegativeFloat, 42 | NegativeInt, 43 | NonNegativeInt, 44 | NonPositiveFloat, 45 | PastDate, 46 | PaymentCardNumber, 47 | PositiveFloat, 48 | PositiveInt, 49 | PostgresDsn, 50 | PyObject, 51 | RedisDsn, 52 | SecretBytes, 53 | SecretStr, 54 | StrictBool, 55 | StrictBytes, 56 | StrictFloat, 57 | StrictInt, 58 | StrictStr, 59 | ) 60 | from pydantic.color import Color 61 | 62 | from pydantic_factories import ModelFactory 63 | from pydantic_factories.exceptions import ParameterError 64 | from tests.models import Person, PersonFactoryWithDefaults, Pet 65 | 66 | 67 | def test_enum_parsing() -> None: 68 | class MyStrEnum(str, Enum): 69 | FIRST_NAME = "Moishe Zuchmir" 70 | SECOND_NAME = "Hannah Arendt" 71 | 72 | class MyIntEnum(Enum): 73 | ONE_HUNDRED = 100 74 | TWO_HUNDRED = 200 75 | 76 | class MyModel(BaseModel): 77 | name: MyStrEnum 78 | worth: MyIntEnum 79 | 80 | class MyFactory(ModelFactory): 81 | __model__ = MyModel 82 | 83 | result = MyFactory.build() 84 | 85 | assert isinstance(result.name, MyStrEnum) 86 | assert isinstance(result.worth, MyIntEnum) 87 | 88 | 89 | def test_callback_parsing() -> None: 90 | today = date.today() 91 | 92 | class MyModel(BaseModel): 93 | name: str 94 | birthday: date 95 | secret: Callable 96 | 97 | class MyFactory(ModelFactory): 98 | __model__ = MyModel 99 | 100 | name = lambda: "moishe zuchmir" # noqa: E731 101 | birthday = lambda: today # noqa: E731 102 | 103 | result = MyFactory.build() 104 | 105 | assert result.name == "moishe zuchmir" 106 | assert result.birthday == today 107 | assert callable(result.secret) 108 | 109 | 110 | def test_alias_parsing() -> None: 111 | class MyModel(BaseModel): 112 | aliased_field: str = Field(alias="special_field") 113 | 114 | class MyFactory(ModelFactory): 115 | __model__ = MyModel 116 | 117 | assert isinstance(MyFactory.build().aliased_field, str) 118 | 119 | 120 | def test_literal_parsing() -> None: 121 | class MyModel(BaseModel): 122 | literal_field: "Literal['yoyos']" 123 | multi_literal_field: "Literal['nolos', 'zozos', 'kokos']" 124 | 125 | class MyFactory(ModelFactory): 126 | __model__ = MyModel 127 | 128 | assert MyFactory.build().literal_field == "yoyos" 129 | batch = MyFactory.batch(30) 130 | values = {v.multi_literal_field for v in batch} 131 | assert values == {"nolos", "zozos", "kokos"} 132 | 133 | 134 | def test_embedded_models_parsing() -> None: 135 | class MyModel(BaseModel): 136 | pet: Pet 137 | 138 | class MyFactory(ModelFactory): 139 | __model__ = MyModel 140 | 141 | result = MyFactory.build() 142 | assert isinstance(result.pet, Pet) 143 | 144 | 145 | def test_embedded_factories_parsing() -> None: 146 | class MyModel(BaseModel): 147 | person: Person 148 | 149 | class MyFactory(ModelFactory): 150 | __model__ = MyModel 151 | person = PersonFactoryWithDefaults 152 | 153 | result = MyFactory.build() 154 | assert isinstance(result.person, Person) 155 | 156 | 157 | def test_type_property_parsing() -> None: 158 | class MyModel(BaseModel): 159 | object_field: object 160 | float_field: float 161 | int_field: int 162 | bool_field: bool 163 | str_field: str 164 | bytes_field: bytes 165 | # built-in objects 166 | dict_field: dict 167 | tuple_field: tuple 168 | list_field: list 169 | set_field: set 170 | frozenset_field: frozenset 171 | deque_field: deque 172 | # standard library objects 173 | Path_field: Path 174 | Decimal_field: Decimal 175 | UUID_field: UUID 176 | # datetime 177 | datetime_field: datetime 178 | date_field: date 179 | time_field: time 180 | timedelta_field: timedelta 181 | # ip addresses 182 | IPv4Address_field: IPv4Address 183 | IPv4Interface_field: IPv4Interface 184 | IPv4Network_field: IPv4Network 185 | IPv6Address_field: IPv6Address 186 | IPv6Interface_field: IPv6Interface 187 | IPv6Network_field: IPv6Network 188 | # types 189 | Callable_field: Callable 190 | # pydantic specific 191 | ByteSize_pydantic_type: ByteSize 192 | PositiveInt_pydantic_type: PositiveInt 193 | FilePath_pydantic_type: FilePath 194 | NegativeFloat_pydantic_type: NegativeFloat 195 | NegativeInt_pydantic_type: NegativeInt 196 | PositiveFloat_pydantic_type: PositiveFloat 197 | NonPositiveFloat_pydantic_type: NonPositiveFloat 198 | NonNegativeInt_pydantic_type: NonNegativeInt 199 | StrictInt_pydantic_type: StrictInt 200 | StrictBool_pydantic_type: StrictBool 201 | StrictBytes_pydantic_type: StrictBytes 202 | StrictFloat_pydantic_type: StrictFloat 203 | StrictStr_pydantic_type: StrictStr 204 | DirectoryPath_pydantic_type: DirectoryPath 205 | EmailStr_pydantic_type: EmailStr 206 | NameEmail_pydantic_type: NameEmail 207 | PyObject_pydantic_type: PyObject 208 | Color_pydantic_type: Color 209 | Json_pydantic_type: Json 210 | PaymentCardNumber_pydantic_type: PaymentCardNumber 211 | AnyUrl_pydantic_type: AnyUrl 212 | AnyHttpUrl_pydantic_type: AnyHttpUrl 213 | HttpUrl_pydantic_type: HttpUrl 214 | PostgresDsn_pydantic_type: PostgresDsn 215 | RedisDsn_pydantic_type: RedisDsn 216 | UUID1_pydantic_type: UUID1 217 | UUID3_pydantic_type: UUID3 218 | UUID4_pydantic_type: UUID4 219 | UUID5_pydantic_type: UUID5 220 | SecretBytes_pydantic_type: SecretBytes 221 | SecretStr_pydantic_type: SecretStr 222 | IPvAnyAddress_pydantic_type: IPvAnyAddress 223 | IPvAnyInterface_pydantic_type: IPvAnyInterface 224 | IPvAnyNetwork_pydantic_type: IPvAnyNetwork 225 | AmqpDsn_pydantic_type: AmqpDsn 226 | KafkaDsn_pydantic_type: KafkaDsn 227 | PastDate_pydantic_type: PastDate 228 | FutureDate_pydantic_type: FutureDate 229 | Counter_pydantic_type: Counter 230 | 231 | class MyFactory(ModelFactory): 232 | __model__ = MyModel 233 | 234 | result = MyFactory.build() 235 | 236 | for key in MyFactory.get_provider_map(): 237 | key_name = key.__name__ if hasattr(key, "__name__") else key._name 238 | if hasattr(result, f"{key_name}_field"): 239 | assert isinstance(getattr(result, f"{key_name}_field"), key) 240 | elif hasattr(result, f"{key_name}_pydantic_type"): 241 | assert getattr(result, f"{key_name}_pydantic_type") is not None 242 | 243 | 244 | def test_class_parsing() -> None: 245 | class TestClassWithoutKwargs: 246 | def __init__(self) -> None: 247 | self.flag = "123" 248 | 249 | class MyModel(BaseModel): 250 | class Config(BaseConfig): 251 | arbitrary_types_allowed = True 252 | 253 | class_field: TestClassWithoutKwargs 254 | # just a few select exceptions, to verify this works 255 | exception_field: Exception 256 | type_error_field: TypeError 257 | attribute_error_field: AttributeError 258 | runtime_error_field: RuntimeError 259 | 260 | class MyFactory(ModelFactory): 261 | __model__ = MyModel 262 | 263 | result = MyFactory.build() 264 | 265 | assert isinstance(result.class_field, TestClassWithoutKwargs) 266 | assert result.class_field.flag == "123" 267 | assert isinstance(result.exception_field, Exception) 268 | assert isinstance(result.type_error_field, TypeError) 269 | assert isinstance(result.attribute_error_field, AttributeError) 270 | assert isinstance(result.runtime_error_field, RuntimeError) 271 | 272 | class TestClassWithKwargs: 273 | def __init__(self, _: str): 274 | self.flag = str 275 | 276 | class MyNewModel(BaseModel): 277 | class Config(BaseConfig): 278 | arbitrary_types_allowed = True 279 | 280 | class_field: TestClassWithKwargs 281 | 282 | class MySecondFactory(ModelFactory): 283 | __model__ = MyNewModel 284 | 285 | with pytest.raises(ParameterError): 286 | MySecondFactory.build() 287 | -------------------------------------------------------------------------------- /tests/test_dataclass.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass as vanilla_dataclass 2 | from dataclasses import field 3 | from typing import Dict, List, Optional 4 | 5 | from pydantic import BaseModel 6 | from pydantic.dataclasses import Field # type: ignore 7 | from pydantic.dataclasses import dataclass as pydantic_dataclass 8 | 9 | from pydantic_factories import ModelFactory 10 | from tests.models import Person 11 | 12 | 13 | def test_factory_vanilla_dc() -> None: 14 | @vanilla_dataclass 15 | class VanillaDC: 16 | id: int 17 | name: str 18 | list_field: List[Dict[str, int]] 19 | field_of_some_value: Optional[int] = field(default_factory=lambda: 0) 20 | 21 | class MyFactory(ModelFactory): 22 | __model__ = VanillaDC 23 | 24 | result = MyFactory.build() 25 | 26 | assert result 27 | assert result.id 28 | assert result.name 29 | assert result.list_field 30 | assert result.list_field[0] 31 | assert [isinstance(value, int) for value in result.list_field[0].values()] 32 | 33 | 34 | def test_factory_pydantic_dc() -> None: 35 | @pydantic_dataclass 36 | class PydanticDC: 37 | id: int 38 | name: str 39 | list_field: List[Dict[str, int]] 40 | field_of_some_value: Optional[int] = field(default_factory=lambda: 0) 41 | constrained_field: int = Field(ge=100) 42 | 43 | class MyFactory(ModelFactory): 44 | __model__ = PydanticDC 45 | 46 | result = MyFactory.build() 47 | 48 | assert result 49 | assert result.id 50 | assert result.name 51 | assert result.list_field 52 | assert result.list_field[0] 53 | assert [isinstance(value, int) for value in result.list_field[0].values()] 54 | assert result.constrained_field >= 100 55 | 56 | 57 | def test_vanilla_dc_with_embedded_model() -> None: 58 | @vanilla_dataclass 59 | class VanillaDC: 60 | people: List[Person] 61 | 62 | class MyFactory(ModelFactory): 63 | __model__ = VanillaDC 64 | 65 | result = MyFactory.build() 66 | 67 | assert result.people 68 | assert [isinstance(person, Person) for person in result.people] 69 | 70 | 71 | def test_pydantic_dc_with_embedded_model() -> None: 72 | @vanilla_dataclass 73 | class PydanticDC: 74 | people: List[Person] 75 | 76 | class MyFactory(ModelFactory): 77 | __model__ = PydanticDC 78 | 79 | result = MyFactory.build() 80 | 81 | assert result.people 82 | assert [isinstance(person, Person) for person in result.people] 83 | 84 | 85 | def test_model_with_embedded_dataclasses() -> None: 86 | @vanilla_dataclass 87 | class VanillaDC: 88 | people: List[Person] 89 | 90 | @vanilla_dataclass 91 | class PydanticDC: 92 | people: List[Person] 93 | 94 | class Crowd(BaseModel): 95 | west: VanillaDC 96 | east: PydanticDC 97 | 98 | class MyFactory(ModelFactory): 99 | __model__ = Crowd 100 | 101 | result = MyFactory.build() 102 | 103 | assert result.west 104 | assert result.west.people 105 | assert result.east 106 | assert result.east.people 107 | 108 | 109 | def function_with_kwargs(first: int, second: float, third: str = "moishe") -> None: 110 | pass 111 | 112 | 113 | def test_complex_embedded_dataclass() -> None: 114 | @vanilla_dataclass 115 | class VanillaDC: 116 | people: List[Person] 117 | 118 | class MyModel(BaseModel): 119 | weirdly_nest_field: List[Dict[str, Dict[str, VanillaDC]]] 120 | 121 | class MyFactory(ModelFactory): 122 | __model__ = MyModel 123 | 124 | result = MyFactory.build() 125 | 126 | assert result.weirdly_nest_field 127 | assert result.weirdly_nest_field[0] 128 | assert list(result.weirdly_nest_field[0].values())[0].values() 129 | assert list(list(result.weirdly_nest_field[0].values())[0].values())[0] 130 | assert isinstance(list(list(result.weirdly_nest_field[0].values())[0].values())[0], VanillaDC) 131 | -------------------------------------------------------------------------------- /tests/test_dicts.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from pydantic import BaseModel 4 | 5 | from pydantic_factories import ModelFactory 6 | 7 | 8 | def test_passing_nested_dict() -> None: 9 | class MyMappedClass(BaseModel): 10 | val: str 11 | 12 | class MyClass(BaseModel): 13 | my_mapping_obj: Dict[str, MyMappedClass] 14 | my_mapping_str: Dict[str, str] 15 | 16 | class MyClassFactory(ModelFactory[MyClass]): 17 | __model__ = MyClass 18 | 19 | obj = MyClassFactory.build( 20 | my_mapping_str={"foo": "bar"}, 21 | my_mapping_obj={"baz": MyMappedClass(val="bar")}, 22 | ) 23 | 24 | assert obj.dict() == {"my_mapping_obj": {"baz": {"val": "bar"}}, "my_mapping_str": {"foo": "bar"}} 25 | -------------------------------------------------------------------------------- /tests/test_discriminated_unions.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, Union 2 | 3 | from pydantic import BaseModel, Field 4 | from typing_extensions import Annotated 5 | 6 | from pydantic_factories import ModelFactory 7 | 8 | 9 | def test_discriminated_unions() -> None: 10 | class BasePet(BaseModel): 11 | name: str 12 | 13 | class BlackCat(BasePet): 14 | pet_type: Literal["cat"] 15 | color: Literal["black"] 16 | 17 | class WhiteCat(BasePet): 18 | pet_type: Literal["cat"] 19 | color: Literal["white"] 20 | 21 | class Dog(BasePet): 22 | pet_type: Literal["dog"] 23 | 24 | class Owner(BaseModel): 25 | pet: Annotated[ 26 | Union[Annotated[Union[BlackCat, WhiteCat], Field(discriminator="color")], Dog], 27 | Field(discriminator="pet_type"), 28 | ] 29 | name: str 30 | 31 | class OwnerFactory(ModelFactory): 32 | __model__ = Owner 33 | 34 | assert OwnerFactory.build() 35 | -------------------------------------------------------------------------------- /tests/test_factory_auto_registration.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass as vanilla_dataclass 2 | from typing import List 3 | 4 | from pydantic import BaseModel 5 | from typing_extensions import TypedDict 6 | 7 | from pydantic_factories import ModelFactory 8 | 9 | 10 | class A(BaseModel): 11 | a_text: str 12 | 13 | 14 | class B(BaseModel): 15 | b_text: str 16 | a: A 17 | 18 | 19 | class C(BaseModel): 20 | b: B 21 | b_list: List[B] 22 | 23 | 24 | def test_auto_register_model_factory() -> None: 25 | class AFactory(ModelFactory): 26 | a_text = "const value" 27 | __model__ = A 28 | 29 | class BFactory(ModelFactory): 30 | b_text = "const value" 31 | __model__ = B 32 | __auto_register__ = True 33 | 34 | class CFactory(ModelFactory): 35 | __model__ = C 36 | 37 | c = CFactory.build() 38 | 39 | assert c.b.b_text == BFactory.b_text 40 | assert c.b_list[0].b_text == BFactory.b_text 41 | assert c.b.a.a_text != AFactory.a_text 42 | 43 | 44 | def test_auto_register_model_factory_using_create_factory() -> None: 45 | const_value = "const value" 46 | ModelFactory.create_factory(model=A, a_text=const_value) 47 | ModelFactory.create_factory(model=B, b_text=const_value, __auto_register__=True) 48 | CFactory = ModelFactory.create_factory(model=C) 49 | 50 | c = CFactory.build() 51 | 52 | assert c.b.b_text == const_value 53 | assert c.b_list[0].b_text == const_value 54 | assert c.b.a.a_text != const_value 55 | 56 | 57 | def test_dataclass_model_factory_auto_registration() -> None: 58 | @vanilla_dataclass 59 | class DataClass: 60 | text: str 61 | 62 | class UpperModel(BaseModel): 63 | nested_field: DataClass 64 | nested_list_field: List[DataClass] 65 | 66 | class UpperModelFactory(ModelFactory): 67 | __model__ = UpperModel 68 | 69 | class DataClassFactory(ModelFactory): 70 | text = "const value" 71 | __model__ = DataClass 72 | __auto_register__ = True 73 | 74 | upper = UpperModelFactory.build() 75 | 76 | assert upper.nested_field.text == DataClassFactory.text 77 | assert upper.nested_list_field[0].text == DataClassFactory.text 78 | 79 | 80 | def test_typeddict_model_factory_auto_registration() -> None: 81 | class TypedDictModel(TypedDict): 82 | text: str 83 | 84 | class UpperSchema(BaseModel): 85 | nested_field: TypedDictModel 86 | nested_list_field: List[TypedDictModel] 87 | 88 | class UpperModelFactory(ModelFactory): 89 | __model__ = UpperSchema 90 | 91 | class TypedDictFactory(ModelFactory): 92 | text = "const value" 93 | __model__ = TypedDictModel 94 | __auto_register__ = True 95 | 96 | upper = UpperModelFactory.build() 97 | 98 | assert upper.nested_field["text"] == TypedDictFactory.text 99 | assert upper.nested_list_field[0]["text"] == TypedDictFactory.text 100 | -------------------------------------------------------------------------------- /tests/test_factory_build.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass as vanilla_dataclass 2 | from uuid import uuid4 3 | 4 | import pytest 5 | from pydantic import BaseModel, Field, ValidationError 6 | 7 | from pydantic_factories import ModelFactory 8 | from pydantic_factories.exceptions import ConfigurationError 9 | from tests.models import PersonFactoryWithDefaults, Pet, PetFactory 10 | 11 | 12 | def test_merges_defaults_with_kwargs() -> None: 13 | first_obj = PersonFactoryWithDefaults.build() 14 | assert first_obj.id == PersonFactoryWithDefaults.id 15 | assert first_obj.name == PersonFactoryWithDefaults.name 16 | assert first_obj.hobbies == PersonFactoryWithDefaults.hobbies 17 | assert first_obj.age == PersonFactoryWithDefaults.age 18 | assert first_obj.pets == PersonFactoryWithDefaults.pets 19 | assert first_obj.birthday == PersonFactoryWithDefaults.birthday 20 | pet = Pet( 21 | name="bluey the blowfish", 22 | species="blowfish", 23 | color="bluish-green", 24 | sound="", 25 | age=1, 26 | ) 27 | kwarg_id_id = uuid4() 28 | kwarg_id_hobbies = ["dancing"] 29 | kwarg_id_age = 35 30 | kwarg_id_pets = [pet] 31 | second_obj = PersonFactoryWithDefaults.build( 32 | id=kwarg_id_id, hobbies=kwarg_id_hobbies, age=kwarg_id_age, pets=kwarg_id_pets 33 | ) 34 | assert second_obj.id == kwarg_id_id 35 | assert second_obj.hobbies == kwarg_id_hobbies 36 | assert second_obj.age == kwarg_id_age 37 | assert second_obj.pets == [pet] 38 | assert second_obj.name == PersonFactoryWithDefaults.name 39 | assert second_obj.birthday == PersonFactoryWithDefaults.birthday 40 | 41 | 42 | def test_respects_none_overrides() -> None: 43 | result = PersonFactoryWithDefaults.build(hobbies=None) 44 | assert result.hobbies is None 45 | 46 | 47 | def test_uses_faker_to_set_values_when_none_available_on_class() -> None: 48 | result = PetFactory.build() 49 | assert isinstance(result.name, str) 50 | assert isinstance(result.species, str) 51 | assert isinstance(result.color, str) 52 | assert isinstance(result.sound, str) 53 | assert isinstance(result.age, float) 54 | 55 | 56 | def test_builds_batch() -> None: 57 | results = PetFactory.batch(10) 58 | assert isinstance(results, list) 59 | assert len(results) == 10 60 | for result in results: 61 | assert isinstance(result.name, str) 62 | assert isinstance(result.species, str) 63 | assert isinstance(result.color, str) 64 | assert isinstance(result.sound, str) 65 | assert isinstance(result.age, float) 66 | 67 | 68 | def test_factory_use_construct() -> None: 69 | invalid_age = "non_valid_age" 70 | non_validated_pet = PetFactory.build(factory_use_construct=True, age=invalid_age) 71 | assert non_validated_pet.age == invalid_age 72 | 73 | with pytest.raises(ValidationError): 74 | PetFactory.build(age=invalid_age) 75 | 76 | with pytest.raises(ValidationError): 77 | PetFactory.build(age=invalid_age) 78 | 79 | @vanilla_dataclass 80 | class VanillaDC: 81 | id: int 82 | 83 | class MyFactory(ModelFactory): 84 | __model__ = VanillaDC 85 | 86 | with pytest.raises(ConfigurationError): 87 | MyFactory.build(factory_use_construct=True) 88 | 89 | 90 | def test_build_instance_by_field_alias_with_allow_population_by_field_name_flag() -> None: 91 | class MyModel(BaseModel): 92 | aliased_field: str = Field(..., alias="special_field") 93 | 94 | class Config: 95 | allow_population_by_field_name = True 96 | 97 | class MyFactory(ModelFactory): 98 | __model__ = MyModel 99 | 100 | instance = MyFactory.build(aliased_field="some") 101 | assert instance.aliased_field == "some" 102 | 103 | 104 | def test_build_instance_by_field_name_with_allow_population_by_field_name_flag() -> None: 105 | class MyModel(BaseModel): 106 | aliased_field: str = Field(..., alias="special_field") 107 | 108 | class Config: 109 | allow_population_by_field_name = True 110 | 111 | class MyFactory(ModelFactory): 112 | __model__ = MyModel 113 | 114 | instance = MyFactory.build(special_field="some") 115 | assert instance.aliased_field == "some" 116 | 117 | 118 | def test_build_model_with_fields_named_like_factory_fields() -> None: 119 | class C(BaseModel): 120 | batch: int 121 | 122 | class CFactory(ModelFactory): 123 | __model__ = C 124 | 125 | assert CFactory.build() 126 | -------------------------------------------------------------------------------- /tests/test_factory_child_models.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Mapping, Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | from pydantic_factories import ModelFactory 6 | 7 | 8 | class Address(BaseModel): 9 | city: str 10 | country: str 11 | 12 | 13 | class Material(BaseModel): 14 | name: str 15 | origin: str 16 | 17 | 18 | class Toy(BaseModel): 19 | name: str 20 | weight: float 21 | materials: List[Material] 22 | 23 | 24 | class Pet(BaseModel): 25 | name: str 26 | age: int 27 | toys: List[Toy] 28 | 29 | 30 | class Person(BaseModel): 31 | name: str 32 | age: int 33 | pets: List[Pet] 34 | address: Address 35 | 36 | 37 | class PersonFactory(ModelFactory): 38 | __model__ = Person 39 | 40 | 41 | def test_factory_child_model_list() -> None: 42 | data = { 43 | "name": "Jean", 44 | "pets": [ 45 | { 46 | "name": "dog", 47 | "toys": [ 48 | { 49 | "name": "ball", 50 | "materials": [{"name": "yarn"}, {"name": "plastic"}], 51 | }, 52 | { 53 | "name": "bone", 54 | }, 55 | ], 56 | }, 57 | { 58 | "name": "cat", 59 | }, 60 | ], 61 | "address": { 62 | "country": "France", 63 | }, 64 | } 65 | 66 | person = PersonFactory.build(**data) # type: ignore 67 | 68 | expected_dict = { 69 | "name": "Jean", 70 | "age": AssertDict.random_int, 71 | "pets": [ 72 | { 73 | "name": "dog", 74 | "age": AssertDict.random_int, 75 | "toys": [ 76 | { 77 | "name": "ball", 78 | "weight": AssertDict.random_float, 79 | "materials": [ 80 | {"name": "yarn", "origin": AssertDict.random_str}, 81 | {"name": "plastic", "origin": AssertDict.random_str}, 82 | ], 83 | }, 84 | { 85 | "name": "bone", 86 | "weight": AssertDict.random_float, 87 | "materials": [ 88 | {"name": AssertDict.random_str, "origin": AssertDict.random_str}, 89 | ], 90 | }, 91 | ], 92 | }, 93 | { 94 | "name": "cat", 95 | "age": AssertDict.random_int, 96 | "toys": [ 97 | { 98 | "name": AssertDict.random_str, 99 | "weight": AssertDict.random_float, 100 | "materials": [ 101 | {"name": AssertDict.random_str, "origin": AssertDict.random_str}, 102 | ], 103 | } 104 | ], 105 | }, 106 | ], 107 | "address": {"city": AssertDict.random_str, "country": "France"}, 108 | } 109 | AssertDict.assert_dict_expected_shape(expected_dict, person.dict()) 110 | 111 | 112 | def test_factory_child_pydantic_model() -> None: 113 | """Given a Pydantic Factory, When I build a model using the factory passing a Pydantic model as attribute, Then the 114 | pydantic model is correctly built. 115 | """ 116 | address = Address(city="Paris", country="France") 117 | person = PersonFactory.build(address=address) 118 | 119 | assert person.address.city == "Paris" 120 | assert person.address.country == "France" 121 | 122 | 123 | def test_factory_child_none() -> None: 124 | """Given a Pydantic Factory, When I build a model using the factory passing None as attribute, Then the pydantic 125 | model is correctly built. 126 | """ 127 | 128 | class PersonOptional(BaseModel): 129 | name: str 130 | address: Optional[Address] 131 | 132 | class PersonOptionalFactory(ModelFactory): 133 | __model__ = PersonOptional 134 | 135 | person = PersonOptionalFactory.build(address=None) 136 | assert person.address is None 137 | 138 | 139 | class AssertDict: 140 | random_float = "random_float" 141 | random_int = "random_int" 142 | random_str = "random_str" 143 | 144 | @staticmethod 145 | def assert_dict_expected_shape(expected_json: Any, json: Any) -> None: 146 | if isinstance(expected_json, list): 147 | assert len(expected_json) == len(json) 148 | for expected, actual in zip(expected_json, json): 149 | AssertDict.assert_dict_expected_shape(expected, actual) 150 | elif isinstance(expected_json, dict): 151 | for key, value in expected_json.items(): 152 | assert key in json 153 | AssertDict.assert_dict_expected_shape(value, json[key]) 154 | elif expected_json == AssertDict.random_float: 155 | assert isinstance(json, float) 156 | elif expected_json == AssertDict.random_int: 157 | assert isinstance(json, int) 158 | elif expected_json == AssertDict.random_str: 159 | assert isinstance(json, str) 160 | else: 161 | assert expected_json == json 162 | 163 | 164 | def test_factory_not_ok() -> None: 165 | """Given a Pydantic Model with nested Mapping field, When I build the model using the factory passing only partial 166 | attributes, Then the model is correctly built. 167 | """ 168 | 169 | class NestedSchema(BaseModel): 170 | v: str 171 | z: int 172 | 173 | class UpperSchema(BaseModel): 174 | a: int 175 | b: Mapping[str, str] 176 | nested: Mapping[str, NestedSchema] 177 | 178 | class UpperSchemaFactory(ModelFactory): 179 | __model__ = UpperSchema 180 | 181 | nested = NestedSchema(v="hello", z=0) 182 | some_dict = {"test": "fine"} 183 | upper = UpperSchemaFactory.build(b=some_dict, nested={"nested_key": nested}) 184 | 185 | assert upper.b["test"] == "fine" 186 | assert "nested_key" in upper.nested 187 | assert upper.nested["nested_key"].v == nested.v 188 | assert upper.nested["nested_key"].z == nested.z 189 | 190 | 191 | def test_factory_with_nested_dict() -> None: 192 | """Given a Pydantic Model with nested Dict field, When I build the model using the factory passing only partial 193 | attributes, Then the model is correctly built. 194 | """ 195 | 196 | class NestedSchema(BaseModel): 197 | z: int 198 | 199 | class UpperSchema(BaseModel): 200 | nested: Mapping[str, NestedSchema] 201 | 202 | class UpperSchemaFactory(ModelFactory): 203 | __model__ = UpperSchema 204 | 205 | nested = NestedSchema(z=0) 206 | upper = UpperSchemaFactory.build(nested={"nested_dict": nested}) 207 | 208 | assert "nested_dict" in upper.nested 209 | assert upper.nested["nested_dict"].z == nested.z 210 | 211 | 212 | def test_factory_with_partial_kwargs_deep_in_tree() -> None: 213 | # the code below is a modified copy of the bug reproduction example in 214 | # https://github.com/starlite-api/pydantic-factories/issues/115 215 | class A(BaseModel): 216 | name: str 217 | age: int 218 | 219 | class B(BaseModel): 220 | a: A 221 | 222 | class C(BaseModel): 223 | b: B 224 | 225 | class D(BaseModel): 226 | c: C 227 | 228 | class DFactory(ModelFactory): 229 | __model__ = D 230 | 231 | build_result = DFactory.build(factory_use_construct=False, **{"c": {"b": {"a": {"name": "test"}}}}) 232 | assert build_result 233 | assert build_result.c.b.a.name == "test" 234 | -------------------------------------------------------------------------------- /tests/test_factory_fields.py: -------------------------------------------------------------------------------- 1 | import random 2 | from datetime import datetime, timedelta 3 | from typing import Any, Optional 4 | 5 | import pytest 6 | from pydantic import BaseModel 7 | 8 | from pydantic_factories import ModelFactory, Use 9 | from pydantic_factories.exceptions import MissingBuildKwargError 10 | from pydantic_factories.fields import Ignore, PostGenerated, Require 11 | 12 | 13 | def test_use() -> None: 14 | class MyClass: 15 | name: str 16 | 17 | @classmethod 18 | def builder(cls, name: str) -> "MyClass": 19 | instance = MyClass() 20 | instance.name = name 21 | return instance 22 | 23 | default_name = "Moishe Zuchmir" 24 | 25 | class MyModel(BaseModel): 26 | my_class: MyClass 27 | 28 | class Config: 29 | arbitrary_types_allowed = True 30 | 31 | class MyFactory(ModelFactory): 32 | __model__ = MyModel 33 | my_class = Use(fn=MyClass.builder, name=default_name) 34 | 35 | result = MyFactory.build() 36 | assert result.my_class.name == default_name 37 | 38 | 39 | def test_sub_factory() -> None: 40 | default_name = "Moishe Zuchmir" 41 | 42 | class FirstModel(BaseModel): 43 | name: str 44 | 45 | class SecondModel(BaseModel): 46 | first_model: FirstModel 47 | 48 | class MyFactory(ModelFactory): 49 | __model__ = SecondModel 50 | first_model = Use(fn=ModelFactory.create_factory(FirstModel).build, name=default_name) 51 | 52 | result = MyFactory.build() 53 | assert result.first_model.name == default_name 54 | 55 | 56 | def test_build_kwarg() -> None: 57 | class MyModel(BaseModel): 58 | name: str 59 | 60 | class MyFactory(ModelFactory): 61 | __model__ = MyModel 62 | name = Require() 63 | 64 | with pytest.raises(MissingBuildKwargError): 65 | MyFactory.build() 66 | 67 | assert MyFactory.build(name="moishe").name == "moishe" 68 | 69 | 70 | def test_ignored() -> None: 71 | class MyModel(BaseModel): 72 | name: Optional[str] 73 | 74 | class MyFactory(ModelFactory): 75 | __model__ = MyModel 76 | name = Ignore() 77 | 78 | assert MyFactory.build().name is None 79 | 80 | 81 | def test_post_generation() -> None: 82 | random_delta = timedelta(days=random.randint(0, 12), seconds=random.randint(13, 13000)) 83 | 84 | def add_timedelta(name: str, values: Any, **kwargs: Any) -> datetime: 85 | assert name == "to_dt" 86 | assert "from_dt" in values 87 | assert isinstance(values["from_dt"], datetime) 88 | return values["from_dt"] + random_delta 89 | 90 | def decide_long(name: str, values: Any, **kwargs: Any) -> bool: 91 | assert name == "is_long" 92 | assert "from_dt" in values 93 | assert "to_dt" in values 94 | assert "threshold" in kwargs 95 | assert isinstance(values["from_dt"], datetime) 96 | assert isinstance(values["to_dt"], datetime) 97 | difference = values["to_dt"] - values["from_dt"] 98 | return difference.days > kwargs["threshold"] # type: ignore 99 | 100 | def make_caption(name: str, values: Any, **kwargs: Any) -> str: 101 | assert name == "caption" 102 | assert "is_long" in values 103 | return "this was really long for me" if values["is_long"] else "just this" 104 | 105 | class MyModel(BaseModel): 106 | from_dt: datetime 107 | to_dt: datetime 108 | is_long: bool 109 | caption: str 110 | 111 | class MyFactory(ModelFactory): 112 | __model__ = MyModel 113 | to_dt = PostGenerated(add_timedelta) 114 | is_long = PostGenerated(decide_long, threshold=1) 115 | caption = PostGenerated(make_caption) 116 | 117 | result = MyFactory.build() 118 | assert result.to_dt - result.from_dt == random_delta 119 | assert result.is_long == (random_delta.days > 1) 120 | if result.is_long: 121 | assert result.caption == "this was really long for me" 122 | else: 123 | assert result.caption == "just this" 124 | -------------------------------------------------------------------------------- /tests/test_factory_options.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | import pytest 4 | from faker import Faker 5 | from pydantic import BaseModel, Field 6 | 7 | from pydantic_factories import ConfigurationError, ModelFactory 8 | from tests.models import Pet 9 | 10 | 11 | def test_allows_user_to_define_faker_instance() -> None: 12 | my_faker = Faker() 13 | setattr(my_faker, "__test__attr__", None) # noqa: B010 14 | 15 | class MyFactory(ModelFactory): 16 | __model__ = Pet 17 | __faker__ = my_faker 18 | 19 | assert hasattr(MyFactory.get_faker(), "__test__attr__") 20 | 21 | 22 | def test_validates_model_is_set_in_build() -> None: 23 | class MyFactory(ModelFactory): 24 | pass 25 | 26 | with pytest.raises(ConfigurationError): 27 | MyFactory.build() 28 | 29 | 30 | def test_validates_model_is_set_in_batch() -> None: 31 | class MyFactory(ModelFactory): 32 | pass 33 | 34 | with pytest.raises(ConfigurationError): 35 | MyFactory.batch(2) 36 | 37 | 38 | def test_validates_connection_in_create_sync() -> None: 39 | class MyFactory(ModelFactory): 40 | pass 41 | 42 | with pytest.raises(ConfigurationError): 43 | MyFactory.create_sync() 44 | 45 | 46 | def test_validates_connection_in_create_batch_sync() -> None: 47 | class MyFactory(ModelFactory): 48 | pass 49 | 50 | with pytest.raises(ConfigurationError): 51 | MyFactory.create_batch_sync(2) 52 | 53 | 54 | @pytest.mark.asyncio() 55 | async def test_validates_connection_in_create_async() -> None: 56 | class MyFactory(ModelFactory): 57 | pass 58 | 59 | with pytest.raises(ConfigurationError): 60 | await MyFactory.create_async() 61 | 62 | 63 | @pytest.mark.asyncio() 64 | async def test_validates_connection_in_create_batch_async() -> None: 65 | class MyFactory(ModelFactory): 66 | pass 67 | 68 | with pytest.raises(ConfigurationError): 69 | await MyFactory.create_batch_async(2) 70 | 71 | 72 | def test_factory_handling_of_optionals() -> None: 73 | class ModelWithOptionalValues(BaseModel): 74 | name: Optional[str] 75 | id: str 76 | complex: List[Optional[str]] = Field(min_items=1) 77 | 78 | class FactoryWithNoneOptionals(ModelFactory): 79 | __model__ = ModelWithOptionalValues 80 | 81 | FactoryWithNoneOptionals.seed_random(1) 82 | 83 | assert any(r.name is None for r in [FactoryWithNoneOptionals.build() for _ in range(10)]) 84 | assert any(r.name is not None for r in [FactoryWithNoneOptionals.build() for _ in range(10)]) 85 | assert all(r.id is not None for r in [FactoryWithNoneOptionals.build() for _ in range(10)]) 86 | assert any(r.complex[0] is None for r in [FactoryWithNoneOptionals.build() for _ in range(10)]) 87 | assert any(r.complex[0] is not None for r in [FactoryWithNoneOptionals.build() for _ in range(10)]) 88 | 89 | class FactoryWithoutNoneOptionals(ModelFactory): 90 | __model__ = ModelWithOptionalValues 91 | __allow_none_optionals__ = False 92 | 93 | assert all(r.name is not None for r in [FactoryWithoutNoneOptionals.build() for _ in range(10)]) 94 | assert all(r.id is not None for r in [FactoryWithoutNoneOptionals.build() for _ in range(10)]) 95 | assert any(r.complex[0] is not None for r in [FactoryWithoutNoneOptionals.build() for _ in range(10)]) 96 | 97 | 98 | def test_determine_results() -> None: 99 | class ModelWithOptionalValues(BaseModel): 100 | name: Optional[str] 101 | 102 | class FactoryWithNoneOptionals(ModelFactory): 103 | __model__ = ModelWithOptionalValues 104 | 105 | ModelFactory.seed_random(0) 106 | before_seeding = [FactoryWithNoneOptionals.build() for _ in range(10)] 107 | ModelFactory.seed_random(0) 108 | after_seeding = [FactoryWithNoneOptionals.build() for _ in range(10)] 109 | assert before_seeding == after_seeding 110 | -------------------------------------------------------------------------------- /tests/test_generics.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, List, Optional, TypeVar, Union 2 | 3 | from pydantic import BaseModel 4 | from pydantic.generics import GenericModel 5 | 6 | from pydantic_factories import ModelFactory 7 | 8 | Inner = TypeVar("Inner") 9 | APIResponseData = TypeVar("APIResponseData") 10 | 11 | 12 | class Attributes(GenericModel, Generic[Inner]): 13 | attributes: Inner 14 | 15 | 16 | class OneInner(BaseModel): 17 | one: str 18 | id: Optional[int] 19 | description: Optional[str] 20 | 21 | 22 | class OneResponse(BaseModel): 23 | one: Attributes[OneInner] 24 | 25 | 26 | class TwoInner(BaseModel): 27 | two: str 28 | id: Optional[int] 29 | description: Optional[str] 30 | 31 | 32 | class TwoResponse(BaseModel): 33 | two: Attributes[TwoInner] 34 | 35 | 36 | class ThreeInner(BaseModel): 37 | three: str 38 | relation: int 39 | 40 | 41 | class ThreeResponse(BaseModel): 42 | three: Attributes[ThreeInner] 43 | 44 | 45 | class APIResponse(GenericModel, Generic[APIResponseData]): 46 | data: List[APIResponseData] 47 | 48 | 49 | def test_generic_factory_one_response() -> None: 50 | class APIResponseFactory(ModelFactory): 51 | __model__ = APIResponse[OneResponse] 52 | 53 | result = APIResponseFactory.build() 54 | 55 | assert result.data 56 | assert isinstance(result.data[0], OneResponse) 57 | 58 | 59 | def test_generic_factory_two_response() -> None: 60 | class APIResponseFactory(ModelFactory): 61 | __model__ = APIResponse[TwoResponse] 62 | 63 | result = APIResponseFactory.build() 64 | 65 | assert result.data 66 | assert isinstance(result.data[0], TwoResponse) 67 | 68 | 69 | def test_generic_factory_three_response() -> None: 70 | class APIResponseFactory(ModelFactory): 71 | __model__ = APIResponse[ThreeResponse] 72 | 73 | result = APIResponseFactory.build() 74 | 75 | assert result.data 76 | assert isinstance(result.data[0], ThreeResponse) 77 | 78 | 79 | def test_generic_factory_union_response() -> None: 80 | class APIResponseFactory(ModelFactory): 81 | __model__ = APIResponse[Union[OneResponse, TwoResponse, ThreeResponse]] 82 | 83 | result = APIResponseFactory.build() 84 | 85 | assert result.data 86 | assert isinstance(result.data[0], (OneResponse, TwoResponse, ThreeResponse)) 87 | -------------------------------------------------------------------------------- /tests/test_new_types.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from decimal import Decimal 3 | from typing import Any, Dict, List, NewType, Optional, Tuple, Union 4 | 5 | from pydantic import ( 6 | BaseModel, 7 | PositiveFloat, 8 | conbytes, 9 | condate, 10 | condecimal, 11 | confloat, 12 | confrozenset, 13 | conint, 14 | conlist, 15 | conset, 16 | constr, 17 | ) 18 | 19 | from pydantic_factories import ModelFactory 20 | 21 | 22 | def test_new_types() -> None: 23 | MyInt = NewType("MyInt", int) 24 | WrappedInt = NewType("WrappedInt", MyInt) 25 | 26 | class MyModel(BaseModel): 27 | int_field: MyInt 28 | wrapped_int_field: WrappedInt 29 | 30 | class MyModelFactory(ModelFactory): 31 | __model__ = MyModel 32 | 33 | result = MyModelFactory.build() 34 | assert isinstance(result.int_field, int) 35 | assert isinstance(result.wrapped_int_field, int) 36 | 37 | 38 | def test_complex_new_types() -> None: 39 | MyStr = NewType("MyStr", str) 40 | MyInt = NewType("MyInt", int) 41 | 42 | class NestedModel(BaseModel): 43 | nested_int_field: MyInt 44 | 45 | MyNestedModel = NewType("MyNestedModel", NestedModel) 46 | 47 | class MyModel(BaseModel): 48 | list_int_field: List[MyInt] 49 | union_field: Union[MyInt, MyStr] 50 | optional_str_field: Optional[MyStr] 51 | tuple_str_str: Tuple[MyStr, MyStr] 52 | dict_field: Dict[MyStr, Any] 53 | complex_dict_field: Dict[MyStr, Dict[Union[MyInt, MyStr], MyInt]] 54 | nested_model_field: MyNestedModel 55 | 56 | class MyModelFactory(ModelFactory): 57 | __model__ = MyModel 58 | 59 | result = MyModelFactory.build() 60 | 61 | assert isinstance(result.list_int_field[0], int) 62 | assert isinstance(result.union_field, (int, str)) 63 | assert result.optional_str_field is None or isinstance(result.optional_str_field, str) 64 | assert isinstance(result.tuple_str_str, tuple) 65 | assert isinstance(result.dict_field, dict) 66 | assert isinstance(result.nested_model_field, NestedModel) 67 | assert isinstance(result.nested_model_field.nested_int_field, int) 68 | 69 | 70 | def test_constrained_new_types() -> None: 71 | ConBytes = NewType("ConBytes", conbytes(min_length=2, max_length=4)) # type: ignore[misc] 72 | ConStr = NewType("ConStr", constr(min_length=10, max_length=15)) # type: ignore[misc] 73 | ConInt = NewType("ConInt", conint(gt=100, lt=110)) # type: ignore[misc] 74 | ConFloat = NewType("ConFloat", confloat(lt=-100)) # type: ignore[misc] 75 | ConDecimal = NewType("ConDecimal", condecimal(ge=Decimal(6), le=Decimal(8))) # type: ignore[misc] 76 | ConDate = NewType("ConDate", condate(gt=date.today())) # type: ignore[misc] 77 | ConList = NewType("ConList", conlist(item_type=int, min_items=3)) # type: ignore[misc] 78 | ConSet = NewType("ConSet", conset(item_type=int, min_items=4)) # type: ignore[misc] 79 | ConFrozenSet = NewType("ConFrozenSet", confrozenset(item_type=str, min_items=5)) # type: ignore[misc] 80 | ConMyPositiveFloat = NewType("ConMyPositiveFloat", PositiveFloat) 81 | 82 | class ConstrainedModel(BaseModel): 83 | conbytes_field: ConBytes 84 | constr_field: ConStr 85 | conint_field: ConInt 86 | confloat_field: ConFloat 87 | condecimal_field: ConDecimal 88 | condate_field: ConDate 89 | conlist_field: ConList 90 | conset_field: ConSet 91 | confrozenset_field: ConFrozenSet 92 | conpositive_float_field: ConMyPositiveFloat 93 | 94 | class MyFactory(ModelFactory): 95 | __model__ = ConstrainedModel 96 | 97 | result = MyFactory.build() 98 | 99 | # we want to make sure that NewType is correctly unwrapped retaining 100 | # original type and its attributes. More elaborate testing of constrained 101 | # fields is done in tests/constraints/ 102 | assert isinstance(result.conbytes_field, bytes) 103 | assert 2 <= len(result.conbytes_field) <= 4 104 | 105 | assert isinstance(result.constr_field, str) 106 | assert 10 <= len(result.constr_field) <= 15 107 | 108 | assert isinstance(result.conint_field, int) 109 | assert 100 < result.conint_field < 110 110 | 111 | assert isinstance(result.confloat_field, float) 112 | assert result.confloat_field < -100 113 | 114 | assert isinstance(result.condecimal_field, Decimal) 115 | assert Decimal(6) <= result.condecimal_field <= Decimal(8) 116 | 117 | assert isinstance(result.condate_field, date) 118 | assert result.condate_field > date.today() 119 | 120 | assert isinstance(result.conlist_field, list) 121 | assert len(result.conlist_field) >= 3 122 | 123 | assert isinstance(result.conset_field, set) 124 | assert len(result.conset_field) >= 4 125 | 126 | assert isinstance(result.confrozenset_field, frozenset) 127 | assert len(result.confrozenset_field) >= 5 128 | 129 | assert isinstance(result.conpositive_float_field, float) 130 | assert result.conpositive_float_field > 0 131 | -------------------------------------------------------------------------------- /tests/test_protocols.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import pytest 4 | from pydantic import BaseModel 5 | 6 | from pydantic_factories import ( 7 | AsyncPersistenceProtocol, 8 | ModelFactory, 9 | SyncPersistenceProtocol, 10 | ) 11 | 12 | 13 | class MyModel(BaseModel): 14 | name: str 15 | 16 | 17 | class MySyncPersistenceHandler(SyncPersistenceProtocol): 18 | def save(self, data: Any, *args: Any, **kwargs: Any) -> Any: 19 | return data 20 | 21 | def save_many(self, data: Any, *args: Any, **kwargs: Any) -> Any: 22 | return data 23 | 24 | 25 | class MyAsyncPersistenceHandler(AsyncPersistenceProtocol): 26 | async def save(self, data: Any, *args: Any, **kwargs: Any) -> Any: 27 | return data 28 | 29 | async def save_many(self, data: Any, *args: Any, **kwargs: Any) -> Any: 30 | return data 31 | 32 | 33 | def test_sync_persistence_handler_is_set_and_called_with_instance() -> None: 34 | class MyFactory(ModelFactory): 35 | __model__ = MyModel 36 | __sync_persistence__ = MySyncPersistenceHandler() 37 | 38 | assert MyFactory.create_sync().name 39 | assert [instance.name for instance in MyFactory.create_batch_sync(size=2)] 40 | 41 | 42 | def test_sync_persistence_handler_is_set_and_called_with_class() -> None: 43 | class MyFactory(ModelFactory): 44 | __model__ = MyModel 45 | __sync_persistence__ = MySyncPersistenceHandler 46 | 47 | assert MyFactory.create_sync().name 48 | assert [instance.name for instance in MyFactory.create_batch_sync(size=2)] 49 | 50 | 51 | @pytest.mark.asyncio() 52 | async def test_async_persistence_handler_is_set_and_called_with_instance() -> None: 53 | class MyFactory(ModelFactory): 54 | __model__ = MyModel 55 | __async_persistence__ = MyAsyncPersistenceHandler() 56 | 57 | assert (await MyFactory.create_async()).name 58 | assert [instance.name for instance in (await MyFactory.create_batch_async(size=2))] 59 | 60 | 61 | @pytest.mark.asyncio() 62 | async def test_async_persistence_handler_is_set_and_called_with_class() -> None: 63 | class MyFactory(ModelFactory): 64 | __model__ = MyModel 65 | __async_persistence__ = MyAsyncPersistenceHandler 66 | 67 | assert (await MyFactory.create_async()).name 68 | assert [instance.name for instance in (await MyFactory.create_batch_async(size=2))] 69 | -------------------------------------------------------------------------------- /tests/test_random_seed.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | 3 | from pydantic_factories import ModelFactory 4 | 5 | 6 | def test_random_seed() -> None: 7 | class MyModel(BaseModel): 8 | id: int 9 | special_id: str = Field(regex=r"ID-[1-9]{3}\.[1-9]{3}") 10 | 11 | class MyModelFactory(ModelFactory): 12 | __model__ = MyModel 13 | 14 | ModelFactory.seed_random(1651) 15 | 16 | ins = MyModelFactory.build() 17 | 18 | assert ins.id == 4 19 | assert ins.special_id == "ID-515.943" 20 | -------------------------------------------------------------------------------- /tests/test_refex_factory.py: -------------------------------------------------------------------------------- 1 | """The tests in this file have been adapted from: 2 | 3 | https://github.com/crdoconnor/xeger/blob/master/xeger/tests/test_xeger.py 4 | """ 5 | 6 | import re 7 | from typing import TYPE_CHECKING, Union 8 | 9 | import pytest 10 | 11 | from pydantic_factories.value_generators.regex import RegexFactory 12 | 13 | if TYPE_CHECKING: 14 | from re import Pattern 15 | 16 | 17 | def match(pattern: Union[str, "Pattern"]) -> None: 18 | for _ in range(100): 19 | assert re.match(pattern, RegexFactory()(pattern)) 20 | 21 | 22 | def test_single_dot() -> None: 23 | """Verify that the dot character produces only a single character.""" 24 | match(r"^.$") 25 | 26 | 27 | def test_dot() -> None: 28 | """Verify that the dot character doesn't produce newlines. 29 | 30 | See: https://bitbucket.org/leapfrogdevelopment/rstr/issue/1/ 31 | """ 32 | for _ in range(100): 33 | match(r".+") 34 | 35 | 36 | def test_date() -> None: 37 | match(r"^([1-9]|0[1-9]|[12][0-9]|3[01])\D([1-9]|0[1-9]|1[012])\D(19[0-9][0-9]|20[0-9][0-9])$") 38 | 39 | 40 | def test_up_to_closing_tag() -> None: 41 | match(r"([^<]*)") 42 | 43 | 44 | def test_ipv4() -> None: 45 | match(r"^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]){3}$") 46 | 47 | 48 | def test_year_1900_2099() -> None: 49 | match(r"^(19|20)[\d]{2,2}$") 50 | 51 | 52 | def test_positive_or_negative_number() -> None: 53 | match(r"^-{0,1}\d*\.{0,1}\d+$") 54 | 55 | 56 | def test_positive_integers() -> None: 57 | match(r"^\d+$") 58 | 59 | 60 | def test_email_complicated() -> None: 61 | match(r'^([0-9a-zA-Z]([\+\-_\.][0-9a-zA-Z]+)*)+"@(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]*\.)+[a-zA-Z0-9]{2,17})$') 62 | 63 | 64 | def test_email() -> None: 65 | match(r"(.*?)\@(.*?)\.(.*?)") 66 | 67 | 68 | def test_alpha() -> None: 69 | match(r"[:alpha:]") 70 | 71 | 72 | def test_zero_or_more_anything_non_greedy() -> None: 73 | match(r"(.*?)") 74 | 75 | 76 | def test_literals() -> None: 77 | match(r"foo") 78 | 79 | 80 | def test_digit() -> None: 81 | match(r"\d") 82 | 83 | 84 | def test_nondigits() -> None: 85 | match(r"\D") 86 | 87 | 88 | def test_literal_with_repeat() -> None: 89 | match(r"A{3}") 90 | 91 | 92 | def test_literal_with_range_repeat() -> None: 93 | match(r"A{2, 5}") 94 | 95 | 96 | def test_word() -> None: 97 | match(r"\w") 98 | 99 | 100 | def test_nonword() -> None: 101 | match(r"\W") 102 | 103 | 104 | def test_or() -> None: 105 | match(r"foo|bar") 106 | 107 | 108 | def test_or_with_subpattern() -> None: 109 | match(r"(foo|bar)") 110 | 111 | 112 | def test_range() -> None: 113 | match(r"[A-F]") 114 | 115 | 116 | def test_character_group() -> None: 117 | match(r"[ABC]") 118 | 119 | 120 | def test_caret() -> None: 121 | match(r"^foo") 122 | 123 | 124 | def test_dollarsign() -> None: 125 | match(r"foo$") 126 | 127 | 128 | def test_not_literal() -> None: 129 | match(r"[^a]") 130 | 131 | 132 | def test_negation_group() -> None: 133 | match(r"[^AEIOU]") 134 | 135 | 136 | def test_lookahead() -> None: 137 | match(r"foo(?=bar)") 138 | 139 | 140 | def test_lookbehind() -> None: 141 | pattern = r"(?<=foo)bar" 142 | assert re.search(pattern, RegexFactory()(pattern)) 143 | 144 | 145 | def test_backreference() -> None: 146 | match(r"(foo|bar)baz\1") 147 | 148 | 149 | def test_zero_or_more_greedy() -> None: 150 | match(r"a*") 151 | match(r"(.*)") 152 | 153 | 154 | def test_zero_or_more_non_greedy() -> None: 155 | match(r"a*?") 156 | 157 | 158 | @pytest.mark.parametrize("limit", range(5)) 159 | def test_incoherent_limit_and_qualifier(limit: int) -> None: 160 | r = RegexFactory(limit=limit) 161 | o = r(r"a{2}") 162 | assert len(o) == 2 163 | 164 | 165 | @pytest.mark.parametrize("seed", [777, 1234, 369, 8031]) 166 | def test_regex_factory_object_seeding(seed: int) -> None: 167 | xg1 = RegexFactory(seed=seed) 168 | string1 = xg1(r"\w{3,4}") 169 | 170 | xg2 = RegexFactory(seed=seed) 171 | string2 = xg2(r"\w{3,4}") 172 | 173 | assert string1 == string2 174 | 175 | 176 | def test_regex_factorx_random_instance() -> None: 177 | xg1 = RegexFactory() 178 | xg_random = xg1._random 179 | 180 | xg2 = RegexFactory() 181 | xg2._random = xg_random 182 | 183 | assert xg1._random == xg2._random 184 | # xg_random is used by both, so if we give 185 | # the same seed, the result should be the same 186 | 187 | xg_random.seed(90) 188 | string1 = xg1(r"\w\d\w") 189 | xg_random.seed(90) 190 | string2 = xg2(r"\w\d\w") 191 | 192 | assert string1 == string2 193 | -------------------------------------------------------------------------------- /tests/test_typeddict.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional 2 | 3 | from pydantic import BaseModel 4 | from typing_extensions import TypedDict 5 | 6 | from pydantic_factories import ModelFactory 7 | 8 | 9 | class TypedDictModel(TypedDict): 10 | id: int 11 | name: str 12 | list_field: List[Dict[str, int]] 13 | int_field: Optional[int] 14 | 15 | 16 | def test_factory_with_typeddict() -> None: 17 | class MyFactory(ModelFactory[TypedDictModel]): 18 | __model__ = TypedDictModel 19 | 20 | result = MyFactory.build() 21 | 22 | assert isinstance(result, dict) 23 | assert result["id"] 24 | assert result["name"] 25 | assert result["list_field"][0] 26 | assert result["int_field"] 27 | 28 | 29 | def test_factory_model_with_typeddict_attribute_value() -> None: 30 | class MyModel(BaseModel): 31 | td: TypedDictModel 32 | name: str 33 | list_field: List[Dict[str, int]] 34 | int_field: Optional[int] 35 | 36 | class MyFactory(ModelFactory[MyModel]): 37 | __model__ = MyModel 38 | 39 | result = MyFactory.build() 40 | 41 | assert isinstance(result.td, dict) 42 | assert result.td["id"] 43 | assert result.td["name"] 44 | assert result.td["list_field"][0] 45 | assert result.td["int_field"] 46 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | import sys 3 | from decimal import Decimal 4 | from typing import Any, NewType, Union 5 | 6 | from hypothesis import given 7 | from hypothesis.strategies import decimals, floats, integers 8 | from pydantic import BaseModel 9 | 10 | from pydantic_factories.factory import ModelFactory 11 | from pydantic_factories.utils import ( 12 | is_multiply_of_multiple_of_in_range, 13 | is_new_type, 14 | is_union, 15 | unwrap_new_type_if_needed, 16 | ) 17 | 18 | 19 | def test_is_union() -> None: 20 | class UnionTest(BaseModel): 21 | union: Union[int, str] 22 | no_union: Any 23 | 24 | class UnionTestFactory(ModelFactory): 25 | __model__ = UnionTest 26 | 27 | for field_name, model_field in UnionTestFactory.get_model_fields(UnionTestFactory._get_model()): 28 | if field_name == "union": 29 | assert is_union(model_field) 30 | else: 31 | assert not is_union(model_field) 32 | 33 | # for python 3.10 we need to run the test as well with the union_pipe operator 34 | if sys.version_info >= (3, 10): 35 | 36 | class UnionTestWithPipe(BaseModel): 37 | union_pipe: int | str | None # Pipe syntax supported from Python 3.10 onwards 38 | union_normal: Union[int, str] 39 | no_union: Any 40 | 41 | class UnionTestWithPipeFactory(ModelFactory): 42 | __model__ = UnionTestWithPipe 43 | 44 | for field_name, model_field in UnionTestWithPipeFactory.get_model_fields(UnionTestWithPipeFactory._get_model()): 45 | if field_name in ("union_pipe", "union_normal"): 46 | assert is_union(model_field) 47 | else: 48 | assert not is_union(model_field) 49 | 50 | 51 | def test_is_new_type() -> None: 52 | MyInt = NewType("MyInt", int) 53 | 54 | assert is_new_type(MyInt) 55 | assert not is_new_type(int) 56 | 57 | 58 | def test_unwrap_new_type_is_needed() -> None: 59 | MyInt = NewType("MyInt", int) 60 | WrappedInt = NewType("WrappedInt", MyInt) 61 | 62 | assert unwrap_new_type_if_needed(MyInt) is int 63 | assert unwrap_new_type_if_needed(WrappedInt) is int 64 | assert unwrap_new_type_if_needed(int) is int 65 | 66 | 67 | def test_is_multiply_of_multiple_of_in_range_extreme_cases() -> None: 68 | assert is_multiply_of_multiple_of_in_range(minimum=None, maximum=10.0, multiple_of=20.0) 69 | assert not is_multiply_of_multiple_of_in_range(minimum=5.0, maximum=10.0, multiple_of=20.0) 70 | 71 | assert is_multiply_of_multiple_of_in_range(minimum=1.0, maximum=1.0, multiple_of=0.33333333333) 72 | assert is_multiply_of_multiple_of_in_range( 73 | minimum=Decimal(1), maximum=Decimal(1), multiple_of=Decimal("0.33333333333") 74 | ) 75 | assert not is_multiply_of_multiple_of_in_range(minimum=Decimal(1), maximum=Decimal(1), multiple_of=Decimal("0.333")) 76 | 77 | assert is_multiply_of_multiple_of_in_range(minimum=5, maximum=5, multiple_of=5) 78 | 79 | # while multiple_of=0.0 leads to ZeroDivision exception in pydantic 80 | # it can handle values close to zero properly so we should support this too 81 | assert is_multiply_of_multiple_of_in_range(minimum=10.0, maximum=20.0, multiple_of=1e-10) 82 | # test corner case found by peterschutt 83 | assert not is_multiply_of_multiple_of_in_range( 84 | minimum=Decimal("999999999.9999999343812775"), 85 | maximum=Decimal("999999999.990476"), 86 | multiple_of=Decimal("-0.556"), 87 | ) 88 | 89 | 90 | @given( 91 | floats(allow_nan=False, allow_infinity=False, min_value=1e-6, max_value=1000000000), 92 | integers(min_value=-100000, max_value=100000), 93 | ) 94 | def test_is_multiply_of_multiple_of_in_range_for_floats(base_multiple_of: float, multiplier: int) -> None: 95 | if multiplier != 0: 96 | for multiple_of in [base_multiple_of, -base_multiple_of]: 97 | minimum, maximum = sorted( 98 | [ 99 | multiplier * multiple_of + random.random() * 100, 100 | (multiplier + random.randint(1, 100)) * multiple_of + random.random() * 100, 101 | ] 102 | ) 103 | assert is_multiply_of_multiple_of_in_range(minimum=minimum, maximum=maximum, multiple_of=multiple_of) 104 | 105 | minimum, maximum = sorted( 106 | [ 107 | (multiplier + (random.random() / 2 + 0.01)) * multiple_of, 108 | (multiplier + (random.random() / 2 + 0.45)) * multiple_of, 109 | ] 110 | ) 111 | assert not is_multiply_of_multiple_of_in_range(minimum=minimum, maximum=maximum, multiple_of=multiple_of) 112 | 113 | 114 | @given( 115 | integers(min_value=-1000000000, max_value=1000000000), 116 | integers(min_value=-100000, max_value=100000), 117 | ) 118 | def test_is_multiply_of_multiple_of_in_range_for_int(base_multiple_of: int, multiplier: int) -> None: 119 | if multiplier != 0 and base_multiple_of not in [-1, 0, 1]: 120 | for multiple_of in [base_multiple_of, -base_multiple_of]: 121 | minimum, maximum = sorted( 122 | [ 123 | multiplier * multiple_of + random.randint(1, 100), 124 | (multiplier + random.randint(1, 100)) * multiple_of + random.randint(1, 100), 125 | ] 126 | ) 127 | assert is_multiply_of_multiple_of_in_range(minimum=minimum, maximum=maximum, multiple_of=multiple_of) 128 | 129 | 130 | @given( 131 | decimals(min_value=Decimal("1e-6"), max_value=Decimal("1000000000")), 132 | integers(min_value=-100000, max_value=100000), 133 | ) 134 | def test_is_multiply_of_multiple_of_in_range_for_decimals(base_multiple_of: Decimal, multiplier: int) -> None: 135 | if multiplier != 0 and base_multiple_of != 0: 136 | for multiple_of in [base_multiple_of, -base_multiple_of]: 137 | minimum, maximum = sorted( 138 | [ 139 | multiplier * multiple_of + Decimal(random.random() * 100), 140 | (multiplier + random.randint(1, 100)) * multiple_of + Decimal(random.random() * 100), 141 | ] 142 | ) 143 | assert is_multiply_of_multiple_of_in_range(minimum=minimum, maximum=maximum, multiple_of=multiple_of) 144 | 145 | minimum, maximum = sorted( 146 | [ 147 | (multiplier + Decimal(random.random() / 2 + 0.01)) * multiple_of, 148 | (multiplier + Decimal(random.random() / 2 + 0.45)) * multiple_of, 149 | ] 150 | ) 151 | assert not is_multiply_of_multiple_of_in_range(minimum=minimum, maximum=maximum, multiple_of=multiple_of) 152 | -------------------------------------------------------------------------------- /tests/typing_test_strict.py: -------------------------------------------------------------------------------- 1 | """Module exists only to test generic boundaries. 2 | 3 | Filename should not start with "test_". 4 | """ 5 | import dataclasses 6 | 7 | import pydantic.dataclasses 8 | from pydantic import BaseModel 9 | 10 | from pydantic_factories import ModelFactory 11 | 12 | 13 | class PydanticClass(BaseModel): 14 | field: str 15 | 16 | 17 | class PydanticClassFactory(ModelFactory[PydanticClass]): 18 | __model__ = PydanticClass 19 | 20 | 21 | @pydantic.dataclasses.dataclass 22 | class PydanticDataClass: 23 | field: str 24 | 25 | 26 | class PydanticDataClassFactory(ModelFactory[PydanticDataClass]): 27 | __model__ = PydanticDataClass 28 | 29 | 30 | @dataclasses.dataclass() 31 | class PythonDataClass: 32 | field: str 33 | 34 | 35 | class PythonDataClassFactory(ModelFactory[PythonDataClass]): 36 | __model__ = PythonDataClass 37 | --------------------------------------------------------------------------------