├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 
12 | 
13 |
14 | [](https://discord.gg/X3FJqy8d2j) [](https://matrix.to/#/#starlitespace:matrix.org) [](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 |
3 |
4 |
5 |
6 |
7 |
8 | 
9 | 
10 |
11 | [](https://lgtm.com/projects/g/Goldziher/pydantic-factories/context:python)
12 | [](https://lgtm.com/projects/g/Goldziher/pydantic-factories/alerts/)
13 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_pydantic-factories)
14 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_pydantic-factories)
15 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_pydantic-factories)
16 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_pydantic-factories)
17 |
18 | [](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 |
--------------------------------------------------------------------------------