pax_global_header00006660000000000000000000000064147576723030014530gustar00rootroot0000000000000052 comment=2ef7632ae60b5f81b37a3d7a7d9778640687481c model_bakery-1.20.4/000077500000000000000000000000001475767230300142515ustar00rootroot00000000000000model_bakery-1.20.4/.editorconfig000066400000000000000000000004541475767230300167310ustar00rootroot00000000000000# http://editorconfig.org root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.py] indent_style = space indent_size = 4 [*.{rst,ini}] indent_style = space indent_size = 4 [*.{yml,html,xml,xsl,json}] indent_style = space indent_size = 2 model_bakery-1.20.4/.github/000077500000000000000000000000001475767230300156115ustar00rootroot00000000000000model_bakery-1.20.4/.github/CODE_OF_CONDUCT.md000066400000000000000000000001311475767230300204030ustar00rootroot00000000000000This project follows [Django's Code of Conduct](https://www.djangoproject.com/conduct/). model_bakery-1.20.4/.github/FUNDING.yml000066400000000000000000000000541475767230300174250ustar00rootroot00000000000000github: amureki tidelift: pypi/model-bakery model_bakery-1.20.4/.github/ISSUE_TEMPLATE/000077500000000000000000000000001475767230300177745ustar00rootroot00000000000000model_bakery-1.20.4/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000006261475767230300224720ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the issue** A clear and concise description of what the issue is. **To Reproduce** Steps to reproduce the behavior. **Expected behavior** A clear and concise description of what you expected to happen. **Versions** - Python: [e.g. 3.10] - Django [e.g. 4.1] - Model Bakery [e.g. 1.6.0] model_bakery-1.20.4/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000002741475767230300214150ustar00rootroot00000000000000**Describe the change** A clear and concise description of what the change is. **PR Checklist** - [ ] Change is covered with tests - [ ] [CHANGELOG.md](CHANGELOG.md) is updated if needed model_bakery-1.20.4/.github/dependabot.yml000066400000000000000000000003531475767230300204420ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "monthly" rebase-strategy: "auto" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" model_bakery-1.20.4/.github/workflows/000077500000000000000000000000001475767230300176465ustar00rootroot00000000000000model_bakery-1.20.4/.github/workflows/ci.yml000066400000000000000000000062301475767230300207650ustar00rootroot00000000000000name: Tests on: push: branches: - main pull_request: concurrency: group: ${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: linters: runs-on: ubuntu-22.04 strategy: matrix: lint-command: - ruff check --output-format=github . - black --check --diff . - mypy model_bakery steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" cache: pip - run: python -m pip install .[test] - run: ${{ matrix.lint-command }} tests: name: Python ${{ matrix.python-version }} runs-on: ubuntu-22.04 strategy: matrix: python-version: - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" env: PGUSER: postgres PGPASSWORD: postgres services: postgis: image: postgis/postgis env: POSTGRES_DB: postgres POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres ports: - 5432:5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v4 - name: Set up PostgreSQL run: | sudo apt-get update sudo apt-get install -y gdal-bin psql template1 -c "CREATE EXTENSION citext;" -U postgres -h localhost -p 5432 psql template1 -c "CREATE EXTENSION hstore;" -U postgres -h localhost -p 5432 psql template1 -c "CREATE EXTENSION postgis;" -U postgres -h localhost -p 5432 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true cache: pip - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel tox - name: Run tox targets for ${{ matrix.python-version }} run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .) - name: Upload coverage data uses: actions/upload-artifact@v4 with: name: coverage-data-${{ matrix.python-version }} include-hidden-files: true path: '.coverage.*' if-no-files-found: ignore coverage: name: Coverage runs-on: ubuntu-22.04 needs: tests steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install dependencies run: python -m pip install --upgrade coverage[toml] - name: Download data uses: actions/download-artifact@v4 with: pattern: coverage-data-* merge-multiple: true - name: Combine coverage and fail if it's <95% run: | python -m coverage combine python -m coverage html --skip-covered --skip-empty python -m coverage report --fail-under=95 - name: Upload HTML report if: ${{ failure() }} uses: actions/upload-artifact@v4 with: name: html-report path: htmlcov model_bakery-1.20.4/.github/workflows/release.yml000066400000000000000000000011261475767230300220110ustar00rootroot00000000000000name: PyPI release on: release: types: [ published ] jobs: package: name: Build & verify package runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" cache: pip - name: Build package run: | python -m pip install -U build twine wheel python -m build twine check --strict dist/* - name: Publish package uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_API_TOKEN }} model_bakery-1.20.4/.gitignore000066400000000000000000000002061475767230300162370ustar00rootroot00000000000000*.pyc *.swp *.swo *.kpf bin/ include/ lib/ local/ share/ *.egg-info/ .idea .tox build/ lib64 pyvenv.cfg dist/ /.coverage /.coverage.* model_bakery-1.20.4/.pre-commit-config.yaml000066400000000000000000000011571475767230300205360ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: check-added-large-files - id: debug-statements - id: end-of-file-fixer - id: requirements-txt-fixer - id: trailing-whitespace - id: check-merge-conflict - id: check-yaml - repo: local hooks: - id: ruff name: ruff entry: ruff check language: system args: [ --fix, --exit-non-zero-on-fix ] types: - python - id: black name: black entry: black language: system types: - python model_bakery-1.20.4/.readthedocs.yaml000066400000000000000000000003061475767230300174770ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.11" python: install: - method: pip path: . extra_requirements: - docs sphinx: configuration: docs/conf.py model_bakery-1.20.4/CHANGELOG.md000066400000000000000000000447511475767230300160750ustar00rootroot00000000000000# Changelog All notable changes to `model_bakery` will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased](https://github.com/model-bakers/model_bakery/tree/main) ### Added ### Changed ### Removed ## [1.20.4](https://pypi.org/project/model-bakery/1.20.4/) ### Changed - Fix regression introduced in 1.20.3 that prevented using `auto_now` and `auto_now_add` fields with seq or callable. ## [1.20.3](https://pypi.org/project/model-bakery/1.20.3/) ### Changed - Fix support of `auto_now` and `auto_now_add` fields in combination with `_fill_optional` - Isolate Recipe defaults to prevent modification via instances ## [1.20.2](https://pypi.org/project/model-bakery/1.20.2/) ### Changed - Fix setting GFK parameter by a callable - Fix regression forbidding using Proxy models as GFK ## [1.20.1](https://pypi.org/project/model-bakery/1.20.1/) ### Added - docs: Add missing doc on `_refresh_after_create` option ### Changed - Fix `Recipe.prepare` without `_quantity` (on one-to-one relation) ### Removed - Remove deprecation warning of `datetime.datetime.utcfromtimestamp`. ## [1.20.0](https://pypi.org/project/model-bakery/1.20.0/) ### Added - Support to Field `db_default` value - Support to Python 3.13 ## [1.19.5](https://pypi.org/project/model-bakery/1.19.5/) ### Changed - Reset `content_type` and `object_id` fields when the content object is `None` ## [1.19.4](https://pypi.org/project/model-bakery/1.19.4/) ### Changed - Allow `None` value for generic foreign keys within iterators - Make `TextField` generator respect `max_length` - Deprecate `model_bakery.random_gen.gen_text` in favor of `model_bakery.random_gen.gen_string` ## [1.19.3](https://pypi.org/project/model-bakery/1.19.3/) ### Changed - Do not handle GFK fields when the object is None ## [1.19.2](https://pypi.org/project/model-bakery/1.19.2/) ### Added - docs: Add Django settings example for custom field generators ### Changed - Align GFK and content type fields generation - Allow `prepare()` to be used with GFK ## [1.19.1](https://pypi.org/project/model-bakery/1.19.1/) ### Changed - Handle bulk creation when using reverse related name ## [1.19.0](https://pypi.org/project/model-bakery/1.19.0/) ### Added - Add Django 5.1 support ## [1.18.3](https://pypi.org/project/model-bakery/1.18.3/) ### Changed - Fix support of `GenericForeignKey` fields in combination with `_fill_optional` ## [1.18.2](https://pypi.org/project/model-bakery/1.18.2/) ### Changed - Fix `make_recipe` to work with `_quantity` (#28) ## [1.18.1](https://pypi.org/project/model-bakery/1.18.1/) ### Changed - Replace expensive `count()` with cheap `exists()` ## [1.18.0](https://pypi.org/project/model-bakery/1.18.0/) ### Added - Add Django 5.0 support ### Changed - Allow baking without `contenttypes` framework ### Removed - Drop Django 3.2 and 4.1 support (reached end of life) ## [1.17.0](https://pypi.org/project/model-bakery/1.17.0/) ### Added - Add support to `auto_now` and `auto_now_add` fields. ### Changed - Remove unnecessary casting to string methods `random_gen.gen_slug` and `random_gen.gen_string` - [doc] Update installation command ## [1.16.0](https://pypi.org/project/model-bakery/1.16.0/) ### Added - [dev] Test coverage report ### Changed - Improved performance of `Baker.get_fields()` - [dev] Cleanup Sphinx documentation config - [dev] Update `pre-commit` config - [dev] CI: remove hard requirement on linters for tests to run ## [1.15.0](https://pypi.org/project/model-bakery/1.15.0/) ### Added - Add Python 3.12 support ### Changed - Revert erroneous optimisation of related logic (fix #439) - [dev] Bring tox back ### Removed ## [1.14.0](https://pypi.org/project/model-bakery/1.14.0/) ### Added - forward "_create_files" flag to child generators for relational fields ### Changed - Small improvements to `recipe.py::_mapping` - Improvements to `baker.py::bulk_create` - [dev] Replaced `pycodestyle`, `pydocstyle`, `flake8` and `isort` with `ruff` - [dev] Drop tox in favor of using GitHub Actions matrix ### Removed - Drop `baker.py::is_iterator` - Drop Python 3.7 support (reached end of life) ## [1.13.0](https://pypi.org/project/model-bakery/1.13.0/) ### Added - Add support for global seeding to baker random generation ## [1.12.0](https://pypi.org/project/model-bakery/1.12.0/) ### Added - Add support for CharField with max_length=None ### Changed - Fix utils.seq with start=0 ## [1.11.0](https://pypi.org/project/model-bakery/1.11.0/) ### Added - Add psycopg3 support for Django 4.2 ## [1.10.3](https://pypi.org/project/model-bakery/1.10.3/) ### Changed - Enforce Python 3.7 as a minimum version in project metadata ### Removed - dropped support for `FloatRangeField` as it was removed in Django 3.1 - [dev] Temporary drop Django 4.2 to package classifiers (waiting for build backend support) ## [1.10.2](https://pypi.org/project/model-bakery/1.10.2/) ### Changed - [dev] Test Python 3.11 with Django 4.2 - [dev] Add Django 4.2 to package classifiers ## [1.10.1](https://pypi.org/project/model-bakery/1.10.1/) ### Changed - [dev] Fix GitHub Action for publishing to PyPI ## [1.10.0](https://pypi.org/project/model-bakery/1.10.0/) ### Added - Django 4.2 support ### Changed - [dev] Switch to Python 3.11 release in CI - [dev] Unify and simplify tox config with tox-py - [dev] `pre-commit autoupdate && pre-commit run --all-files` - [dev] Run `pyupgrade` with Python 3.7 as a base - [dev] PEP 621: Migrate from setup.py and setup.cfg to pyproject.toml - [dev] Convert `format` and some string interpolations to `fstring` ## [1.9.0](https://pypi.org/project/model-bakery/1.9.0/) ### Changed - Fixed a bug with `seq` being passed a tz-aware start value [PR #353](https://github.com/model-bakers/model_bakery/pull/353) - Create m2m when using `_bulk_create=True` [PR #354](https://github.com/model-bakers/model_bakery/pull/354) - [dev] Use official postgis docker image in CI [PR #355](https://github.com/model-bakers/model_bakery/pull/355) ## [1.8.0](https://pypi.org/project/model-bakery/1.8.0/) ### Changed - Improve `Baker.get_fields()` to subtract lists instead of extra set cast [PR #352](https://github.com/model-bakers/model_bakery/pull/352) ## [1.7.1](https://pypi.org/project/model-bakery/1.7.1/) ### Changed - Remove warning for future Django deprecation [PR #339](https://github.com/model-bakers/model_bakery/pull/339) ## [1.7.0](https://pypi.org/project/model-bakery/1.7.0/) ### Changed - Fixed a bug with overwritten `_save_kwargs` and other custom arguments [PR #330](https://github.com/model-bakers/model_bakery/pull/330) ## [1.6.0](https://pypi.org/project/model-bakery/1.6.0/) ### Added - Python 3.11 support [PR #327](https://github.com/model-bakers/model_bakery/pull/327) - Django 4.1 support [PR #327](https://github.com/model-bakers/model_bakery/pull/327) - Added documentation for callables, iterables, sequences [PR #309](https://github.com/model-bakers/model_bakery/pull/309) ### Changed - [dev] Replace changelog reminder action with a custom solution that can ignore Dependabot PRs [PR #328](https://github.com/model-bakers/model_bakery/pull/328) ### Removed - Drop Python 3.6 support [PR #325](https://github.com/model-bakers/model_bakery/pull/325) - Drop Django 2.2 support [PR #326](https://github.com/model-bakers/model_bakery/pull/326) ## [1.5.0](https://pypi.org/project/model-bakery/1.5.0/) ### Added - Add py.typed export per [PEP 561](https://www.python.org/dev/peps/pep-0561/) [PR #158](https://github.com/model-bakers/model_bakery/pull/158) ### Changed - Extend type hints in `model_bakery.recipe` module, make `Recipe` class generic [PR #292](https://github.com/model-bakers/model_bakery/pull/292) - Explicitly add _fill_optional parameters to baker.make and baker.prepare to aid IDE autocomplete function. [PR #264](https://github.com/model-bakers/model_bakery/pull/264) - Fixed errors with reverse M2M relationships [PR #299](https://github.com/model-bakers/model_bakery/pull/299) - Fixed errors with reverse M2O relationships [PR #300](https://github.com/model-bakers/model_bakery/pull/300) - Improve exception message for unknown field types [PR #301](https://github.com/model-bakers/model_bakery/pull/301) - Fixed random generation of ContentType values when there is no database access [#290](https://github.com/model-bakers/model_bakery/pull/290) ## [1.4.0](https://pypi.org/project/model-bakery/1.4.0/) ### Added - Added postgis version to test settings - Add support for Python 3.10 [PR #244](https://github.com/model-bakers/model_bakery/pull/244) - Support for Django 4.0 [PR #236](https://github.com/model-bakers/model_bakery/pull/236) ### Changed - Validate `increment_by` parameter of `seq` helper when `value` is an instance of `datetime` [PR #247](https://github.com/model-bakers/model_bakery/pull/247) - Fix a simple typo in `bulk_create` disclaimer in docs [PR #245](https://github.com/model-bakers/model_bakery/pull/245) - Allow relation `_id` fields to use sequences [PR #253](https://github.com/model-bakers/model_bakery/pull/253) - Fix bulk_create not working with multi-database setup [PR #252](https://github.com/model-bakers/model_bakery/pull/252) - Conditionally support NullBooleanField, it's under deprecation and will be removed in Django 4.0 [PR #250](https://github.com/model-bakers/model_bakery/pull/250) - Fix Django max version pin in requirements file [PR #251](https://github.com/model-bakers/model_bakery/pull/251) - Improve type hinting to return the correct type depending on `_quantity` usage [PR #261](https://github.com/model-bakers/model_bakery/pull/261) ### Removed - Drop official Django 3.1 support. Django 2.2 is still supported, and 3.1 will likely keep working, but it’s not tested [PR #236](https://github.com/model-bakers/model_bakery/pull/236) ## [1.3.3](https://pypi.org/project/model-bakery/1.3.3/) ### Added - `_bulk_create` flag is not populating related objects as well [PR #206](https://github.com/model-bakers/model_bakery/pull/206) - Add support for iterators on GFK fields when using _quantity param [PR #207](https://github.com/model-bakers/model_bakery/pull/207) - Add support for iterators on many-to-many fields [PR#237](https://github.com/model-bakers/model_bakery/pull/237) ### Changed - Fix typos in Recipes documentation page [PR #212](https://github.com/model-bakers/model_bakery/pull/212) - Add `venv` to ignored folders of `flake8` and `pydocstyle` [PR#214](https://github.com/model-bakers/model_bakery/pull/214) - Run `flake8` after code modifications when linting [PR#214](https://github.com/model-bakers/model_bakery/pull/214) - Add typing for `baker.make` and `baker.prepare` [PR#213](https://github.com/model-bakers/model_bakery/pull/213) ## [1.3.2](https://pypi.org/project/model-bakery/1.3.2/) ### Changed - Fixed a bug (introduced in [1.2.1](https://pypi.org/project/model-bakery/1.2.1/)) that was breaking imports of recipes from non-installed-app modules [PR #201](https://github.com/model-bakers/model_bakery/pull/201) - Dependencies updates ## [1.3.1](https://pypi.org/project/model-bakery/1.3.1/) ### Added - [dev] Add explanations to imports in `generators.py` to match with current supported Django versions [PR #179](https://github.com/model-bakers/model_bakery/pull/179) ### Changed - Fix `requirements.txt` to cover Django 3.2 (everything from 2.2 till 4.0) [PR #182](https://github.com/model-bakers/model_bakery/pull/182) ## [1.3.0](https://pypi.org/project/model-bakery/1.3.0/) ### Added - Add Django 3.2 LTS support [PR #176](https://github.com/model-bakers/model_bakery/pull/176) - Add new `_bulk_create` parameter to `make` for using Django manager `bulk_create` with `_quantity` [PR #134](https://github.com/model-bakers/model_bakery/pull/134) - Add the functionality to import Django models using the `app_name.ModelName` convention in `import_from_str` [PR #140](https://github.com/model-bakers/model_bakery/pull/140) - Add the functionality to import recipes using `app_name.recipe_name` [PR #140](https://github.com/model-bakers/model_bakery/pull/140) - Add new `one_to_one` parameter to `foreign_key` to allow usage of `_quantity` for recipes based on models with OneToOne fields [PR #169](https://github.com/model-bakers/model_bakery/pull/169) - [docs] Improved documentation on Recipe's import string [PR #175](https://github.com/model-bakers/model_bakery/pull/175/) - [dev] Add a unit test for `utils.seq` [PR #143](https://github.com/model-bakers/model_bakery/pull/143) - [dev] Run CI against `main` Django branch to cover possible upcoming changes/deprecations [PR #159](https://github.com/model-bakers/model_bakery/pull/159) - [dev] Add GH Action for package releasing [PR #168](https://github.com/model-bakers/model_bakery/pull/168) ### Changed - Fixed a bug (introduced in 1.2.1) that was breaking creation of model instances with related model fields [PR #164](https://github.com/model-bakers/model_bakery/pull/164) - Type hinting fixed for Recipe "_model" parameter [PR #124](https://github.com/model-bakers/model_bakery/pull/124) - Dependencies updates from [dependabot](https://dependabot.com/) PRs [#170](https://github.com/model-bakers/model_bakery/pull/170) - [#171](https://github.com/model-bakers/model_bakery/pull/171) - [#172](https://github.com/model-bakers/model_bakery/pull/172) - [#173](https://github.com/model-bakers/model_bakery/pull/173) - [#174](https://github.com/model-bakers/model_bakery/pull/174) - [dev] Modify `setup.py` to not import the whole module for package data, but get it from `__about__.py` [PR #142](https://github.com/model-bakers/model_bakery/pull/142) - [dev] Add Dependabot config file [PR #146](https://github.com/model-bakers/model_bakery/pull/146) - [dev] Update Dependabot config file to support GH Actions and auto-rebase [PR #160](https://github.com/model-bakers/model_bakery/pull/160) ### Removed - `model_bakery.timezone.now` fallback (use `django.utils.timezone.now` instead) [PR #141](https://github.com/model-bakers/model_bakery/pull/141) - `model_bakery.timezone.smart_datetime` function (directly use `model_bakery.timezone.tz_aware` instead) [PR #147](https://github.com/model-bakers/model_bakery/pull/147) - Remove all signs of Django 1.11 (as we dropped it in 1.2.1) [PR #157](https://github.com/model-bakers/model_bakery/pull/157) - Drop unsupported Django 3.0 from CI (https://www.djangoproject.com/download/#unsupported-versions) [PR #176](https://github.com/model-bakers/model_bakery/pull/176) ## [1.2.1](https://pypi.org/project/model-bakery/1.2.1/) ### Added - Add ability to pass `str` values to `foreign_key` for recipes from other modules [PR #120](https://github.com/model-bakers/model_bakery/pull/120) - Add new parameter `_using` to support multi database Django applications [PR #126](https://github.com/model-bakers/model_bakery/pull/126) - [dev] Add instructions and script for running `postgres` and `postgis` tests. [PR #118](https://github.com/model-bakers/model_bakery/pull/118) ### Changed - Fixed _model parameter annotations [PR #115](https://github.com/model-bakers/model_bakery/pull/115) - Fixes bug when field has callable `default` [PR #117](https://github.com/model-bakers/model_bakery/pull/117) ### Removed - [dev] Drop Python 3.5 support as it is retired (https://www.python.org/downloads/release/python-3510/) [PR #119](https://github.com/model-bakers/model_bakery/pull/119) - [dev] Remove support for Django<2.2 ([more about Django supported versions](https://www.djangoproject.com/download/#supported-versions)) [PR #126](https://github.com/model-bakers/model_bakery/pull/126) ## [1.2.0](https://pypi.org/project/model-bakery/1.2.0/) ### Added - Support to django 3.1 `JSONField` [PR #85](https://github.com/model-bakers/model_bakery/pull/85) and [PR #106](https://github.com/model-bakers/model_bakery/pull/106) - Added type annotations [PR #100](https://github.com/model-bakers/model_bakery/pull/100) - Support for Python 3.9 [PR #113](https://github.com/model-bakers/model_bakery/pull/113/) - [dev] Changelog reminder (GitHub action) - Add pytest example ### Changed - Support for `prefix` in `seq` values ([PR #111](https://github.com/model-bakers/model_bakery/pull/111) fixes [Issue #93](https://github.com/model-bakers/model_bakery/issues/93)) - [dev] CI switched to GitHub Actions - [dev] Freeze dev requirements - [dev] Add Django 3.1 to test matrix [PR #103](https://github.com/model-bakers/model_bakery/pull/103) and [PR #112](https://github.com/model-bakers/model_bakery/pull/112) - [dev] pre-commit to use local packages (so versions will match) - [dev] consistent use of pydocstyle - [dev] Updates to MANIFEST.in - [dev] Correct field in recipe docs - [dev] Adjust imports for Django 3.1 compatibility [PR #112](https://github.com/model-bakers/model_bakery/pull/112) ### Removed ## [1.1.1](https://pypi.org/project/model-bakery/1.1.1/) ### Added - Support to Postgres fields: `DecimalRangeField`, `FloatRangeField`, `IntegerRangeField`, `BigIntegerRangeField`, `DateRangeField`, `DateTimeRangeField` [PR #80](https://github.com/model-bakers/model_bakery/pull/80) ### Changed - Add isort and fix imports [PR #77](https://github.com/model-bakers/model_bakery/pull/77) - Enable `seq` to be imported from `baker` [PR #76](https://github.com/model-bakers/model_bakery/pull/76) - Fix PostGIS model registration [PR #67](https://github.com/model-bakers/model_bakery/pull/67) ### Removed ## [1.1.0](https://pypi.org/project/model-bakery/1.1.0/) ### Added - Django 3.0 and Python 3.8 to CI [PR #48](https://github.com/model-bakers/model_bakery/pull/48/) ### Changed - Improve code comments [PR #31](https://github.com/model-bakers/model_bakery/pull/31) - Switch to tox-travis [PR #43](https://github.com/model-bakers/model_bakery/pull/43) - Add black job [PR #42](https://github.com/model-bakers/model_bakery/pull/42) - README.md instead of rst [PR #44](https://github.com/model-bakers/model_bakery/pull/44) - New `start` argument in `baker.seq` [PR #56](https://github.com/model-bakers/model_bakery/pull/56) - Fixes bug when registering custom fields generator via `settings.py` [PR #58](https://github.com/model-bakers/model_bakery/pull/58) - The different IntegerField types now will generate values on their min/max range [PR #59](https://github.com/model-bakers/model_bakery/pull/59) ### Removed ## [1.0.2](https://pypi.org/project/model-bakery/1.0.2/) ### Added ### Changed - Improvements on the migrations script ### Removed ## [1.0.1](https://pypi.org/project/model-bakery/1.0.1/) ### Added - Python script to help developers on migrating from Model Mommy to Model Bakery ### Changed ### Removed ## [1.0.0](https://pypi.org/project/model-bakery/1.0.0/) ### Added ### Changed - Rename model_mommy code to model_bakery ### Removed model_bakery-1.20.4/CONTRIBUTING.md000066400000000000000000000024251475767230300165050ustar00rootroot00000000000000## How to Contribute This page list all the steps you need to follow to set up `model_bakery` and be able to code it. Here they are: 1. [Fork this repo](https://github.com/model-bakers/model_bakery/fork) and clone it to your computer: ``` git clone git@github.com:YOUR_USER/model_bakery.git ``` 2. Install the dev dependencies: ``` pip install .[test] ``` 3. Change the code and run your tests with: ``` make test ``` 4. We use [pre-commit](https://pre-commit.com/) to ensure a unique code formatting for the project. But, if you ran into any CI issues with that, make sure your code changes respect it: ``` make lint ``` If you don't follow the step 4, your PR may fail due to `black` or `ruff` warnings. To run `postgresql` and `postgis` specific tests: 1. [Install `docker`](https://docs.docker.com/get-docker/). 2. Install the `postgis` dependencies. Follow the [instructions from the Django docs](https://docs.djangoproject.com/en/stable/ref/contrib/gis/install/geolibs/): If you are on Ubuntu/Debian you run the following: ```shell sudo apt update -y && sudo apt install -y binutils libproj-dev gdal-bin ``` 3. Run the following script: ```shell ./postgis-tests.sh ``` That will spin up a `docker` container with `postgresql` and `postgis` enabled and run the full test suite. model_bakery-1.20.4/LICENSE000066400000000000000000000011431475767230300152550ustar00rootroot00000000000000Copyright 2019 Vanderson Mota and individual contributors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. model_bakery-1.20.4/MANIFEST.in000066400000000000000000000003541475767230300160110ustar00rootroot00000000000000include model_bakery/mock_file.txt include model_bakery/mock_img.jpeg include LICENSE include README.md include CHANGELOG.md include CONTRIBUTING.md recursive-include tests *.py recursive-include docs *.rst recursive-include utils *.py model_bakery-1.20.4/Makefile000066400000000000000000000004261475767230300157130ustar00rootroot00000000000000help: @echo "Available Targets:" @cat Makefile | egrep '^(\w+?):' | sed 's/:\(.*\)//g' | sed 's/^/- /g' test: @python -m pytest release: @python setup.py sdist bdist_wheel @twine upload dist/* lint: @black . @ruff . @mypy model_bakery .PHONY: help test release lint model_bakery-1.20.4/README.md000066400000000000000000000052521475767230300155340ustar00rootroot00000000000000# Model Bakery: Smart fixtures for better tests [![Build](https://img.shields.io/github/actions/workflow/status/model-bakers/model_bakery/ci.yml?branch=main)](https://github.com/model-bakers/model_bakery/actions?workflow=Tests) [![Coverage](https://img.shields.io/badge/Coverage-97%25-success)](https://github.com/model-bakers/model_bakery/actions?workflow=Tests) [![Latest PyPI version](https://img.shields.io/pypi/v/model_bakery.svg)](https://pypi.python.org/pypi/model_bakery/) [![Documentation Status](https://readthedocs.org/projects/model-bakery/badge/?version=latest)](https://model-bakery.readthedocs.io/en/latest/?badge=latest) *Model Bakery* offers you a smart way to create fixtures for testing in Django. With a simple and powerful API you can create many objects with a single line of code. Model Bakery is a rename of the legacy [Model Mommy project](https://pypi.org/project/model_mommy/). ## Install ```bash pip install model-bakery ``` ## Usage and Info ### Basic usage ```python # models.py from django.db import models class Customer(models.Model): enjoy_jards_macale = models.BooleanField() name = models.CharField(max_length=30) email = models.EmailField() age = models.IntegerField() bio = models.TextField() days_since_last_login = models.BigIntegerField() birthday = models.DateField() last_shopping = models.DateTimeField() # test_models.py from django.test import TestCase from model_bakery import baker from pprint import pprint class TestCustomerModel(TestCase): def setUp(self): self.customer = baker.make('shop.Customer') pprint(self.customer.__dict__) """ {'_state': , 'age': 3841, 'bio': 'vUFzMUMyKzlnTyiCxfgODIhrnkjzgQwHtzIbtnVDKflqevczfnaOACkDNqvCHwvtWdLwoiKrCqfppAlogSLECtMmfleeveyqefkGyTGnpbkVQTtviQVDESpXascHAluGHYEotSypSiHvHzFteKIcUebrzUVigiOacfnGdvijEPrZdSCIIBjuXZMaWLrMXyrsUCdKPLRBRYklRdtZhgtxuASXdhNGhDsrnPHrYRClhrSJSVFojMkUHBvSZhoXoCrTfHsAjenCEHvcLeCecsXwXgWJcnJPSFdOmOpiHRnhSgRF', 'birthday': datetime.date(2019, 12, 3), 'enjoy_jards_macale': True, 'id': 1, 'last_shopping': datetime.datetime(2019, 12, 3, 21, 42, 34, 77019), 'name': 'qiayYnESvqcYLLBzxpFOcGBIfnQEPx', 'days_since_last_login': 6016} """ ``` Check out [documentation]() for more complete examples. ## Contributing Detailed info [here](https://github.com/model-bakers/model_bakery/blob/main/CONTRIBUTING.md). ## Maintainers - [Bernardo Fontes](https://github.com/berinhard/) - [Rustem Saiargaliev](https://github.com/amureki/) - [Tim Klein](https://github.com/timjklein36) ## Creator - [Vanderson Mota](https://github.com/vandersonmota/) model_bakery-1.20.4/SECURITY.md000066400000000000000000000003241475767230300160410ustar00rootroot00000000000000# Security Policy ## Security contact information To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. model_bakery-1.20.4/docs/000077500000000000000000000000001475767230300152015ustar00rootroot00000000000000model_bakery-1.20.4/docs/basic_usage.md000066400000000000000000000227051475767230300177760ustar00rootroot00000000000000# Basic Usage Let's say you have an app **shop** with a model like this: File: **models.py** ```python class Customer(models.Model): """ Model class Customer of shop app """ enjoy_jards_macale = models.BooleanField() name = models.CharField(max_length=30) email = models.EmailField() age = models.IntegerField() bio = models.TextField() days_since_last_login = models.BigIntegerField() birthday = models.DateField() last_shopping = models.DateTimeField() ``` To create a persisted instance, just call Model Bakery: File: **test_models.py** ```python #Core Django imports from django.test import TestCase #Third-party app imports from model_bakery import baker from shop.models import Customer class CustomerTestModel(TestCase): """ Class to test the model Customer """ def setUp(self): self.customer = baker.make(Customer) ``` Importing every model over and over again is boring. So let Model Bakery import them for you: ```python from model_bakery import baker # 1st form: app_label.model_name customer = baker.make('shop.Customer') # 2nd form: model_name product = baker.make('Product') ``` ```{note} You can only use the 2nd form on unique model names. If you have an app shop with a Product, and an app stock with a Product, you must use the app_label.model_name form. ``` ```{note} model_name is case insensitive. ``` ## Model Relationships Model Bakery also handles relationships. Let's say the customer has a purchase history: File: **models.py** ```python class Customer(models.Model): """ Model class Customer of shop app """ enjoy_jards_macale = models.BooleanField() name = models.CharField(max_length=30) email = models.EmailField() age = models.IntegerField() bio = models.TextField() days_since_last_login = models.BigIntegerField() birthday = models.DateField() appointment = models.DateTimeField() class PurchaseHistory(models.Model): """ Model class PurchaseHistory of shop app """ customer = models.ForeignKey('Customer') products = models.ManyToManyField('Product') year = models.IntegerField() ``` You can use Model Bakery as: ```python from django.test import TestCase from model_bakery import baker class PurchaseHistoryTestModel(TestCase): def setUp(self): self.history = baker.make('shop.PurchaseHistory') print(self.history.customer) ``` It will also create the Customer, automagically. **NOTE: ForeignKeys and OneToOneFields** - Since Django 1.8, ForeignKey and OneToOne fields don't accept unpersisted model instances anymore. This means that if you run: ```python baker.prepare('shop.PurchaseHistory') ``` You'll end up with a persisted "Customer" instance. ## M2M Relationships By default, Model Bakery doesn't create related instances for many-to-many relationships. If you want them to be created, you have to turn it on as the following: ```python from django.test import TestCase from model_bakery import baker class PurchaseHistoryTestModel(TestCase): def setUp(self): self.history = baker.make('shop.PurchaseHistory', make_m2m=True) print(self.history.products.count()) ``` ## Explicit M2M Relationships If you want to, you can prepare your own set of related object and pass it to Model Bakery. Here's an example: ```python products_set = baker.prepare(Product, _quantity=5) history = baker.make(PurchaseHistory, products=products_set) ``` ## Explicit values for fields By default, Model Bakery uses random values to populate the model's fields. But it's possible to explicitly set values for them as well. ```python from django.test import TestCase from model_bakery import baker class CustomerTestModel(TestCase): def setUp(self): self.customer = baker.make( 'shop.Customer', age=21 ) self.older_customer = baker.make( 'shop.Customer', age=42 ) ``` You can use callable to explicitly set values as: ```python import random from django.test import TestCase from model_bakery import baker class CustomerTestModel(TestCase): def get_random_name(self): return random.choice(["Suraj Magdum", "Avadhut More", "Rohit Chile"]) def setUp(self): self.customer = baker.make( 'shop.Customer', age=21, name = self.get_random_name ) ``` You can also use iterable to explicitly set values as: ```python from django.test import TestCase from model_bakery import baker class CustomerTestModel(TestCase): def setUp(self): names = ("Onkar Awale", "Pruthviraj Patil", "Shubham Ojha") self.customer = baker.make( 'shop.Customer', age=21, name = itertools.cycle(names) ) ``` Sometimes, you have a field with an unique value and using `make` can cause random errors. Also, passing an attribute value just to avoid uniqueness validation problems can be tedious. To solve this you can define a sequence with `seq` ```python from django.test import TestCase from model_bakery import baker from model_bakery.recipe import seq class CustomerTestModel(TestCase): def setUp(self): self.customer = baker.make( 'shop.Customer', age=21, name = seq('Joe') ) ``` Related objects fields are also reachable by their name or related names in a very similar way as Django does with [field lookups](https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups): ```python from django.test import TestCase from model_bakery import baker class PurchaseHistoryTestModel(TestCase): def setUp(self): self.bob_history = baker.make( 'shop.PurchaseHistory', customer__name='Bob' ) ``` ## Creating Files Model Bakery does not create files for FileField types. If you need to have the files created, you can pass the flag `_create_files=True` (defaults to `False`) to either `baker.make` or `baker.make_recipe`. **Important**: the lib does not do any kind of file clean up, so it's up to you to delete the files created by it. ## Refreshing Instances After Creation By default, Model Bakery does not refresh the instance after it is created and saved. If you want to refresh the instance after it is created, you can pass the flag `_refresh_after_create=True` to either `baker.make` or `baker.make_recipe`. This ensures that any changes made by the database or signal handlers are reflected in the instance. ```python from model_bakery import baker # default behavior customer = baker.make('shop.Customer', birthday='1990-01-01', _refresh_after_create=False) assert customer.birthday == '1990-01-01' customer = baker.make('shop.Customer', birthday='1990-01-01', _refresh_after_create=True) assert customer.birthday == datetime.date(1990, 1, 1) ``` ## Non persistent objects If you don't need a persisted object, Model Bakery can handle this for you as well with the **prepare** method: ```python from model_bakery import baker customer = baker.prepare('shop.Customer') ``` It works like `make` method, but it doesn't persist the instance neither the related instances. If you want to persist only the related instances but not your model, you can use the `_save_related` parameter for it: ```python from model_bakery import baker history = baker.prepare('shop.PurchaseHistory', _save_related=True) assert history.id is None assert bool(history.customer.id) is True ``` ## More than one instance If you need to create more than one instance of the model, you can use the `_quantity` parameter for it: ```python from model_bakery import baker customers = baker.make('shop.Customer', _quantity=3) assert len(customers) == 3 ``` It also works with `prepare`: ```python from model_bakery import baker customers = baker.prepare('shop.Customer', _quantity=3) assert len(customers) == 3 ``` The `make` method also accepts a parameter `_bulk_create` to use Django's [bulk_create](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#bulk-create) method instead of calling `obj.save()` for each created instance. ```{note} Django's `bulk_create` does not update the created object primary key as explained in their docs. Because of that, there's no way for model-bakery to avoid calling `save` method for all the foreign keys. But this behavior can depends on which Django version and database backend you're using. So, for example, if you're trying to create 20 instances of a model with a foreign key using `_bulk_create` this will result in 21 queries (20 for each foreign key object and one to bulk create your 20 instances). ``` If you want to avoid that, you'll have to perform individual bulk creations per foreign keys as the following example: ```python from model_bakery import baker baker.prepare(User, _quantity=5, _bulk_create=True) user_iter = User.objects.all().iterator() baker.prepare(Profile, user=user_iter, _quantity=5, _bulk_create=True) ``` ## Multi-database support Model Bakery supports django application with more than one database. If you want to determine which database bakery should use, you have the `_using` parameter: ```python from model_bakery import baker custom_db = "your_custom_db" assert custom_db in settings.DATABASES history = baker.make('shop.PurchaseHistory', _using=custom_db) assert history in PurchaseHistory.objects.using(custom_db).all() assert history.customer in Customer.objects.using(custom_db).all() # default database tables with no data assert not PurchaseHistory.objects.exists() assert not Customer.objects.exists() ``` model_bakery-1.20.4/docs/conf.py000066400000000000000000000007321475767230300165020ustar00rootroot00000000000000import os import sys from model_bakery import __about__ sys.path.insert(0, os.path.abspath("..")) project = "Model Bakery" copyright = "2023, Rust Saiargaliev" author = "Rust Saiargaliev" version = release = __about__.__version__ extensions = [ "myst_parser", ] myst_enable_extensions = [ "colon_fence", ] source_suffix = [".rst", ".md"] templates_path = ["_templates"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] html_theme = "sphinx_rtd_theme" model_bakery-1.20.4/docs/how_bakery_behaves.md000066400000000000000000000112111475767230300213460ustar00rootroot00000000000000# How Model Bakery behaves? By default, Model Bakery skips fields with `null=True` or `blank=True`. Also if a field has a `default` value, it will be used. You can override this behavior by: 1. Explicitly defining values ```python # from "Basic Usage" page, assume all fields either null=True or blank=True from model_bakery import baker customer = baker.make('shop.Customer', enjoy_jards_macale=True, bio="A fan of Jards Malacé") ``` 2. Passing `_fill_optional` with a list of fields to fill with random data ```python customer = baker.make('shop.Customer', _fill_optional=['enjoy_jards_macale', 'bio']) ``` 3. Passing `_fill_optional=True` to fill all fields with random data ```python customer = baker.make('shop.Customer', _fill_optional=True) ``` ## When shouldn't you let Baker generate things for you? If you have fields with special validation, you should set their values by yourself. Model Bakery should handle fields that: 1. don't matter for the test you're writing; 2. don't require special validation (like unique, etc); 3. are required to create the object. ## Currently supported fields - `BooleanField`, `IntegerField`, `BigIntegerField`, `SmallIntegerField`, `PositiveIntegerField`, `PositiveBigIntegerField`, `PositiveSmallIntegerField`, `FloatField`, `DecimalField` - `CharField`, `TextField`, `BinaryField`, `SlugField`, `URLField`, `EmailField`, `IPAddressField`, `GenericIPAddressField`, `ContentType` - `ForeignKey`, `OneToOneField`, `ManyToManyField` (even with through model) - `DateField`, `DateTimeField`, `TimeField`, `DurationField` - `FileField`, `ImageField` - `JSONField`, `ArrayField`, `HStoreField` - `CICharField`, `CIEmailField`, `CITextField` - `DecimalRangeField`, `IntegerRangeField`, `BigIntegerRangeField`, `DateRangeField`, `DateTimeRangeField` Require `django.contrib.gis` in `INSTALLED_APPS`: - `GeometryField`, `PointField`, `LineStringField`, `PolygonField`, `MultiPointField`, `MultiLineStringField`, `MultiPolygonField`, `GeometryCollectionField` ## Custom fields Model Bakery allows you to define generators methods for your custom fields or overrides its default generators. This can be achieved by specifying the field and generator function for the `generators.add` function. It can also be done by specifying the field and generator function in the `BAKER_CUSTOM_FIELDS_GEN` setting. Both can be the real python objects imported in settings or just specified as import path string. Examples: ```python from model_bakery import baker def gen_func(): return 'value' baker.generators.add('test.generic.fields.CustomField', gen_func) ``` ```python # in the module code.path: def gen_func(): return 'value' # in your tests.py file: from model_bakery import baker baker.generators.add('test.generic.fields.CustomField', 'code.path.gen_func') ``` ```python # in your settings.py file: BAKER_CUSTOM_FIELDS_GEN = { 'test.generic.fields.CustomField': 'code.path.gen_func', } ``` ## Customizing Baker In some rare cases, you might need to customize the way Baker base class behaves. This can be achieved by creating a new class and specifying it in your settings files. It is likely that you will want to extend Baker, however the minimum requirement is that the custom class have `make` and `prepare` functions. In order for the custom class to be used, make sure to use the `model_bakery.baker.make` and `model_bakery.baker.prepare` functions, and not `model_bakery.baker.Baker` directly. Examples: ```python # in the module code.path: class CustomBaker(baker.Baker): def get_fields(self): return [ field for field in super(CustomBaker, self).get_fields() if not isinstance(field, CustomField) ] # in your settings.py file: BAKER_CUSTOM_CLASS = 'code.path.CustomBaker' ``` Additionally, if you want to your created instance to be returned respecting one of your custom ModelManagers, you can use the `_from_manager` parameter as the example below: ```python movie = baker.make(Movie, title='Old Boys', _from_manager='availables') # This will use the Movie.availables model manager ``` ## Save method custom parameters If you have overwritten the `save` method for a model, you can pass custom parameters to it using Model Bakery. Example: ```python class ProjectWithCustomSave(models.Model): # some model fields created_by = models.ForeignKey(settings.AUTH_USER_MODEL) def save(self, user, *args, **kwargs): self.created_by = user return super(ProjectWithCustomSave, self).save(*args, **kwargs) #with model baker: user = baker.make(settings.AUTH_USER_MODEL) project = baker.make(ProjectWithCustomSave, _save_kwargs={'user': user}) assert user == project.user ``` model_bakery-1.20.4/docs/index.md000066400000000000000000000025671475767230300166440ustar00rootroot00000000000000# Model Bakery: Smart fixtures for better tests Model Bakery offers you a smart way to create fixtures for testing in Django. With a simple and powerful API, you can create many objects with a single line of code. Model Bakery is a rename of the legacy [model_mommy\'s project](https://pypi.org/project/model_mommy/). This is because the project\'s creator and maintainers decided to not reinforce gender stereotypes for women in technology. You can read more about this subject[here](https://witi.com/articles/1017/How-Gender-Stereotypes-are-Still-Affecting-Women-in-Tech/). # Compatibility Model Bakery supports Django \>= 4.2. # Install Install it with `pip` ```console $ pip install model-bakery ``` # Contributing to Model Bakery As an open-source project, Model Bakery welcomes contributions of many forms. Examples of contributions include: - Code Patches - Documentation improvements - Bug reports Take a look in our [GitHub repo](https://github.com/model-bakers/model_bakery/blob/main/CONTRIBUTING.md) for more instructions on how to set up your local environment to help Model Bakery to grow. # Doubts? Loved it? Hated it? Suggestions? Feel free to [open an issue](https://github.com/model-bakers/model_bakery/issues/new) for support, development or ideas! ## Contents ```{toctree} :maxdepth: 4 basic_usage recipes how_bakery_behaves test_runners migrating_from_mommy ``` model_bakery-1.20.4/docs/migrating_from_mommy.md000066400000000000000000000015721475767230300217520ustar00rootroot00000000000000# Migrating from Model Mommy Model Bakery has a [Python script](https://github.com/model-bakers/model_bakery/blob/main/utils/from_mommy_to_bakery.py) to help you to migrate your project\'s test code from Model Mommy to Model Bakery. This script will rename recipe files and replace legacy imports by the new ones. **From your project\'s root dir**, execute the following commands: ```console $ pip uninstall model_mommy $ pip install model_bakery $ wget https://raw.githubusercontent.com/model-bakers/model_bakery/main/utils/from_mommy_to_bakery.py $ python from_mommy_to_bakery.py --dry-run # will list the files that'll be changed $ python from_mommy_to_bakery.py # migrate from model_mommy to model_bakery $ python manage.py test ``` This command will only migrate `*.py` files. Any other file type such as `tox.ini`, `requirements.txt` etc, have to be updated manually. model_bakery-1.20.4/docs/recipes.md000066400000000000000000000234401475767230300171600ustar00rootroot00000000000000# Recipes If you're not comfortable with random data or even if you just want to improve the semantics of the generated data, there's hope for you. You can define a **Recipe**, which is a set of rules to generate data for your models. It's also possible to store the Recipes in a module called *baker_recipes.py* at your app's root directory. This recipes can later be used with the `make_recipe` function: ``` shop/ migrations/ __init__.py admin.py apps.py baker_recipes.py <--- where you should place your Recipes models.py tests.py views.py ``` File: **baker_recipes.py** ```python from model_bakery.recipe import Recipe from shop.models import Customer customer_joe = Recipe( Customer, name='John Doe', nickname='joe', age=18, birthday=date.today(), last_shopping=datetime.now() ) ``` ```{note} You don't have to declare all the fields if you don't want to. Omitted fields will be generated automatically. ``` File: **test_model.py** ```python from django.test import TestCase from model_bakery import baker class CustomerTestModel(TestCase): def setUp(self): # Load the recipe 'customer_joe' from 'shop/baker_recipes.py' self.customer_one = baker.make_recipe( 'shop.customer_joe' ) ``` Or if you don't want a persisted instance: ```python from model_bakery import baker baker.prepare_recipe('shop.customer_joe') ``` ```{note} You don't have to place necessarily your `baker_recipes.py` file inside your app's root directory. If you have a tests directory within the app, for example, you can add your recipes inside it and still use `make_recipe`/`prepare_recipe` by adding the tests module to the string you've passed as an argument. For example: `baker.make_recipe("shop.tests.customer_joe")` So, short summary, you can place your `baker_recipes.py` **anywhere** you want to and to use it having in mind you'll only have to simulate an import but obfuscating the `baker_recipes` module from the import string. ``` ```{note} You can use the \_quantity parameter as well if you want to create more than one object from a single recipe. ``` You can define recipes locally to your module or test case as well. This can be useful for cases where a particular set of values may be unique to a particular test case, but used repeatedly there. For example: File: **baker_recipes.py** ```python company_recipe = Recipe(Company, name='WidgetCo') ``` File: **test_model.py** ```python class EmployeeTest(TestCase): def setUp(self): self.employee_recipe = Recipe( Employee, name=seq('Employee '), company=baker.make_recipe('app.company_recipe') ) def test_employee_list(self): self.employee_recipe.make(_quantity=3) # test stuff.... def test_employee_tasks(self): employee1 = self.employee_recipe.make() task_recipe = Recipe(Task, employee=employee1) task_recipe.make(status='done') task_recipe.make(due_date=datetime(2014, 1, 1)) # test stuff.... ``` ## Recipes with foreign keys You can define `foreign_key` relations: ```python from model_bakery.recipe import Recipe, foreign_key from shop.models import Customer, PurchaseHistory customer = Recipe(Customer, name='John Doe', nickname='joe', age=18, birthday=date.today(), appointment=datetime.now() ) history = Recipe(PurchaseHistory, owner=foreign_key(customer) ) ``` Notice that `customer` is a *recipe*. You may be thinking: "I can put the Customer model instance directly in the owner field". That's not recommended. Using the `foreign_key` is important for 2 reasons: - Semantics. You'll know that attribute is a foreign key when you're reading; - The associated instance will be created only when you call `make_recipe` and not during recipe definition; You can also use `related`, when you want two or more models to share the same parent: ```python from model_bakery.recipe import related, Recipe from shop.models import Customer, PurchaseHistory history = Recipe(PurchaseHistory) customer_with_2_histories = Recipe(Customer, name='Albert', purchasehistory_set=related('history', 'history'), ) ``` Note this will only work when calling `make_recipe` because the related manager requires the objects in the related_set to be persisted. That said, calling `prepare_recipe` the related_set will be empty. If you want to set m2m relationship you can use `related` as well: ```python from model_bakery.recipe import related, Recipe pencil = Recipe(Product, name='Pencil') pen = Recipe(Product, name='Pen') history = Recipe(PurchaseHistory) history_with_prods = history.extend( products=related(pencil, pen) ) ``` When creating models based on a `foreign_key` recipe using the `_quantity` argument, only one related model will be created for all new instances. ```python from model_baker.recipe import foreign_key, Recipe person = Recipe(Person, name='Albert') dog = Recipe(Dog, owner=foreign_key(person)) # All dogs share the same owner dogs = dog.make_recipe(_quantity=2) assert dogs[0].owner.id == dogs[1].owner.id ``` This will cause an issue if your models use `OneToOneField`. In that case, you can provide `one_to_one=True` to the recipe to make sure every instance created by `_quantity` has a unique id. ```python from model_baker.recipe import foreign_key, Recipe person = Recipe(Person, name='Albert') dog = Recipe(Dog, owner=foreign_key(person, one_to_one=True)) # Each dog has a unique owner dogs = dog.make_recipe(_quantity=2) assert dogs[0].owner.id != dogs[1].owner.id ``` ## Recipes with callables It's possible to use `callables` as recipe's attribute value. ```python from datetime import date from model_bakery.recipe import Recipe from shop.models import Customer customer = Recipe( Customer, birthday=date.today, ) ``` When you call `make_recipe`, Model Bakery will set the attribute to the value returned by the callable. ## Recipes with iterators You can also use *iterators* (including *generators*) to provide multiple values to a recipe. ```python from itertools import cycle names = ['Ada Lovelace', 'Grace Hopper', 'Ida Rhodes', 'Barbara Liskov'] customer = Recipe(Customer, name=cycle(names) ) ``` Model Bakery will use the next value in the *iterator* every time you create a model from the recipe. ## Sequences in recipes Sometimes, you have a field with an unique value and using `make` can cause random errors. Also, passing an attribute value just to avoid uniqueness validation problems can be tedious. To solve this you can define a sequence with `seq` ```python from model_bakery.recipe import Recipe, seq from shop.models import Customer CustomerRecipe = Recipe(Customer, name=seq('Joe'), age=seq(15) ) customer = baker.make_recipe('shop.CustomerRecipe') customer.name # 'Joe1' customer.age # 16 new_customer = baker.make_recipe('shop.CustomerRecipe') new_customer.name # 'Joe2' new_customer.age # 17 ``` This will append a counter to strings to avoid uniqueness problems, and it will sum the counter with numerical values. An optional `suffix` parameter can be supplied to augment the value for cases like generating emails or other strings with common suffixes. ```python from model_bakery import.recipe import Recipe, seq from shop.models import Customer CustomerRecipe = Recipe(Customer, email=seq('user', suffix='@example.com')) customer = baker.make_recipe('shop.CustomerRecipe') customer.email # 'user1@example.com' customer = baker.make_recipe('shop.CustomerRecipe') customer.email # 'user2@example.com' ``` Sequences and iterables can be used not only for recipes, but with `baker` as well: ```python from model_bakery import baker customer = baker.make('Customer', name=baker.seq('Joe')) customer.name # 'Joe1' customers = baker.make('Customer', name=baker.seq('Chad'), _quantity=3) for customer in customers: print(customer.name) # 'Chad1' # 'Chad2' # 'Chad3' ``` You can also provide an optional `increment_by` argument which will modify incrementing behaviour. This can be an integer, float, Decimal or timedelta. If you want to start your increment differently, you can use the `start` argument, only if it's not a sequence for `date`, `datetime` or `time` objects. ```python from datetime import date, timedelta from model_bakery.recipe import Recipe, seq from shop.models import Customer CustomerRecipe = Recipe(Customer, age=seq(15, increment_by=3) height_ft=seq(5.5, increment_by=.25) # assume today's date is 21/07/2014 appointment=seq(date(2014, 7, 21), timedelta(days=1)), name=seq('Custom num: ', increment_by=2, start=5), ) customer = baker.make_recipe('shop.CustomerRecipe') customer.age # 18 customer.height_ft # 5.75 customer.appointment # datetime.date(2014, 7, 22) customer.name # 'Custom num: 5' new_customer = baker.make_recipe('shop.CustomerRecipe') new_customer.age # 21 new_customer.height_ft # 6.0 new_customer.appointment # datetime.date(2014, 7, 23) customer.name # 'Custom num: 7' ``` Be aware that `seq` may query the database to determine when to reset. Therefore, a `SimpleTestCase` test method (which disallows database access) can call `prepare_recipe` on a Recipe with a `seq` once, but not not more than once within a test, even though the record itself is never saved to the database. ## Overriding recipe definitions Passing values when calling `make_recipe` or `prepare_recipe` will override the recipe rule. ```python from model_bakery import baker baker.make_recipe('shop.customer', name='Ada Lovelace') ``` This is useful when you have to create multiple objects and you have some unique field, for instance. ## Recipe inheritance If you need to reuse and override existent recipe call extend method: ```python customer = Recipe( Customer, bio='Some customer bio', age=30, enjoy_jards_macale=True, ) sad_customer = customer.extend( enjoy_jards_macale=False, ) ``` model_bakery-1.20.4/docs/test_runners.md000066400000000000000000000027661475767230300202710ustar00rootroot00000000000000# Test Runners Most of the code examples shown so far have used the [Django TestCase](https://docs.djangoproject.com/en/dev/topics/testing/tools/#testcase) to explain how Model Bakery is used. However, [pytest](https://docs.pytest.org/en/stable/) (with the [pytest-django](https://pytest-django.readthedocs.io/en/latest/) plugin) is often preferred for it\'s simplicity and other benefits. See [here](https://realpython.com/django-pytest-fixtures/). The following examples show Model Bakery usage with different test runners. ## Django ```python # Core Django imports from django.test import TestCase # Third-party app imports from model_bakery import baker from shop.models import Customer class CustomerTestModel(TestCase): """ Class to test the model Customer """ def setUp(self): """Set up test class.""" self.customer = baker.make(Customer) def test_using_customer(self): """Test function using baked model.""" self.assertIsInstance(self.customer, Customer) ``` ## pytest ```python # pytest import import pytest # Third-party app imports from model_bakery import baker from shop.models import Customer @pytest.fixture def customer(): """Fixture for baked Customer model.""" return baker.make(Customer) def test_using_customer(customer): """Test function using fixture of baked model.""" assert isinstance(customer, Customer) ``` model_bakery-1.20.4/model_bakery/000077500000000000000000000000001475767230300167065ustar00rootroot00000000000000model_bakery-1.20.4/model_bakery/__about__.py000066400000000000000000000000271475767230300211650ustar00rootroot00000000000000__version__ = "1.20.4" model_bakery-1.20.4/model_bakery/__init__.py000066400000000000000000000000371475767230300210170ustar00rootroot00000000000000from .utils import seq # NoQA model_bakery-1.20.4/model_bakery/_types.py000066400000000000000000000002021475767230300205550ustar00rootroot00000000000000from typing import TypeVar from django.db.models import Model M = TypeVar("M", bound=Model) NewM = TypeVar("NewM", bound=Model) model_bakery-1.20.4/model_bakery/baker.py000066400000000000000000000750541475767230300203570ustar00rootroot00000000000000import collections from os.path import dirname, join from typing import ( Any, Callable, Dict, Generic, Iterator, List, Optional, Set, Type, Union, cast, overload, ) from django.apps import apps from django.conf import settings from django.db.models import ( AutoField, BooleanField, Field, FileField, ForeignKey, ManyToManyField, Model, OneToOneField, ) from django.db.models.fields import NOT_PROVIDED from django.db.models.fields.proxy import OrderWrt from django.db.models.fields.related import ( ReverseManyToOneDescriptor as ForeignRelatedObjectsDescriptor, ) from django.db.models.fields.reverse_related import ManyToOneRel, OneToOneRel from . import generators, random_gen from ._types import M, NewM from .content_types import BAKER_CONTENTTYPES from .exceptions import ( AmbiguousModelName, CustomBakerNotFound, InvalidCustomBaker, InvalidQuantityException, ModelNotFound, RecipeIteratorEmpty, ) from .utils import ( import_from_str, seq, # noqa: F401 - Enable seq to be imported from recipes ) if BAKER_CONTENTTYPES: from django.contrib.contenttypes import models as contenttypes_models from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation else: contenttypes_models = None GenericRelation = None recipes = None # FIXME: use pkg_resource mock_file_jpeg = join(dirname(__file__), "mock_img.jpeg") mock_file_txt = join(dirname(__file__), "mock_file.txt") MAX_MANY_QUANTITY = 5 def _valid_quantity(quantity: Optional[Union[str, int]]) -> bool: return quantity is not None and (not isinstance(quantity, int) or quantity < 1) def _is_auto_datetime_field(field: Field) -> bool: return getattr(field, "auto_now_add", False) or getattr(field, "auto_now", False) def seed(seed: Union[int, float, str, bytes, bytearray, None]) -> None: Baker.seed(seed) @overload def make( _model: Union[str, Type[M]], _quantity: None = None, make_m2m: bool = False, _save_kwargs: Optional[Dict] = None, _refresh_after_create: bool = False, _create_files: bool = False, _using: str = "", _bulk_create: bool = False, **attrs: Any, ) -> M: ... @overload def make( _model: Union[str, Type[M]], _quantity: int, make_m2m: bool = False, _save_kwargs: Optional[Dict] = None, _refresh_after_create: bool = False, _create_files: bool = False, _using: str = "", _bulk_create: bool = False, _fill_optional: Union[List[str], bool] = False, **attrs: Any, ) -> List[M]: ... def make( _model, _quantity: Optional[int] = None, make_m2m: bool = False, _save_kwargs: Optional[Dict] = None, _refresh_after_create: bool = False, _create_files: bool = False, _using: str = "", _bulk_create: bool = False, _fill_optional: Union[List[str], bool] = False, **attrs: Any, ): """Create a persisted instance from a given model its associated models. Baker fills the fields with random values, or you can specify which fields you want to define its values by yourself. """ _save_kwargs = _save_kwargs or {} attrs.update({"_fill_optional": _fill_optional}) baker: Baker = Baker.create( _model, make_m2m=make_m2m, create_files=_create_files, _using=_using ) if _valid_quantity(_quantity): raise InvalidQuantityException if _quantity and _bulk_create: return bulk_create(baker, _quantity, _save_kwargs=_save_kwargs, **attrs) elif _quantity: return [ baker.make( _save_kwargs=_save_kwargs, _refresh_after_create=_refresh_after_create, **attrs, ) for _ in range(_quantity) ] return baker.make( _save_kwargs=_save_kwargs, _refresh_after_create=_refresh_after_create, **attrs ) @overload def prepare( _model: Union[str, Type[M]], _quantity: None = None, _save_related: bool = False, _using: str = "", **attrs, ) -> M: ... @overload def prepare( _model: Union[str, Type[M]], _quantity: int, _save_related: bool = False, _using: str = "", _fill_optional: Union[List[str], bool] = False, **attrs, ) -> List[M]: ... def prepare( _model: Union[str, Type[M]], _quantity: Optional[int] = None, _save_related: bool = False, _using: str = "", _fill_optional: Union[List[str], bool] = False, **attrs, ): """Create but do not persist an instance from a given model. Baker fills the fields with random values, or you can specify which fields you want to define its values by yourself. """ attrs.update({"_fill_optional": _fill_optional}) baker = Baker.create(_model, _using=_using) if _valid_quantity(_quantity): raise InvalidQuantityException if _quantity: return [ baker.prepare(_save_related=_save_related, **attrs) for i in range(_quantity) ] return baker.prepare(_save_related=_save_related, **attrs) def _recipe(name: str) -> Any: app_name, recipe_name = name.rsplit(".", 1) try: module = apps.get_app_config(app_name).module pkg = module.__package__ if module else app_name except LookupError: pkg = app_name return import_from_str(".".join((pkg, "baker_recipes", recipe_name))) def make_recipe(baker_recipe_name, _quantity=None, _using="", **new_attrs): return _recipe(baker_recipe_name).make( _quantity=_quantity, _using=_using, **new_attrs ) def prepare_recipe( baker_recipe_name, _quantity=None, _save_related=False, _using="", **new_attrs ): return _recipe(baker_recipe_name).prepare( _quantity=_quantity, _save_related=_save_related, _using=_using, **new_attrs ) class ModelFinder: """Encapsulates all the logic for finding a model to Baker.""" _unique_models: Optional[Dict[str, Type[Model]]] = None _ambiguous_models: Optional[List[str]] = None def get_model(self, name: str) -> Type[Model]: """Get a model. Args: name (str): A name on the form 'applabel.modelname' or 'modelname' Returns: object: a model class """ try: if "." in name: app_label, model_name = name.split(".") model = apps.get_model(app_label, model_name) else: model = self.get_model_by_name(name) except LookupError: model = None if not model: raise ModelNotFound(f"Could not find model '{name.title()}'.") return model def get_model_by_name(self, name: str) -> Optional[Type[Model]]: """Get a model by name. If a model with that name exists in more than one app, raises AmbiguousModelName. """ name = name.lower() if self._unique_models is None or self._ambiguous_models is None: self._populate() if name in cast(List, self._ambiguous_models): raise AmbiguousModelName( f"{name.title()} is a model in more than one app. " 'Use the form "app.model".' ) return cast(Dict, self._unique_models).get(name) def _populate(self) -> None: """Cache models for faster self._get_model.""" unique_models = {} ambiguous_models = [] all_models = apps.all_models for app_model in all_models.values(): for name, model in app_model.items(): if name not in unique_models: unique_models[name] = model else: ambiguous_models.append(name) for name in ambiguous_models: unique_models.pop(name, None) self._ambiguous_models = ambiguous_models self._unique_models = unique_models def is_iterator(value: Any) -> bool: return isinstance(value, collections.abc.Iterator) def _custom_baker_class() -> Optional[Type]: """Return the specified custom baker class. Returns: object: The custom class is specified by BAKER_CUSTOM_CLASS in Django's settings, or None if no custom class is defined. """ custom_class_string = getattr(settings, "BAKER_CUSTOM_CLASS", None) if custom_class_string is None: return None try: baker_class = import_from_str(custom_class_string) for required_function_name in ("make", "prepare"): if not hasattr(baker_class, required_function_name): raise InvalidCustomBaker( f'Custom Baker classes must have a "{required_function_name}" function' ) return baker_class except ImportError: raise CustomBakerNotFound( f"Could not find custom baker class '{custom_class_string}'" ) class Baker(Generic[M]): SENTINEL = object() attr_mapping: Dict[str, Any] = {} type_mapping: Dict = {} _global_seed: Union[object, int, float, str, bytes, bytearray, None] = SENTINEL # Note: we're using one finder for all Baker instances to avoid # rebuilding the model cache for every make_* or prepare_* call. finder = ModelFinder() @classmethod def seed(cls, seed: Union[int, float, str, bytes, bytearray, None]) -> None: random_gen.baker_random.seed(seed) cls._global_seed = seed @classmethod def create( cls, _model: Union[str, Type[NewM]], make_m2m: bool = False, create_files: bool = False, _using: str = "", ) -> "Baker[NewM]": """Create the baker class defined by the `BAKER_CUSTOM_CLASS` setting.""" baker_class = _custom_baker_class() or cls return cast(Type[Baker[NewM]], baker_class)( _model, make_m2m, create_files, _using=_using ) def __init__( self, _model: Union[str, Type[M]], make_m2m: bool = False, create_files: bool = False, _using: str = "", ) -> None: self.make_m2m = make_m2m self.create_files = create_files self.m2m_dict: Dict[str, List] = {} self.iterator_attrs: Dict[str, Iterator] = {} self.model_attrs: Dict[str, Any] = {} self.rel_attrs: Dict[str, Any] = {} self.rel_fields: List[str] = [] self._using = _using if isinstance(_model, str): self.model = cast(Type[M], self.finder.get_model(_model)) else: self.model = cast(Type[M], _model) self.init_type_mapping() def init_type_mapping(self) -> None: self.type_mapping = generators.get_type_mapping() generators_from_settings = getattr(settings, "BAKER_CUSTOM_FIELDS_GEN", {}) for k, v in generators_from_settings.items(): field_class = import_from_str(k) generator = import_from_str(v) self.type_mapping[field_class] = generator def make( self, _save_kwargs: Optional[Dict[str, Any]] = None, _refresh_after_create: bool = False, _from_manager=None, _fill_optional: Union[List[str], bool] = False, **attrs: Any, ): """Create and persist an instance of the model associated with Baker instance.""" params = { "commit": True, "commit_related": True, "_save_kwargs": _save_kwargs, "_refresh_after_create": _refresh_after_create, "_from_manager": _from_manager, "_fill_optional": _fill_optional, } params.update(attrs) return self._make(**params) def prepare( self, _save_related=False, _fill_optional: Union[List[str], bool] = False, **attrs: Any, ) -> M: """Create, but do not persist, an instance of the associated model.""" params = { "commit": False, "commit_related": _save_related, "_fill_optional": _fill_optional, } params.update(attrs) return self._make(**params) def get_fields(self) -> Set[Any]: return set(self.model._meta.get_fields()) - set( self.model._meta.related_objects ) def _make( # noqa: C901 self, commit=True, commit_related=True, _save_kwargs=None, _refresh_after_create=False, _from_manager=None, **attrs: Any, ) -> M: _save_kwargs = _save_kwargs or {} if self._using: _save_kwargs["using"] = self._using self._clean_attrs(attrs) for field in self.get_fields(): if self._skip_field(field): continue if isinstance(field, ManyToManyField): if field.name not in self.model_attrs: self.m2m_dict[field.name] = self.m2m_value(field) else: if field.name in self.iterator_attrs: self.model_attrs[field.name] = [ next(self.iterator_attrs[field.name]) ] else: self.m2m_dict[field.name] = self.model_attrs.pop(field.name) # is an _id relation that has a sequence defined elif ( isinstance(field, (OneToOneField, ForeignKey)) and hasattr(field, "attname") and field.attname in self.iterator_attrs ): self.model_attrs[field.attname] = next( self.iterator_attrs[field.attname] ) elif field.name not in self.model_attrs: if ( not isinstance(field, ForeignKey) or hasattr(field, "attname") and field.attname not in self.model_attrs ): self.model_attrs[field.name] = self.generate_value( field, commit_related ) elif callable(self.model_attrs[field.name]): self.model_attrs[field.name] = self.model_attrs[field.name]() elif field.name in self.iterator_attrs: try: self.model_attrs[field.name] = next(self.iterator_attrs[field.name]) except StopIteration: raise RecipeIteratorEmpty(f"{field.name} iterator is empty.") instance = self.instance( self.model_attrs, _commit=commit, _from_manager=_from_manager, _save_kwargs=_save_kwargs, ) if commit: for related in self.model._meta.related_objects: self.create_by_related_name(instance, related) if _refresh_after_create: instance.refresh_from_db() return instance def m2m_value(self, field: ManyToManyField) -> List[Any]: if field.name in self.rel_fields: return self.generate_value(field) if not self.make_m2m or field.null and not field.fill_optional: return [] return self.generate_value(field) def instance( self, attrs: Dict[str, Any], _commit, _save_kwargs, _from_manager ) -> M: one_to_many_keys = {} auto_now_keys = {} generic_foreign_keys = {} for k in tuple(attrs.keys()): field = getattr(self.model, k, None) if not field: continue if isinstance(field, ForeignRelatedObjectsDescriptor): one_to_many_keys[k] = attrs.pop(k) if hasattr(field, "field") and _is_auto_datetime_field(field.field): auto_now_keys[k] = attrs[k] if BAKER_CONTENTTYPES and isinstance(field, GenericForeignKey): generic_foreign_keys[k] = { "value": attrs.pop(k), "content_type_field": field.ct_field, "object_id_field": field.fk_field, } instance = self.model(**attrs) self._handle_generic_foreign_keys(instance, generic_foreign_keys) if _commit: instance.save(**_save_kwargs) self._handle_one_to_many(instance, one_to_many_keys) self._handle_m2m(instance) self._handle_auto_now(instance, auto_now_keys) if _from_manager: # Fetch the instance using the given Manager, e.g. # 'objects'. This will ensure any additional code # within its get_queryset() method (e.g. annotations) # is run. manager = getattr(self.model, _from_manager) instance = cast(M, manager.get(pk=instance.pk)) return instance def create_by_related_name( self, instance: Model, related: Union[ManyToOneRel, OneToOneRel] ) -> None: rel_name = related.get_accessor_name() if not rel_name or rel_name not in self.rel_fields: return kwargs = filter_rel_attrs(rel_name, **self.rel_attrs) kwargs[related.field.name] = instance make(related.field.model, **kwargs) def _clean_attrs(self, attrs: Dict[str, Any]) -> None: def is_rel_field(x: str): return "__" in x self.fill_in_optional = attrs.pop("_fill_optional", False) # error for non existing fields if isinstance(self.fill_in_optional, (tuple, list, set)): # parents and relations wrong_fields = set(self.fill_in_optional) - { f.name for f in self.get_fields() } if wrong_fields: raise AttributeError( f"_fill_optional field(s) {list(wrong_fields)} are not " f"related to model {self.model.__name__}" ) self.iterator_attrs = { k: v for k, v in attrs.items() if isinstance(v, collections.abc.Iterator) } self.model_attrs = {k: v for k, v in attrs.items() if not is_rel_field(k)} self.rel_attrs = {k: v for k, v in attrs.items() if is_rel_field(k)} self.rel_fields = [x.split("__")[0] for x in self.rel_attrs if is_rel_field(x)] def _skip_field(self, field: Field) -> bool: # noqa: C901 # check for fill optional argument if isinstance(self.fill_in_optional, bool): field.fill_optional = self.fill_in_optional else: field.fill_optional = field.name in self.fill_in_optional if isinstance(field, FileField) and not self.create_files: return True # Don't Skip related _id fields defined in the iterator attributes if ( isinstance(field, (OneToOneField, ForeignKey)) and hasattr(field, "attname") and field.attname in self.iterator_attrs ): return False # Skip links to parent so parent is not created twice. if isinstance(field, OneToOneField) and self._remote_field(field).parent_link: return True other_fields_to_skip = [ AutoField, OrderWrt, ] if BAKER_CONTENTTYPES: other_fields_to_skip.extend([GenericRelation, GenericForeignKey]) if isinstance(field, tuple(other_fields_to_skip)): return True if all( # noqa: SIM102 [ field.name not in self.model_attrs, field.name not in self.rel_fields, field.name not in self.attr_mapping, ] ): # Django is quirky in that BooleanFields are always "blank", # but have no default. if not field.fill_optional and ( not issubclass(field.__class__, Field) or field.has_default() or (field.blank and not isinstance(field, BooleanField)) ): return True if field.name not in self.model_attrs: # noqa: SIM102 if field.name not in self.rel_fields and ( not field.fill_optional and field.null ): return True return False def _handle_auto_now(self, instance: Model, attrs: Dict[str, Any]): if not attrs: return # use .update() to force update auto_now fields instance.__class__.objects.filter(pk=instance.pk).update(**attrs) # to make the resulting instance has the specified values for k, v in attrs.items(): setattr(instance, k, v) def _handle_one_to_many(self, instance: Model, attrs: Dict[str, Any]): for key, values in attrs.items(): manager = getattr(instance, key) if callable(values): values = values() for value in values: # Django will handle any operation to persist nested non-persisted FK because # save doesn't do so and, thus, raises constraint errors. That's why save() # only gets called if the object doesn't have a pk and also doesn't hold fk # pointers. fks = any( fk for fk in value._meta.fields if isinstance(fk, (ForeignKey, OneToOneField)) ) if not value.pk and not fks: value.save() try: manager.set(values, bulk=False, clear=True) except TypeError: # for many-to-many relationships the bulk keyword argument doesn't exist manager.set(values, clear=True) def _handle_m2m(self, instance: Model): for key, values in self.m2m_dict.items(): if callable(values): values = values() for value in values: if not value.pk: value.save() m2m_relation = getattr(instance, key) through_model = m2m_relation.through # using related manager to fire m2m_changed signal if through_model._meta.auto_created: m2m_relation.add(*values) else: for value in values: base_kwargs = { m2m_relation.source_field_name: instance, m2m_relation.target_field_name: value, } make(through_model, _using=self._using, **base_kwargs) def _handle_generic_foreign_keys(self, instance: Model, attrs: Dict[str, Any]): """Set content type and object id for GenericForeignKey fields.""" for field_name, data in attrs.items(): ct_field_name = data["content_type_field"] oid_field_name = data["object_id_field"] value = data["value"] if callable(value): value = value() if is_iterator(value): value = next(value) if value is None: # when GFK is None, we should try to set the content type and object id to None content_type_field = instance._meta.get_field(ct_field_name) object_id_field = instance._meta.get_field(oid_field_name) if content_type_field.null: setattr(instance, ct_field_name, None) if object_id_field.null: setattr(instance, oid_field_name, None) else: setattr(instance, field_name, value) setattr( instance, ct_field_name, contenttypes_models.ContentType.objects.get_for_model( value, for_concrete_model=False ), ) setattr(instance, oid_field_name, value.pk) def _remote_field( self, field: Union[ForeignKey, OneToOneField] ) -> Union[OneToOneRel, ManyToOneRel]: return field.remote_field def generate_value(self, field: Field, commit: bool = True) -> Any: # noqa: C901 """Call the associated generator with a field passing all required args. Generator Resolution Precedence Order: -- `field.default` - model field default value, unless explicitly overwritten during baking -- `field.db_default` - model field db default value, unless explicitly overwritten -- `attr_mapping` - mapping per attribute name -- `choices` -- mapping from available field choices -- `type_mapping` - mapping from user defined type associated generators -- `default_mapping` - mapping from pre-defined type associated generators `attr_mapping` and `type_mapping` can be defined easily overwriting the model. """ is_content_type_fk = False is_generic_fk = False if BAKER_CONTENTTYPES: is_content_type_fk = isinstance(field, ForeignKey) and issubclass( self._remote_field(field).model, contenttypes_models.ContentType ) is_generic_fk = isinstance(field, GenericForeignKey) if is_generic_fk: generator = self.type_mapping[GenericForeignKey] # we only use default unless the field is overwritten in `self.rel_fields` elif field.has_default() and field.name not in self.rel_fields: if callable(field.default): return field.default() return field.default elif getattr(field, "db_default", NOT_PROVIDED) != NOT_PROVIDED: return field.db_default elif field.name in self.attr_mapping: generator = self.attr_mapping[field.name] elif field.choices: generator = random_gen.gen_from_choices(field.choices) elif is_content_type_fk: generator = self.type_mapping[contenttypes_models.ContentType] elif generators.get(field.__class__): generator = generators.get(field.__class__) elif field.__class__ in self.type_mapping: generator = self.type_mapping[field.__class__] else: raise TypeError( f"field {field.name} type {field.__class__} is not supported by baker." ) # attributes like max_length, decimal_places are taken into account when # generating the value. field._using = self._using generator_attrs = get_required_values(generator, field) if field.name in self.rel_fields: generator_attrs.update(filter_rel_attrs(field.name, **self.rel_attrs)) if ( field.__class__ in (ForeignKey, OneToOneField, ManyToManyField) and not is_content_type_fk ): # create files also on related models if required generator_attrs["_create_files"] = self.create_files if not commit: generator = getattr(generator, "prepare", generator) return generator(**generator_attrs) def get_required_values( generator: Callable, field: Field ) -> Dict[str, Union[bool, int, str, List[Callable]]]: """Get required values for a generator from the field. If required value is a function, calls it with field as argument. If required value is a string, simply fetch the value from the field and return. """ required_values = {} # type: Dict[str, Any] if hasattr(generator, "required"): for item in generator.required: # type: ignore[attr-defined] if callable(item): # baker can deal with the nasty hacking too! key, value = item(field) required_values[key] = value elif isinstance(item, str): required_values[item] = getattr(field, item) else: raise ValueError( f"Required value '{item}' is of wrong type. Don't make baker sad." ) return required_values def filter_rel_attrs(field_name: str, **rel_attrs) -> Dict[str, Any]: clean_dict = {} for k, v in rel_attrs.items(): if k.startswith(f"{field_name}__"): splitted_key = k.split("__") key = "__".join(splitted_key[1:]) clean_dict[key] = v else: clean_dict[k] = v return clean_dict def _save_related_objs(model, objects, _using=None) -> None: """Recursively save all related foreign keys for each entry.""" _save_kwargs = {"using": _using} if _using else {} fk_fields = [ f for f in model._meta.fields if isinstance(f, (OneToOneField, ForeignKey)) ] for fk in fk_fields: fk_objects = [] for obj in objects: fk_obj = getattr(obj, fk.name, None) if fk_obj and not fk_obj.pk: fk_objects.append(fk_obj) if fk_objects: _save_related_objs(fk.related_model, fk_objects) for i, fk_obj in enumerate(fk_objects): fk_obj.save(**_save_kwargs) setattr(objects[i], fk.name, fk_obj) def bulk_create(baker: Baker[M], quantity: int, **kwargs) -> List[M]: """ Bulk create entries and all related FKs as well. Important: there's no way to avoid save calls since Django does not return the created objects after a bulk_insert call. """ # Create a list of entries by calling the prepare method of the Baker instance # quantity number of times, passing in the additional keyword arguments entries = [ baker.prepare( **kwargs, ) for _ in range(quantity) ] _save_related_objs(baker.model, entries, _using=baker._using) # Use the desired database to create the entries if baker._using: manager = baker.model._base_manager.using(baker._using) else: manager = baker.model._base_manager created_entries = manager.bulk_create(entries) # set many-to-many relations from kwargs for entry in created_entries: for field in baker.model._meta.many_to_many: if field.name in kwargs: through_model = getattr(entry, field.name).through through_model.objects.bulk_create( [ through_model( **{ field.remote_field.name: entry, field.related_model._meta.model_name: obj, } ) for obj in kwargs[field.name] ] ) # set many-to-many relations that are specified using related name from kwargs for field in baker.model._meta.get_fields(): if field.many_to_many and hasattr(field, "related_model"): reverse_relation_name = ( field.related_query_name or field.related_name or f"{field.related_model._meta.model_name}_set" ) if reverse_relation_name in kwargs: getattr(entry, reverse_relation_name).set( kwargs[reverse_relation_name] ) return created_entries model_bakery-1.20.4/model_bakery/content_types.py000066400000000000000000000006131475767230300221560ustar00rootroot00000000000000from django.apps import apps BAKER_CONTENTTYPES = apps.is_installed("django.contrib.contenttypes") default_contenttypes_mapping = {} __all__ = ["BAKER_CONTENTTYPES", "default_contenttypes_mapping"] if BAKER_CONTENTTYPES: from django.contrib.contenttypes.models import ContentType from . import random_gen default_contenttypes_mapping[ContentType] = random_gen.gen_content_type model_bakery-1.20.4/model_bakery/exceptions.py000066400000000000000000000005151475767230300214420ustar00rootroot00000000000000class RecipeNotFound(Exception): pass class RecipeIteratorEmpty(Exception): pass class ModelNotFound(Exception): pass class AmbiguousModelName(Exception): pass class InvalidQuantityException(Exception): pass class CustomBakerNotFound(Exception): pass class InvalidCustomBaker(Exception): pass model_bakery-1.20.4/model_bakery/generators.py000066400000000000000000000120651475767230300214350ustar00rootroot00000000000000from decimal import Decimal from typing import Any, Callable, Dict, Optional, Type, Union from django.db.backends.base.operations import BaseDatabaseOperations from django.db.models import ( AutoField, BigAutoField, BigIntegerField, BinaryField, BooleanField, CharField, DateField, DateTimeField, DecimalField, DurationField, EmailField, FileField, FloatField, ForeignKey, GenericIPAddressField, ImageField, IntegerField, IPAddressField, JSONField, ManyToManyField, OneToOneField, PositiveBigIntegerField, PositiveIntegerField, PositiveSmallIntegerField, SlugField, SmallAutoField, SmallIntegerField, TextField, TimeField, URLField, UUIDField, ) from . import random_gen from .utils import import_from_str try: # PostgreSQL-specific field (only available when psycopg is installed) from django.contrib.postgres.fields import ArrayField except ImportError: ArrayField = None try: # PostgreSQL-specific field (only available when psycopg is installed) from django.contrib.postgres.fields import HStoreField except ImportError: HStoreField = None try: # PostgreSQL-specific fields (only available when psycopg is installed) from django.contrib.postgres.fields.citext import ( CICharField, CIEmailField, CITextField, ) except ImportError: CICharField = None CIEmailField = None CITextField = None try: # PostgreSQL-specific fields (only available when psycopg is installed) from django.contrib.postgres.fields.ranges import ( BigIntegerRangeField, DateRangeField, DateTimeRangeField, DecimalRangeField, IntegerRangeField, ) except ImportError: BigIntegerRangeField = None DateRangeField = None DateTimeRangeField = None DecimalRangeField = None IntegerRangeField = None def _make_integer_gen_by_range(field_type: Any) -> Callable: min_int, max_int = BaseDatabaseOperations.integer_field_ranges[field_type.__name__] def gen_integer(): return random_gen.gen_integer(min_int=min_int, max_int=max_int) return gen_integer default_mapping = { ForeignKey: random_gen.gen_related, OneToOneField: random_gen.gen_related, ManyToManyField: random_gen.gen_m2m, BooleanField: random_gen.gen_boolean, AutoField: _make_integer_gen_by_range(AutoField), BigAutoField: _make_integer_gen_by_range(BigAutoField), IntegerField: _make_integer_gen_by_range(IntegerField), SmallAutoField: _make_integer_gen_by_range(SmallAutoField), BigIntegerField: _make_integer_gen_by_range(BigIntegerField), SmallIntegerField: _make_integer_gen_by_range(SmallIntegerField), PositiveBigIntegerField: _make_integer_gen_by_range(PositiveBigIntegerField), PositiveIntegerField: _make_integer_gen_by_range(PositiveIntegerField), PositiveSmallIntegerField: _make_integer_gen_by_range(PositiveSmallIntegerField), FloatField: random_gen.gen_float, DecimalField: random_gen.gen_decimal, BinaryField: random_gen.gen_byte_string, CharField: random_gen.gen_string, TextField: random_gen.gen_string, SlugField: random_gen.gen_slug, UUIDField: random_gen.gen_uuid, DateField: random_gen.gen_date, DateTimeField: random_gen.gen_datetime, TimeField: random_gen.gen_time, URLField: random_gen.gen_url, EmailField: random_gen.gen_email, IPAddressField: random_gen.gen_ipv4, GenericIPAddressField: random_gen.gen_ip, FileField: random_gen.gen_file_field, ImageField: random_gen.gen_image_field, DurationField: random_gen.gen_interval, JSONField: random_gen.gen_json, } # type: Dict[Type, Callable] if ArrayField: default_mapping[ArrayField] = random_gen.gen_array if HStoreField: default_mapping[HStoreField] = random_gen.gen_hstore if CICharField: default_mapping[CICharField] = random_gen.gen_string if CIEmailField: default_mapping[CIEmailField] = random_gen.gen_email if CITextField: default_mapping[CITextField] = random_gen.gen_string if DecimalRangeField: default_mapping[DecimalRangeField] = random_gen.gen_pg_numbers_range(Decimal) if IntegerRangeField: default_mapping[IntegerRangeField] = random_gen.gen_pg_numbers_range(int) if BigIntegerRangeField: default_mapping[BigIntegerRangeField] = random_gen.gen_pg_numbers_range(int) if DateRangeField: default_mapping[DateRangeField] = random_gen.gen_date_range if DateTimeRangeField: default_mapping[DateTimeRangeField] = random_gen.gen_datetime_range # Add GIS fields def get_type_mapping() -> Dict[Type, Callable]: from .content_types import default_contenttypes_mapping from .gis import default_gis_mapping mapping = default_mapping.copy() mapping.update(default_contenttypes_mapping) mapping.update(default_gis_mapping) return mapping.copy() user_mapping = {} def add(field: str, func: Optional[Union[Callable, str]]) -> None: user_mapping[import_from_str(field)] = import_from_str(func) def get(field: Any) -> Optional[Callable]: return user_mapping.get(field) model_bakery-1.20.4/model_bakery/gis.py000066400000000000000000000020041475767230300200360ustar00rootroot00000000000000from django.apps import apps BAKER_GIS = apps.is_installed("django.contrib.gis") default_gis_mapping = {} __all__ = ["BAKER_GIS", "default_gis_mapping"] if BAKER_GIS: from django.contrib.gis.db.models import ( GeometryCollectionField, GeometryField, LineStringField, MultiLineStringField, MultiPointField, MultiPolygonField, PointField, PolygonField, ) from . import random_gen default_gis_mapping[GeometryField] = random_gen.gen_geometry default_gis_mapping[PointField] = random_gen.gen_point default_gis_mapping[LineStringField] = random_gen.gen_line_string default_gis_mapping[PolygonField] = random_gen.gen_polygon default_gis_mapping[MultiPointField] = random_gen.gen_multi_point default_gis_mapping[MultiLineStringField] = random_gen.gen_multi_line_string default_gis_mapping[MultiPolygonField] = random_gen.gen_multi_polygon default_gis_mapping[GeometryCollectionField] = random_gen.gen_geometry_collection model_bakery-1.20.4/model_bakery/mock_file.txt000066400000000000000000000000121475767230300213700ustar00rootroot00000000000000mock file model_bakery-1.20.4/model_bakery/mock_img.jpeg000066400000000000000000000035741475767230300213530ustar00rootroot00000000000000JFIF  ! '(*%$%"/,#3)8,/ 25<,A27-5 2$$-,4./50,,/55)5.4*4*,,)),))-.4),)),,,,),),),,)))),)8">!1AQa"qt2Srs%46T#$5B !1AQa" ?sWN;TY!U3d?Vz|Vv&j*ȭ. T.:hAgn`(KNP= P*#t,\JwݽsߵB*h)CJRR(rVkiD.!~҅-|{ɱw8m*'JX' xW|E4vPB;L$N}9oW'PiJ!ư&)fvLWEAqC~Y zԾKݵ>yT  r>O]akMq nG+#ȟz| -55)W@#F =Xod;Z! MHQRH9#:T/C 4[c̆w}0ߨGxk 72*b6u$1ު>Z9HDeadbez xc~?2j̶9pAQ %=ۇ$w]:K(UI6PO^鎹2rZqS?u$š15c_5B*'a=aEIe❜pK*0R$~Pz{zd[t}xAӕŤDFqMf+}KRyiwp"c]ʤ;HCWF7[?67Hm+{}֏<\s$*\OVP;Phrg-Ѵ*e ]Y‡ ;=+a=gILH"4v/r*;:JMd2F^eK0luK4dsᝀGZ޽qX{o[ D<:bmJG,S) 0G=׌Hu B[W "5P}Ϲ5PÕxjVӴNVfPDiIv^'۽BnZYI͘^U9u)C3C?O:Hb,yRdy:|YR)JR)@~XHJI9>(S)JW+LҦGopW?{JPV^ "':vl]FF|OsJR⏣model_bakery-1.20.4/model_bakery/py.typed000066400000000000000000000000001475767230300203730ustar00rootroot00000000000000model_bakery-1.20.4/model_bakery/random_gen.py000066400000000000000000000235441475767230300214010ustar00rootroot00000000000000"""Generators are callables that return a value used to populate a field. If this callable has a `required` attribute (a list, mostly), for each item in the list, if the item is a string, the field attribute with the same name will be fetched from the field and used as argument for the generator. If it is a callable (which will receive `field` as first argument), it should return a list in the format (key, value) where key is the argument name for generator and value is the value for that argument. """ import string import warnings from datetime import date, datetime, time, timedelta from decimal import Decimal from os.path import abspath, dirname, join from random import Random from typing import Any, Callable, List, Optional, Tuple, Union from uuid import UUID from django.core.files.base import ContentFile from django.db.models import Field, Model from django.utils.timezone import now MAX_LENGTH = 300 # Using sys.maxint here breaks a bunch of tests when running against a # Postgres database. MAX_INT = 100000000000 baker_random = Random() # noqa: S311 def get_content_file(content: bytes, name: str) -> ContentFile: return ContentFile(content, name=name) def gen_file_field() -> ContentFile: name = "mock_file.txt" file_path = abspath(join(dirname(__file__), name)) with open(file_path, "rb") as f: return get_content_file(f.read(), name=name) def gen_image_field() -> ContentFile: name = "mock_img.jpeg" file_path = abspath(join(dirname(__file__), name)) with open(file_path, "rb") as f: return get_content_file(f.read(), name=name) def gen_from_list(a_list: Union[List[str], range]) -> Callable: """Make sure all values of the field are generated from a list. Examples: Here how to use it. >>> from baker import Baker >>> class ExperienceBaker(Baker): >>> attr_mapping = {'some_field': gen_from_list(['A', 'B', 'C'])} """ return lambda: baker_random.choice(list(a_list)) # -- DEFAULT GENERATORS -- def gen_from_choices(choices: List) -> Callable: choice_list = [] for value, label in choices: if isinstance(label, (list, tuple)): for val, _lbl in label: choice_list.append(val) else: choice_list.append(value) return gen_from_list(choice_list) def gen_integer(min_int: int = -MAX_INT, max_int: int = MAX_INT) -> int: return baker_random.randint(min_int, max_int) def gen_float() -> float: return baker_random.random() * gen_integer() def gen_decimal(max_digits: int, decimal_places: int) -> Decimal: def num_as_str(x: int) -> str: return "".join([str(baker_random.randint(0, 9)) for _ in range(x)]) if decimal_places: return Decimal( f"{num_as_str(max_digits - decimal_places - 1)}.{num_as_str(decimal_places)}" ) return Decimal(num_as_str(max_digits)) gen_decimal.required = ["max_digits", "decimal_places"] # type: ignore[attr-defined] def gen_date() -> date: return now().date() def gen_datetime() -> datetime: return now() def gen_time() -> time: return now().time() def gen_string(max_length: int) -> str: return "".join(baker_random.choice(string.ascii_letters) for _ in range(max_length)) def _gen_string_get_max_length(field: Field) -> Tuple[str, int]: max_length = getattr(field, "max_length", None) if max_length is None: max_length = MAX_LENGTH return "max_length", max_length gen_string.required = [_gen_string_get_max_length] # type: ignore[attr-defined] def gen_slug(max_length: int) -> str: valid_chars = string.ascii_letters + string.digits + "_-" return "".join(baker_random.choice(valid_chars) for _ in range(max_length)) gen_slug.required = ["max_length"] # type: ignore[attr-defined] def gen_text() -> str: warnings.warn( "\n" "Accessing `model_bakery.random_gen.gen_text` is deprecated " "and will be removed in a future major release. Please use " "`model_bakery.random_gen.gen_string` instead." "\n", DeprecationWarning, stacklevel=2, ) return gen_string(MAX_LENGTH) def gen_boolean() -> bool: return baker_random.choice((True, False)) def gen_null_boolean(): return baker_random.choice((True, False, None)) def gen_url() -> str: return f"http://www.{gen_string(30)}.com/" def gen_email() -> str: return f"{gen_string(10)}@example.com" def gen_ipv6() -> str: return ":".join(format(baker_random.randint(1, 65535), "x") for _ in range(8)) def gen_ipv4() -> str: return ".".join(str(baker_random.randint(1, 255)) for _ in range(4)) def gen_ipv46() -> str: ip_gen = baker_random.choice([gen_ipv4, gen_ipv6]) return ip_gen() def gen_ip(protocol: str, default_validators: List[Callable]) -> str: from django.core.exceptions import ValidationError protocol = (protocol or "").lower() if not protocol: field_validator = default_validators[0] dummy_ipv4 = "1.1.1.1" dummy_ipv6 = "FE80::0202:B3FF:FE1E:8329" try: field_validator(dummy_ipv4) field_validator(dummy_ipv6) generator = gen_ipv46 except ValidationError: try: field_validator(dummy_ipv4) generator = gen_ipv4 except ValidationError: generator = gen_ipv6 elif protocol == "ipv4": generator = gen_ipv4 elif protocol == "ipv6": generator = gen_ipv6 else: generator = gen_ipv46 return generator() gen_ip.required = ["protocol", "default_validators"] # type: ignore[attr-defined] def gen_byte_string(max_length: int = 16) -> bytes: generator = (baker_random.randint(0, 255) for x in range(max_length)) return bytes(generator) def gen_interval(interval_key: str = "milliseconds", offset: int = 0) -> timedelta: interval = gen_integer() + offset kwargs = {interval_key: interval} return timedelta(**kwargs) def gen_content_type(): from django.apps import apps from django.contrib.contenttypes.models import ContentType try: return ContentType.objects.get_for_model(baker_random.choice(apps.get_models())) except (AssertionError, RuntimeError): # AssertionError is raised by Django's test framework when db access is not available: # https://github.com/django/django/blob/stable/4.0.x/django/test/testcases.py#L150 # RuntimeError is raised by pytest-django when db access is not available: # https://github.com/pytest-dev/pytest-django/blob/v4.5.2/pytest_django/plugin.py#L709 warnings.warn( "Database access disabled, returning ContentType raw instance", stacklevel=1 ) return ContentType() def gen_uuid() -> UUID: import uuid return uuid.uuid4() def gen_array(): return [] def gen_json(): return {} def gen_hstore(): return {} def _fk_model(field: Field) -> Tuple[str, Optional[Model]]: try: return ("model", field.related_model) except AttributeError: return ("model", field.related.parent_model) def _prepare_related( model: str, _create_files=False, **attrs: Any ) -> Union[Model, List[Model]]: from .baker import prepare return prepare(model, **attrs) def gen_related(model, _create_files=False, **attrs): from .baker import make return make(model, _create_files=_create_files, **attrs) gen_related.required = [_fk_model, "_using"] # type: ignore[attr-defined] gen_related.prepare = _prepare_related # type: ignore[attr-defined] def gen_m2m(model, _create_files=False, **attrs): from .baker import MAX_MANY_QUANTITY, make return make( model, _create_files=_create_files, _quantity=MAX_MANY_QUANTITY, **attrs ) gen_m2m.required = [_fk_model, "_using"] # type: ignore[attr-defined] # GIS generators def gen_coord() -> float: return baker_random.uniform(0, 1) def gen_coords() -> str: return f"{gen_coord()} {gen_coord()}" def gen_point() -> str: return f"POINT ({gen_coords()})" def _gen_line_string_without_prefix() -> str: return f"({gen_coords()}, {gen_coords()})" def gen_line_string() -> str: return f"LINESTRING {_gen_line_string_without_prefix()}" def _gen_polygon_without_prefix() -> str: start = gen_coords() return f"(({start}, {gen_coords()}, {gen_coords()}, {start}))" def gen_polygon() -> str: return f"POLYGON {_gen_polygon_without_prefix()}" def gen_multi_point() -> str: return f"MULTIPOINT (({gen_coords()}))" def gen_multi_line_string() -> str: return f"MULTILINESTRING ({_gen_line_string_without_prefix()})" def gen_multi_polygon() -> str: return f"MULTIPOLYGON ({_gen_polygon_without_prefix()})" def gen_geometry(): return gen_point() def gen_geometry_collection() -> str: return f"GEOMETRYCOLLECTION ({gen_point()})" def gen_pg_numbers_range(number_cast: Callable[[int], Any]) -> Callable: def gen_range(): try: from psycopg.types.range import Range except ImportError: from psycopg2._range import NumericRange as Range base_num = gen_integer(1, 100000) return Range(number_cast(-1 * base_num), number_cast(base_num)) return gen_range def gen_date_range(): try: from psycopg.types.range import DateRange except ImportError: from psycopg2.extras import DateRange base_date = gen_date() interval = gen_interval(offset=24 * 60 * 60 * 1000) # force at least 1 day interval args = sorted([base_date - interval, base_date + interval]) return DateRange(*args) def gen_datetime_range(): try: from psycopg.types.range import TimestamptzRange except ImportError: from psycopg2.extras import DateTimeTZRange as TimestamptzRange base_datetime = gen_datetime() interval = gen_interval() args = sorted([base_datetime - interval, base_datetime + interval]) return TimestamptzRange(*args) model_bakery-1.20.4/model_bakery/recipe.py000066400000000000000000000205701475767230300205330ustar00rootroot00000000000000import collections import copy import itertools from typing import ( Any, Dict, Generic, List, Optional, Type, TypeVar, Union, cast, overload, ) from django.db.models import Model from . import baker from ._types import M from .exceptions import RecipeNotFound from .utils import ( get_calling_module, seq, # noqa: F401 - Enable seq to be imported from recipes ) finder = baker.ModelFinder() class Recipe(Generic[M]): _T = TypeVar("_T", bound="Recipe[M]") def __init__(self, _model: Union[str, Type[M]], **attrs: Any) -> None: self.attr_mapping = attrs self._model = _model # _iterator_backups will hold values of the form (backup_iterator, usable_iterator). self._iterator_backups = {} # type: Dict[str, Any] def _mapping( # noqa: C901 self, _using: str, new_attrs: Dict[str, Any] ) -> Dict[str, Any]: _save_related = new_attrs.get("_save_related", True) _quantity = new_attrs.get("_quantity", 1) rel_fields_attrs = {k: v for k, v in new_attrs.items() if "__" in k} new_attrs = {k: v for k, v in new_attrs.items() if "__" not in k} mapping = self.attr_mapping.copy() for k, v in self.attr_mapping.items(): # do not generate values if field value is provided if k in new_attrs: continue elif isinstance(v, collections.abc.Iterator): if isinstance(self._model, str): m = finder.get_model(self._model) else: m = self._model if k not in self._iterator_backups or not m.objects.exists(): self._iterator_backups[k] = itertools.tee( self._iterator_backups.get(k, [v])[0] ) mapping[k] = self._iterator_backups[k][1] elif isinstance(v, RecipeForeignKey): attrs = {} # Remove any related field attrs from the recipe attrs before filtering for key, _value in list(rel_fields_attrs.items()): if key.startswith(f"{k}__"): attrs[key] = rel_fields_attrs.pop(key) recipe_attrs = baker.filter_rel_attrs(k, **attrs) if _save_related: # Create a unique foreign key for each quantity if one_to_one required if v.one_to_one is True: rel_gen = [ v.recipe.make(_using=_using, **recipe_attrs) for _ in range(_quantity) ] mapping[k] = itertools.cycle(rel_gen) # Otherwise create shared foreign key for each quantity else: mapping[k] = v.recipe.make(_using=_using, **recipe_attrs) else: mapping[k] = v.recipe.prepare(_using=_using, **recipe_attrs) elif isinstance(v, related): mapping[k] = v.make elif isinstance(v, collections.abc.Container): mapping[k] = copy.deepcopy(v) mapping.update(new_attrs) mapping.update(rel_fields_attrs) return mapping @overload def make( self, _quantity: None = None, make_m2m: bool = False, _refresh_after_create: bool = False, _create_files: bool = False, _using: str = "", _bulk_create: bool = False, _save_kwargs: Optional[Dict[str, Any]] = None, **attrs: Any, ) -> M: ... @overload def make( self, _quantity: int, make_m2m: bool = False, _refresh_after_create: bool = False, _create_files: bool = False, _using: str = "", _bulk_create: bool = False, _save_kwargs: Optional[Dict[str, Any]] = None, **attrs: Any, ) -> List[M]: ... def make( self, _quantity: Optional[int] = None, make_m2m: Optional[bool] = None, _refresh_after_create: Optional[bool] = None, _create_files: Optional[bool] = None, _using: str = "", _bulk_create: Optional[bool] = None, _save_kwargs: Optional[Dict[str, Any]] = None, **attrs: Any, ) -> Union[M, List[M]]: defaults = {} if _quantity is not None: defaults["_quantity"] = _quantity if make_m2m is not None: defaults["make_m2m"] = make_m2m if _refresh_after_create is not None: defaults["_refresh_after_create"] = _refresh_after_create if _create_files is not None: defaults["_create_files"] = _create_files if _bulk_create is not None: defaults["_bulk_create"] = _bulk_create if _save_kwargs is not None: defaults["_save_kwargs"] = _save_kwargs # type: ignore[assignment] defaults.update(attrs) return baker.make(self._model, _using=_using, **self._mapping(_using, defaults)) @overload def prepare( self, _quantity: None = None, _save_related: bool = False, _using: str = "", **attrs: Any, ) -> M: ... @overload def prepare( self, _quantity: int, _save_related: bool = False, _using: str = "", **attrs: Any, ) -> List[M]: ... def prepare( self, _quantity: Optional[int] = None, _save_related: bool = False, _using: str = "", **attrs: Any, ) -> Union[M, List[M]]: defaults = { "_save_related": _save_related, } if _quantity is not None: defaults["_quantity"] = _quantity # type: ignore[assignment] defaults.update(attrs) return baker.prepare( self._model, _using=_using, **self._mapping(_using, defaults) ) def extend(self: _T, **attrs: Any) -> _T: attr_mapping = self.attr_mapping.copy() attr_mapping.update(attrs) return type(self)(self._model, **attr_mapping) def _load_recipe_from_calling_module(recipe: str) -> Recipe[Model]: """Load `Recipe` from the string attribute given from the calling module. Args: recipe (str): the name of the recipe attribute within the module from which it should be loaded Returns: (Recipe): recipe resolved from calling module """ recipe = getattr(get_calling_module(2), recipe) if recipe: return cast(Recipe[Model], recipe) else: raise RecipeNotFound class RecipeForeignKey(Generic[M]): """A `Recipe` to use for making ManyToOne and OneToOne related objects.""" def __init__(self, recipe: Recipe[M], one_to_one: bool) -> None: if isinstance(recipe, Recipe): self.recipe = recipe self.one_to_one = one_to_one else: raise TypeError("Not a recipe") def foreign_key( recipe: Union[Recipe[M], str], one_to_one: bool = False ) -> RecipeForeignKey[M]: """Return a `RecipeForeignKey`. Return the callable, so that the associated `_model` will not be created during the recipe definition. This resolves recipes supplied as strings from other module paths or from the calling code's module. """ if isinstance(recipe, str): # Load `Recipe` from string before handing off to `RecipeForeignKey` try: # Try to load from another module recipe = baker._recipe(recipe) except (AttributeError, ImportError, ValueError): # Probably not in another module, so load it from calling module recipe = _load_recipe_from_calling_module(cast(str, recipe)) return RecipeForeignKey(cast(Recipe[M], recipe), one_to_one) class related(Generic[M]): # FIXME def __init__(self, *args: Union[str, Recipe[M]]) -> None: self.related = [] # type: List[Recipe[M]] for recipe in args: if isinstance(recipe, Recipe): self.related.append(recipe) elif isinstance(recipe, str): recipe = _load_recipe_from_calling_module(recipe) if recipe: self.related.append(recipe) else: raise RecipeNotFound else: raise TypeError("Not a recipe") def make(self) -> List[Union[M, List[M]]]: """Persist objects to m2m relation.""" return [m.make() for m in self.related] model_bakery-1.20.4/model_bakery/timezone.py000066400000000000000000000005311475767230300211110ustar00rootroot00000000000000"""Utility functions to manage timezone code.""" from datetime import datetime, timezone from django.conf import settings def tz_aware(value: datetime) -> datetime: """Return an UTC-aware datetime in case of USE_TZ=True.""" if settings.USE_TZ: return value.replace(tzinfo=timezone.utc) return value.replace(tzinfo=None) model_bakery-1.20.4/model_bakery/utils.py000066400000000000000000000110331475767230300204160ustar00rootroot00000000000000import datetime import importlib import inspect import itertools import warnings from types import ModuleType from typing import Any, Callable, Optional, Union from django.apps import apps from .timezone import tz_aware __all__ = ["import_from_str", "get_calling_module", "seq"] def import_from_str(import_string: Optional[Union[Callable, str]]) -> Any: """Import an object defined as import if it is an string. If `import_string` follows the format `path.to.module.object_name`, this method imports it; else it just return the object. """ if isinstance(import_string, str): path, field_name = import_string.rsplit(".", 1) if apps: model = apps.all_models.get(path, {}).get(field_name.lower()) if model: return model module = importlib.import_module(path) return getattr(module, field_name) return import_string def get_calling_module(levels_back: int) -> Optional[ModuleType]: """Get the module some number of stack frames back from the current one. Make sure to account for the number of frames between the "calling" code and the one that calls this function. Args: levels_back (int): Number of stack frames back from the current Returns: (ModuleType): the module from which the code was called """ frame = inspect.stack()[levels_back + 1][0] return inspect.getmodule(frame) def seq(value, increment_by=1, start=None, suffix=None): """Generate a sequence of values based on a running count. This function can be used to generate sequences of `int`, `float`, `datetime`, `date`, `time`, or `str`: whatever the `type` is of the provided `value`. Args: value (object): the value at which to begin generation (this will be ignored for types `datetime`, `date`, and `time`) increment_by (`int` or `float`, optional): the amount by which to increment for each generated value (defaults to `1`) start (`int` or `float`, optional): the value at which the sequence will begin to add to `value` (if `value` is a `str`, `start` will be appended to it) suffix (`str`, optional): for `str` `value` sequences, this will be appended to the end of each generated value (after the counting value is appended) Returns: object: generated values for sequential data """ _validate_sequence_parameters(value, increment_by, start, suffix) if isinstance(value, (datetime.datetime, datetime.date, datetime.time)): if type(value) is datetime.date: date = datetime.datetime.combine(value, datetime.datetime.now().time()) elif type(value) is datetime.time: date = datetime.datetime.combine(datetime.date.today(), value) else: date = value # convert to epoch time epoch_datetime = datetime.datetime(1970, 1, 1, tzinfo=date.tzinfo) start = (date - epoch_datetime).total_seconds() increment_by = increment_by.total_seconds() for n in itertools.count(increment_by, increment_by): series_date = tz_aware( datetime.datetime.fromtimestamp(start + n, tz=datetime.timezone.utc) ) if type(value) is datetime.time: yield series_date.time() elif type(value) is datetime.date: yield series_date.date() else: yield series_date else: for n in itertools.count( increment_by if start is None else start, increment_by ): if suffix: yield value + str(n) + suffix else: yield value + type(value)(n) def _validate_sequence_parameters(value, increment_by, start, suffix) -> None: if suffix: if not isinstance(suffix, str): raise TypeError("Sequences suffix can only be a string") if not isinstance(value, str): raise TypeError("Sequences with suffix can only be used with text values") if isinstance(value, (datetime.datetime, datetime.date, datetime.time)): if not isinstance(increment_by, datetime.timedelta): raise TypeError( "Sequences with values datetime.datetime, datetime.date and datetime.time, " "incremente_by must be a datetime.timedelta." ) if start: warnings.warn( "start parameter is ignored when using seq with date, time or datetime objects", stacklevel=1, ) model_bakery-1.20.4/postgis-tests.sh000077500000000000000000000055701475767230300174470ustar00rootroot00000000000000#!/usr/bin/env bash # -*- coding: utf-8 -*- set -euo pipefail ### ### Run test suite against PostgreSQL DB with Postgis installed. ### Note postgis version is also explicitly set in django test settings ### ### This script will attempt to spin up a PostgreSQL Docker container against ### which the tests will be run. It will spin it down once the tests are finished, ### so if you are already running PostgreSQL locally, instead of using this ### script, simply run: ### ### # TEST_DB=postgis PGUSER=postgres python -m pytest ### ### This script uses the `python` on the current `$PATH`, but can be overridden ### by setting the `PYTHON_CLI` environment variable. ### ### Usage: ### ### ./postgis-tests.sh [-h|--help] ### ### Options: ### ### -h, --help print (this) help and exit ### function help { # Print this file's contents, but only the lines that start # with `###` (documentation lines, above). sed -rn 's/^### ?//;T;p' "$0" } function HAS { # Check if the system has a certain program ($1) installed. # Output is silenced, but the function returns the result of # the `command` statement. command -v $1 > /dev/null 2>&1 return $? } # Script variables PROJECT_ROOT="$(dirname $(readlink -f "$0"))" # Same level as this file # Dependency variables (e.g. PYTHON3_CLI=python3) PYTHON=${PYTHON_CLI:-python} DEPS="$PYTHON" # Arg defaults export PGPORT=${PGPORT:-5432} export PGUSER=${PGUSER:-postgres} export PGPASSWORD=${PGPASSWORD:-postgres} export TEST_DB=${TEST_DB:-postgis} # Argument parsing using "getopt" OPTIONS=h LONGOPTS=help PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@") PARSE_EXIT=$? if [[ $PARSE_EXIT -ne 0 ]]; then exit $PARSE_EXIT fi eval set -- "$PARSED" while true; do case "$1" in -h|--help) help shift exit 0 ;; --) shift break ;; *) echo "Error. Exiting." exit 3 ;; esac done # Grab the remaining positional argument(s) (not needed here) # if [[ $# -ne 0 ]]; then # POSITIONAL_1=$1 # POSITIONAL_1=$2 # else # echo "Missing positional args?" # exit 1 # fi echo "Checking dependencies [$(echo ${DEPS} | sed 's/ /, /g')]..." for dep in $DEPS; do MISSING_DEP=false if ! HAS $dep; then echo "You do not have '${dep}' installed, or it is not available on your PATH!" echo "If '${dep}' is not what it is called on your system, set the ${dep^^}_CLI environment variable." MISSING_DEP=true fi if [[ $MISSING_DEP = true ]]; then echo "Missing dependencies." exit 1 fi done # Run the postgres container with all extensions installed $PROJECT_ROOT/postgres-docker --port $PGPORT # Run the tests $PYTHON -m pytest # Spin down the postgis container $PROJECT_ROOT/postgres-docker --kill model_bakery-1.20.4/postgres-docker000077500000000000000000000104041475767230300173110ustar00rootroot00000000000000#!/usr/bin/env bash # -*- coding: utf-8 -*- set -euo pipefail ### ### Start a PostgreSQL docker container with the proper extensions installed ### for the full test suite. ### ### To kill an already running container, run the same command again with the ### `--kill` flag. ### ### Usage: ### ### ./postgres-docker [-h|--help] [-p|--port PORT] [-k|--kill] ### ### Options: ### ### -h, --help print (this) help and exit ### ### -p, --port PORT specify the host port to which PostgreSQL will be bound ### (defaults to 5432) ### ### -k, --kill given the container is already running, kill it ### function help { # Print this file's contents, but only the lines that start # with `###` (documentation lines, above). sed -rn 's/^### ?//;T;p' "$0" } function HAS { # Check if the system has a certain program ($1) installed. # Output is silenced, but the function returns the result of # the `command` statement. command -v $1 > /dev/null 2>&1 return $? } # Script variables PROJECT_ROOT="$(dirname $(readlink -f "$0"))" # Same level as this file # Dependency variables (e.g. PYTHON3_CLI=python3) DOCKER=${DOCKER_CLI:-docker} DEPS="$DOCKER" # Arg defaults KILL=false CONTAINER_NAME=model-bakery-postgres PORT=${PGPORT:-5432} # Argument parsing using "getopt" OPTIONS=h,k,p LONGOPTS=help,kill,port PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@") PARSE_EXIT=$? if [[ $PARSE_EXIT -ne 0 ]]; then exit $PARSE_EXIT fi eval set -- "$PARSED" while true; do case "$1" in -p|--port) PGPORT=$2 shift ;; -k|--kill) KILL=true shift ;; -h|--help) help shift exit 0 ;; --) shift break ;; *) echo "Error. Exiting." exit 3 ;; esac done # Grab the remaining positional argument(s) (not needed here) # if [[ $# -ne 0 ]]; then # POSITIONAL_1=$1 # POSITIONAL_1=$2 # else # echo "Missing positional args?" # exit 1 # fi echo "Checking dependencies [$(echo ${DEPS} | sed 's/ /, /g')]..." for dep in $DEPS; do MISSING_DEP=false if ! HAS $dep; then echo "You do not have '${dep}' installed, or it is not available on your PATH!" echo "If '${dep}' is not what it is called on your system, set the ${dep^^}_CLI environment variable." MISSING_DEP=true fi if [[ $MISSING_DEP = true ]]; then echo "Missing dependencies." exit 1 fi done # Check if already running if $DOCKER container inspect $CONTAINER_NAME > /dev/null 2>&1; then RUNNING_ALREADY=true else RUNNING_ALREADY=false fi # If stop desired, stop container if [[ $KILL = true ]]; then if [[ $RUNNING_ALREADY = true ]]; then echo "Stopping '${CONTAINER_NAME}'..." $DOCKER stop $CONTAINER_NAME exit 0 else echo "Container '${CONTAINER_NAME}' is not currently running." exit 1 fi fi # If running already, do nothing but check port if [[ $RUNNING_ALREADY = true ]]; then RUNNING_PORT=$($DOCKER container inspect $CONTAINER_NAME | grep -m 1 -Po "(?<=HostPort\": \")\d+") if [[ $RUNNING_PORT -ne $PORT ]]; then echo "Container '${CONTAINER_NAME}' is already running, but on the wrong port!" echo "Container is using port ${RUNNING_PORT} and the specified port is ${PORT}." exit 1 else echo "Container '${CONTAINER_NAME}' is already running." exit 0 fi fi # Start postgres container echo "Starting '${CONTAINER_NAME}'..." $DOCKER run --rm --name ${CONTAINER_NAME} -e POSTGRES_HOST_AUTH_METHOD=trust -p ${PORT}:5432 -d postgis/postgis:12-3.3 # Wait a few seconds so the DB container can start up echo "Waiting for DB container..." sleep 5s # Enable all of the extensions needed on the template1 database $DOCKER exec $CONTAINER_NAME /bin/bash -c "psql template1 -c \"CREATE EXTENSION IF NOT EXISTS citext;\" -U postgres" $DOCKER exec $CONTAINER_NAME /bin/bash -c "psql template1 -c \"CREATE EXTENSION IF NOT EXISTS hstore;\" -U postgres" $DOCKER exec $CONTAINER_NAME /bin/bash -c "psql template1 -c \"CREATE EXTENSION IF NOT EXISTS postgis;\" -U postgres" model_bakery-1.20.4/pyproject.toml000066400000000000000000000054361475767230300171750ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "model-bakery" dynamic = ["version"] description = "Smart object creation facility for Django." readme = "README.md" requires-python = ">=3.8" license = {text = "Apache License 2.0"} authors = [ { name = "berin", email = "bernardoxhc@gmail.com" }, { name = "amureki", email = "amureki@hey.com" }, ] keywords = [ "django", "factory", "python", "testing", ] classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Django", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", "Framework :: Django :: 5.1", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development", ] dependencies = [ "django>=4.2", ] [project.optional-dependencies] test = [ "coverage", "pillow", "pytest", "pytest-django", "black", "ruff", "mypy", ] docs = [ "Sphinx", "sphinx-rtd-theme", "myst-parser", ] [project.urls] Homepage = "https://github.com/model-bakers/model_bakery" [tool.coverage.run] branch = true parallel = true source = [ "model_bakery", ] omit = [ "model_bakery/__about__.py", "model_bakery/_types.py", ] [tool.coverage.paths] source = [ "model_bakery", ".tox/**/site-packages/model_bakery", ] [tool.coverage.report] show_missing = true [tool.hatch.version] path = "model_bakery/__about__.py" [tool.hatch.build.targets.sdist] include = [ "/model_bakery", ] [tool.mypy] ignore_missing_imports = true disallow_untyped_calls = true [tool.pytest.ini_options] addopts = "--tb=short -rxs --nomigrations" [tool.ruff.lint] select = [ "S", # flake8-bandit "B", # flake8-bugbear "C", # flake8-comprehensions "SIM", # flake8-simplify "I", # isort "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "D", # pydocstyle "UP", # pyupgrade "RUF100", # Unused noqa directive ] ignore = ["B904", "E501", "S101", "D1", "D212"] [tool.ruff.lint.per-file-ignores] "tests/test_*.py" = [ "S", ] [tool.ruff.lint.isort] combine-as-imports=true split-on-trailing-comma=true section-order = ["future", "standard-library", "django", "third-party", "first-party", "local-folder"] force-wrap-aliases=true [tool.ruff.lint.isort.sections] django = ["django"] [tool.ruff.lint.pydocstyle] convention = "google" model_bakery-1.20.4/tests/000077500000000000000000000000001475767230300154135ustar00rootroot00000000000000model_bakery-1.20.4/tests/ambiguous/000077500000000000000000000000001475767230300174065ustar00rootroot00000000000000model_bakery-1.20.4/tests/ambiguous/__init__.py000066400000000000000000000000001475767230300215050ustar00rootroot00000000000000model_bakery-1.20.4/tests/ambiguous/models.py000066400000000000000000000001511475767230300212400ustar00rootroot00000000000000from django.db import models class Ambiguous(models.Model): name = models.CharField(max_length=20) model_bakery-1.20.4/tests/ambiguous2/000077500000000000000000000000001475767230300174705ustar00rootroot00000000000000model_bakery-1.20.4/tests/ambiguous2/__init__.py000066400000000000000000000000001475767230300215670ustar00rootroot00000000000000model_bakery-1.20.4/tests/ambiguous2/models.py000066400000000000000000000001511475767230300213220ustar00rootroot00000000000000from django.db import models class Ambiguous(models.Model): name = models.CharField(max_length=20) model_bakery-1.20.4/tests/conftest.py000066400000000000000000000056501475767230300176200ustar00rootroot00000000000000import os import django from django.conf import settings def pytest_configure(): test_db = os.environ.get("TEST_DB", "sqlite") use_contenttypes = os.environ.get("USE_CONTENTTYPES", False) installed_apps = [ "tests.generic", "tests.ambiguous", "tests.ambiguous2", ] if use_contenttypes: installed_apps.append("django.contrib.contenttypes") # auth app depends on contenttypes installed_apps.append("django.contrib.auth") using_postgres_flag = False postgis_version = () if test_db == "sqlite": db_engine = "django.db.backends.sqlite3" db_name = ":memory:" extra_db_name = ":memory:" elif test_db == "postgresql": using_postgres_flag = True if django.VERSION >= (4, 2): db_engine = "django.db.backends.postgresql" else: db_engine = "django.db.backends.postgresql_psycopg2" db_name = "postgres" installed_apps = ["django.contrib.postgres"] + installed_apps extra_db_name = "extra_db" elif test_db == "postgis": using_postgres_flag = True db_engine = "django.contrib.gis.db.backends.postgis" db_name = "postgres" extra_db_name = "extra_db" installed_apps = [ "django.contrib.postgres", "django.contrib.gis", ] + installed_apps postgis_version = (11, 3, 0) else: raise NotImplementedError(f"Tests for {test_db} are not supported") EXTRA_DB = "extra" settings.configure( EXTRA_DB=EXTRA_DB, DATABASES={ "default": { "ENGINE": db_engine, "NAME": db_name, "HOST": "localhost", # The following DB settings are only used for `postgresql` and `postgis` "PORT": os.environ.get("PGPORT", ""), "USER": os.environ.get("PGUSER", ""), "PASSWORD": os.environ.get("PGPASSWORD", ""), }, # Extra DB used to test multi database support EXTRA_DB: { "ENGINE": db_engine, "NAME": extra_db_name, "HOST": "localhost", "PORT": os.environ.get("PGPORT", ""), "USER": os.environ.get("PGUSER", ""), "PASSWORD": os.environ.get("PGPASSWORD", ""), }, }, INSTALLED_APPS=installed_apps, LANGUAGE_CODE="en", SITE_ID=1, MIDDLEWARE=(), USE_TZ=os.environ.get("USE_TZ", False), USING_POSTGRES=using_postgres_flag, # Set the version explicitly otherwise Django does extra queries # to get the version via SQL when using POSTGIS POSTGIS_VERSION=postgis_version, ) django.setup() from model_bakery import baker def gen_same_text(): return "always the same text" baker.generators.add("tests.generic.fields.CustomFieldViaSettings", gen_same_text) model_bakery-1.20.4/tests/generic/000077500000000000000000000000001475767230300170275ustar00rootroot00000000000000model_bakery-1.20.4/tests/generic/__init__.py000066400000000000000000000000001475767230300211260ustar00rootroot00000000000000model_bakery-1.20.4/tests/generic/baker_recipes.py000066400000000000000000000055761475767230300222140ustar00rootroot00000000000000# ATTENTION: Recipes defined for testing purposes only from datetime import timedelta from decimal import Decimal from django.utils.timezone import now from model_bakery.recipe import Recipe, foreign_key, related, seq from tests.generic.models import ( TEST_TIME, Dog, DummyDefaultFieldsModel, DummyUniqueIntegerFieldModel, LonelyPerson, Person, School, ) person = Recipe( Person, name="John Doe", nickname="joe", age=18, bio="Someone in the crowd", blog="http://joe.blogspot.com", days_since_last_login=4, birthday=now().date(), appointment=now(), birth_time=now(), ) lonely_person = Recipe(LonelyPerson, only_friend=foreign_key(person, one_to_one=True)) serial_person = Recipe( Person, name=seq("joe"), ) serial_numbers = Recipe( DummyDefaultFieldsModel, default_decimal_field=seq(Decimal("20.1")), default_int_field=seq(10), default_float_field=seq(1.23), ) serial_numbers_by = Recipe( DummyDefaultFieldsModel, default_decimal_field=seq(Decimal("20.1"), increment_by=Decimal("2.4")), default_int_field=seq(10, increment_by=3), default_float_field=seq(1.23, increment_by=1.8), ) serial_datetime = Recipe( DummyDefaultFieldsModel, default_date_field=seq(TEST_TIME.date(), timedelta(days=1)), default_date_time_field=seq(TEST_TIME, timedelta(hours=3)), default_time_field=seq(TEST_TIME.time(), timedelta(seconds=15), start="xpto"), ) dog = Recipe(Dog, breed="Pug", owner=foreign_key(person)) homeless_dog = Recipe( Dog, breed="Pug", ) other_dog = Recipe(Dog, breed="Basset", owner=foreign_key("person")) dog_with_friends = dog.extend( friends_with=related(dog, dog), ) dog_with_more_friends = dog.extend( friends_with=related(dog_with_friends), ) extended_dog = dog.extend( breed="Super basset", ) paulo_freire_school = Recipe(School, name="Escola Municipal Paulo Freire") class SmallDogRecipe(Recipe): pass small_dog = SmallDogRecipe(Dog) pug = small_dog.extend( breed="Pug", ) other_dog_unicode = Recipe(Dog, breed="Basset", owner=foreign_key("person")) dummy_unique_field = Recipe( DummyUniqueIntegerFieldModel, value=seq(10), ) dog_lady = Recipe(Person, dog_set=related("dog", other_dog)) nullable_related = Recipe( "generic.DummyBlankFieldsModel", dummynullfieldsmodel_set=related(Recipe("generic.DummyNullFieldsModel")), ) cast_member = Recipe("generic.CastMember", person=foreign_key(person)) movie_with_cast = Recipe( "generic.Movie", cast_members=related(cast_member, cast_member) ) overwritten_save = Recipe("generic.ModelWithOverwrittenSave") with_save_kwargs = Recipe( "generic.ModelWithSaveKwargs", _save_kwargs={"breed": "updated_breed"} ) ip_fields = Recipe( "generic.DummyGenericIPAddressFieldModel", ipv4_field=seq("127.0.0.", increment_by=2), ipv6_field=seq("2001:12f8:0:28::", start=4, increment_by=2), ) model_bakery-1.20.4/tests/generic/fields.py000066400000000000000000000006661475767230300206570ustar00rootroot00000000000000from django.db import models class CustomFieldWithGenerator(models.TextField): pass class CustomFieldWithoutGenerator(models.TextField): pass class CustomFieldViaSettings(models.TextField): pass class FakeListField(models.TextField): def to_python(self, value): return value.split() def get_prep_value(self, value): return " ".join(value) class CustomForeignKey(models.ForeignKey): pass model_bakery-1.20.4/tests/generic/forms.py000066400000000000000000000004251475767230300205300ustar00rootroot00000000000000from django.forms import ModelForm from tests.generic.models import DummyGenericIPAddressFieldModel class DummyGenericIPAddressFieldForm(ModelForm): class Meta: fields = ("ipv4_field", "ipv6_field", "ipv46_field") model = DummyGenericIPAddressFieldModel model_bakery-1.20.4/tests/generic/generators.py000066400000000000000000000000531475767230300215500ustar00rootroot00000000000000def gen_value_string(): return "value" model_bakery-1.20.4/tests/generic/models.py000077500000000000000000000350071475767230300206740ustar00rootroot00000000000000######################################## # TESTING PURPOSE ONLY MODELS!! # # DO NOT ADD THE APP TO INSTALLED_APPS # ######################################## import datetime from decimal import Decimal from tempfile import gettempdir import django from django.conf import settings from django.core.files.storage import FileSystemStorage from django.utils.timezone import now from model_bakery.baker import BAKER_CONTENTTYPES from model_bakery.gis import BAKER_GIS from model_bakery.timezone import tz_aware from .fields import ( CustomFieldViaSettings, CustomFieldWithGenerator, CustomFieldWithoutGenerator, CustomForeignKey, FakeListField, ) # check whether PIL is installed try: from PIL import ImageFile as PilImageFile # NoQA except ImportError: has_pil = False else: has_pil = True if BAKER_GIS: from django.contrib.gis.db import models else: from django.db import models # check if the contenttypes app is installed if BAKER_CONTENTTYPES: from django.contrib.contenttypes import models as contenttypes from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation else: contenttypes = None GenericRelation = None GenericForeignKey = None GENDER_CHOICES = [ ("M", "male"), ("F", "female"), ("N", "non-binary"), ] OCCUPATION_CHOICES = ( ("Service Industry", (("waitress", "Waitress"), ("bartender", "Bartender"))), ("Education", (("teacher", "Teacher"), ("principal", "Principal"))), ) TEST_TIME = datetime.datetime(2014, 7, 21, 15, 39, 58, 457698) class ModelWithImpostorField(models.Model): pass class Profile(models.Model): email = models.EmailField() class User(models.Model): profile = models.ForeignKey( Profile, blank=True, null=True, on_delete=models.CASCADE ) username = models.CharField(max_length=32) class PaymentBill(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) value = models.FloatField() class Person(models.Model): gender = models.CharField(max_length=1, choices=GENDER_CHOICES) # Jards Macalé is an amazing brazilian musician! =] enjoy_jards_macale = models.BooleanField(default=True) like_metal_music = models.BooleanField(default=False) name = models.CharField(max_length=30) nickname = models.SlugField(max_length=36) age = models.IntegerField() bio_summary = models.TextField(max_length=120) bio = models.TextField() birthday = models.DateField() birth_time = models.TimeField() appointment = models.DateTimeField() blog = models.URLField() occupation = models.CharField(max_length=10, choices=OCCUPATION_CHOICES) uuid = models.UUIDField(primary_key=False) name_hash = models.BinaryField(max_length=16) days_since_last_login = models.SmallIntegerField() days_since_account_creation = models.BigIntegerField() duration_of_sleep = models.DurationField() email = models.EmailField() id_document = models.CharField(unique=True, max_length=10) data = models.JSONField() if django.VERSION >= (5, 0): retirement_age = models.IntegerField(db_default=20) try: from django.contrib.postgres.fields import ( ArrayField, HStoreField, ) from django.contrib.postgres.fields.citext import ( CICharField, CIEmailField, CITextField, ) from django.contrib.postgres.fields.ranges import ( BigIntegerRangeField, DateRangeField, DateTimeRangeField, DecimalRangeField, IntegerRangeField, ) if settings.USING_POSTGRES: if django.VERSION >= (4, 2): long_name = models.CharField() acquaintances = ArrayField(models.IntegerField()) hstore_data = HStoreField() ci_char = CICharField(max_length=30) ci_email = CIEmailField() ci_text = CITextField() ci_text_max_length = CITextField(max_length=120) int_range = IntegerRangeField() bigint_range = BigIntegerRangeField() date_range = DateRangeField() datetime_range = DateTimeRangeField() decimal_range = DecimalRangeField() except ImportError: # Skip PostgreSQL-related fields pass if BAKER_GIS: geom = models.GeometryField() point = models.PointField() line_string = models.LineStringField() polygon = models.PolygonField() multi_point = models.MultiPointField() multi_line_string = models.MultiLineStringField() multi_polygon = models.MultiPolygonField() geom_collection = models.GeometryCollectionField() class Dog(models.Model): class Meta: order_with_respect_to = "owner" owner = models.ForeignKey("Person", on_delete=models.CASCADE) breed = models.CharField(max_length=50) created = models.DateTimeField(auto_now_add=True) friends_with = models.ManyToManyField("Dog") class GuardDog(Dog): pass class Home(models.Model): address = models.CharField(max_length=200) owner = models.ForeignKey("Person", on_delete=models.CASCADE) dogs = models.ManyToManyField("Dog") class LonelyPerson(models.Model): only_friend = models.OneToOneField(Person, on_delete=models.CASCADE) class Cake(models.Model): name = models.CharField(max_length=64) class RelatedNamesModel(models.Model): name = models.CharField(max_length=256) one_to_one = models.OneToOneField( Person, related_name="one_related", on_delete=models.CASCADE ) foreign_key = models.ForeignKey( Person, related_name="fk_related", on_delete=models.CASCADE ) def get_default_cake_id(): instance, _ = Cake.objects.get_or_create(name="Muffin") return instance.id class RelatedNamesWithDefaultsModel(models.Model): name = models.CharField(max_length=256, default="Bravo") cake = models.ForeignKey( Cake, on_delete=models.CASCADE, default=get_default_cake_id, ) class RelatedNamesWithEmptyDefaultsModel(models.Model): name = models.CharField(max_length=256, default="Bravo") cake = models.ForeignKey( Cake, on_delete=models.CASCADE, null=True, default=None, ) class ModelWithOverwrittenSave(Dog): def save(self, *args, **kwargs): self.owner = kwargs.pop("owner") return super().save(*args, **kwargs) class ModelWithSaveKwargs(Dog): def save(self, *args, **kwargs): self.breed = kwargs.pop("breed") return super().save(*args, **kwargs) class Classroom(models.Model): students = models.ManyToManyField(Person, null=True) active = models.BooleanField(null=True) class ClassroomM2MRelated(models.Model): """ Regression test for #248. A model with an M2M field (Classroom) being also used as an M2M field from another model (ClassroomM2MRelated). """ related_classrooms = models.ManyToManyField(Classroom) class Store(models.Model): customers = models.ManyToManyField(Person, related_name="favorite_stores") employees = models.ManyToManyField(Person, related_name="employers") suppliers = models.ManyToManyField( Person, related_name="suppliers", blank=True, null=True ) class DummyEmptyModel(models.Model): pass class DummyIntModel(models.Model): int_field = models.IntegerField() small_int_field = models.SmallIntegerField() big_int_field = models.BigIntegerField() class DummyPositiveIntModel(models.Model): positive_small_int_field = models.PositiveSmallIntegerField() positive_int_field = models.PositiveIntegerField() positive_big_int_field = models.PositiveBigIntegerField() class DummyNumbersModel(models.Model): float_field = models.FloatField() class DummyDecimalModel(models.Model): decimal_field = models.DecimalField(max_digits=1, decimal_places=0) class UnsupportedField(models.Field): description = "I'm bad company, baker doesn't know me" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) class UnsupportedModel(models.Model): unsupported_field = UnsupportedField() if BAKER_CONTENTTYPES: class DummyGenericForeignKeyModel(models.Model): content_type = models.ForeignKey( contenttypes.ContentType, on_delete=models.CASCADE, limit_choices_to={"model__in": ["person", "dog"]}, ) object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") class DummyGenericRelationModel(models.Model): relation = GenericRelation(DummyGenericForeignKeyModel) class GenericForeignKeyModelWithOptionalData(models.Model): content_type = models.ForeignKey( contenttypes.ContentType, on_delete=models.CASCADE, blank=True, null=True, limit_choices_to={"model__in": ["person", "dog"]}, ) object_id = models.PositiveIntegerField(blank=True, null=True) content_object = GenericForeignKey("content_type", "object_id") class GenericRelationModelWithOptionalData(models.Model): relation = GenericRelation(GenericForeignKeyModelWithOptionalData) class DummyNullFieldsModel(models.Model): null_foreign_key = models.ForeignKey( "DummyBlankFieldsModel", null=True, on_delete=models.CASCADE ) null_integer_field = models.IntegerField(null=True) class DummyBlankFieldsModel(models.Model): blank_char_field = models.CharField(max_length=50, blank=True) blank_text_field = models.TextField(max_length=300, blank=True) class ExtendedDefaultField(models.IntegerField): pass class DummyDefaultFieldsModel(models.Model): default_id = models.AutoField(primary_key=True) default_char_field = models.CharField(max_length=50, default="default") default_text_field = models.TextField(default="default") default_int_field = models.IntegerField(default=123) default_float_field = models.FloatField(default=123.0) default_date_field = models.DateField(default="2012-01-01") default_date_time_field = models.DateTimeField( default=tz_aware(datetime.datetime(2012, 1, 1)) ) default_time_field = models.TimeField(default="00:00:00") default_decimal_field = models.DecimalField( max_digits=5, decimal_places=2, default=Decimal("0") ) default_email_field = models.EmailField(default="foo@bar.org") default_slug_field = models.SlugField(default="a-slug") default_unknown_class_field = ExtendedDefaultField(default=42) default_callable_int_field = models.IntegerField(default=lambda: 12) default_callable_datetime_field = models.DateTimeField(default=now) class DummyFileFieldModel(models.Model): fs = FileSystemStorage(location=gettempdir()) file_field = models.FileField(upload_to="%Y/%m/%d", storage=fs) if has_pil: class DummyImageFieldModel(models.Model): fs = FileSystemStorage(location=gettempdir()) image_field = models.ImageField(upload_to="%Y/%m/%d", storage=fs) else: # doesn't matter, won't be using class DummyImageFieldModel(models.Model): pass class NestedFileFieldModel(models.Model): attached_file = models.ForeignKey(DummyFileFieldModel, on_delete=models.CASCADE) class DummyMultipleInheritanceModel(DummyDefaultFieldsModel, Person): my_id = models.AutoField(primary_key=True) my_dummy_field = models.IntegerField() class Ambiguous(models.Model): name = models.CharField(max_length=20) class School(models.Model): name = models.CharField(max_length=50) students = models.ManyToManyField(Person, through="SchoolEnrollment") class SchoolEnrollment(models.Model): start_date = models.DateField(auto_now_add=True) school = models.ForeignKey(School, on_delete=models.CASCADE) student = models.ForeignKey(Person, on_delete=models.CASCADE) class NonAbstractPerson(Person): dummy_count = models.IntegerField() class CustomFieldWithGeneratorModel(models.Model): custom_value = CustomFieldWithGenerator() class CustomFieldWithoutGeneratorModel(models.Model): custom_value = CustomFieldWithoutGenerator() class CustomFieldViaSettingsModel(models.Model): custom_value = CustomFieldViaSettings() class CustomForeignKeyWithGeneratorModel(models.Model): custom_fk = CustomForeignKey( Profile, blank=True, null=True, on_delete=models.CASCADE ) class DummyUniqueIntegerFieldModel(models.Model): value = models.IntegerField(unique=True) class ModelWithNext(models.Model): attr = models.CharField(max_length=10) def next(self): return "foo" class BaseModelForNext(models.Model): fk = models.ForeignKey(ModelWithNext, on_delete=models.CASCADE) class BaseModelForList(models.Model): fk = FakeListField() class Movie(models.Model): title = models.CharField(max_length=30) class MovieManager(models.Manager): def get_queryset(self): """ Annotate queryset with an alias field 'name'. We want to test whether this annotation has been run after calling `baker.make()`. """ return super().get_queryset().annotate(name=models.F("title")) class MovieWithAnnotation(Movie): objects = MovieManager() class CastMember(models.Model): movie = models.ForeignKey( Movie, related_name="cast_members", on_delete=models.CASCADE ) person = models.ForeignKey(Person, on_delete=models.CASCADE) class DummyGenericIPAddressFieldModel(models.Model): ipv4_field = models.GenericIPAddressField(protocol="IPv4") ipv6_field = models.GenericIPAddressField(protocol="IPv6") ipv46_field = models.GenericIPAddressField(protocol="both") class AbstractModel(models.Model): class Meta: abstract = True name = models.CharField(max_length=30) class SubclassOfAbstract(AbstractModel): height = models.IntegerField() class NonStandardManager(models.Model): name = models.CharField(max_length=30) manager = models.Manager() # The following models were added after issue 291 # Since they don't hold much meaning, they are only numbered ones class Issue291Model1(models.Model): pass class Issue291Model2(models.Model): m2m_model_1 = models.ManyToManyField(Issue291Model1) class Issue291Model3(models.Model): fk_model_2 = models.ForeignKey( Issue291Model2, related_name="bazs", on_delete=models.CASCADE ) name = models.CharField(max_length=32) class ModelWithAutoNowFields(models.Model): sent_date = models.DateTimeField() created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) model_bakery-1.20.4/tests/generic/tests/000077500000000000000000000000001475767230300201715ustar00rootroot00000000000000model_bakery-1.20.4/tests/generic/tests/sub_package/000077500000000000000000000000001475767230300224355ustar00rootroot00000000000000model_bakery-1.20.4/tests/generic/tests/sub_package/__init__.py000066400000000000000000000000001475767230300245340ustar00rootroot00000000000000model_bakery-1.20.4/tests/generic/tests/sub_package/baker_recipes.py000066400000000000000000000006051475767230300256060ustar00rootroot00000000000000from datetime import date, datetime from model_bakery.recipe import Recipe from tests.generic.models import Person person = Recipe( Person, name="John Deeper", nickname="joe", age=18, bio="Someone in the crowd", blog="http://joe.blogspot.com", days_since_last_login=4, birthday=date.today(), appointment=datetime.now(), birth_time=datetime.now, ) model_bakery-1.20.4/tests/test_baker.py000066400000000000000000001300201475767230300201040ustar00rootroot00000000000000import datetime import itertools from decimal import Decimal from unittest.mock import patch from django.apps import apps from django.conf import settings from django.db.models import Manager from django.db.models.signals import m2m_changed from django.test import TestCase, override_settings import pytest from model_bakery import baker, random_gen from model_bakery.baker import BAKER_CONTENTTYPES, MAX_MANY_QUANTITY from model_bakery.exceptions import ( AmbiguousModelName, InvalidQuantityException, ModelNotFound, ) from model_bakery.timezone import tz_aware from tests.generic import baker_recipes, models from tests.generic.forms import DummyGenericIPAddressFieldForm def test_import_seq_from_baker(): """Test import seq method from baker module.""" try: from model_bakery.baker import seq # NoQA except ImportError: pytest.fail(f"{ImportError.__name__} raised") class TestsModelFinder: def test_unicode_regression(self): obj = baker.prepare("generic.Person") assert isinstance(obj, models.Person) def test_model_class(self): obj = baker.prepare(models.Person) assert isinstance(obj, models.Person) def test_app_model_string(self): obj = baker.prepare("generic.Person") assert isinstance(obj, models.Person) def test_model_string(self): obj = baker.prepare("Person") assert isinstance(obj, models.Person) def test_raise_on_ambiguous_model_string(self): with pytest.raises(AmbiguousModelName): baker.prepare("Ambiguous") def test_raise_model_not_found(self): with pytest.raises(ModelNotFound): baker.Baker("non_existing.Model") with pytest.raises(ModelNotFound): baker.Baker("NonExistingModel") class TestRecipeFinder: def test_from_app_module(self): obj = baker.prepare_recipe("generic.person") assert isinstance(obj, models.Person) assert obj.name == "John Doe" def test_full_path_from_app_module(self): obj = baker.prepare_recipe("tests.generic.person") assert isinstance(obj, models.Person) assert obj.name == "John Doe" def test_from_non_app_module(self): obj = baker.prepare_recipe("uninstalled.person") assert isinstance(obj, models.Person) assert obj.name == "Uninstalled" def test_full_path_from_non_app_module(self): obj = baker.prepare_recipe("tests.uninstalled.person") assert isinstance(obj, models.Person) assert obj.name == "Uninstalled" def test_raise_on_non_module_path(self): # Error trying to parse "app_name" + "recipe_name" from provided string with pytest.raises(ValueError): baker.prepare_recipe("person") @pytest.mark.django_db class TestsBakerCreatesSimpleModel: def test_consider_real_django_fields_only(self): id_ = models.ModelWithImpostorField._meta.get_field("id") with patch.object(baker.Baker, "get_fields") as mock: f = Manager() f.name = "foo" mock.return_value = [id_, f] try: baker.make(models.ModelWithImpostorField) except TypeError: raise AssertionError("TypeError raised") def test_make_should_create_one_object(self): person = baker.make(models.Person) assert isinstance(person, models.Person) # makes sure it is the person we created assert models.Person.objects.filter(id=person.id).exists() def test_prepare_should_not_persist_one_object(self): person = baker.prepare(models.Person) assert isinstance(person, models.Person) # makes sure database is clean assert not models.Person.objects.all().exists() assert person.id is None def test_non_abstract_model_creation(self): person = baker.make( models.NonAbstractPerson, name="bob", enjoy_jards_macale=False ) assert isinstance(person, models.NonAbstractPerson) assert person.name == "bob" assert person.enjoy_jards_macale is False def test_abstract_model_subclass_creation(self): instance = baker.make(models.SubclassOfAbstract) assert isinstance(instance, models.SubclassOfAbstract) assert isinstance(instance, models.AbstractModel) assert isinstance(instance.name, str) assert len(instance.name) == 30 assert isinstance(instance.height, int) def test_multiple_inheritance_creation(self): multiple = baker.make(models.DummyMultipleInheritanceModel) assert isinstance(multiple, models.DummyMultipleInheritanceModel) assert models.Person.objects.filter(id=multiple.id).exists() assert models.DummyDefaultFieldsModel.objects.filter( default_id=multiple.default_id ).exists() @pytest.mark.django_db class TestsBakerRepeatedCreatesSimpleModel(TestCase): def test_make_should_create_objects_respecting_quantity_parameter(self): with self.assertNumQueries(5): baker.make(models.Person, _quantity=5) assert models.Person.objects.count() == 5 with self.assertNumQueries(5): people = baker.make(models.Person, _quantity=5, name="George Washington") assert all(p.name == "George Washington" for p in people) def test_make_quantity_respecting_bulk_create_parameter(self): query_count = 1 with self.assertNumQueries(query_count): baker.make(models.Person, _quantity=5, _bulk_create=True) assert models.Person.objects.count() == 5 with self.assertNumQueries(query_count): people = baker.make( models.Person, name="George Washington", _quantity=5, _bulk_create=True ) assert all(p.name == "George Washington" for p in people) with self.assertNumQueries(query_count): baker.make(models.NonStandardManager, _quantity=3, _bulk_create=True) assert getattr(models.NonStandardManager, "objects", None) is None assert ( models.NonStandardManager._base_manager == models.NonStandardManager.manager ) assert ( models.NonStandardManager._default_manager == models.NonStandardManager.manager ) assert models.NonStandardManager.manager.count() == 3 def test_make_raises_correct_exception_if_invalid_quantity(self): with pytest.raises(InvalidQuantityException): baker.make(_model=models.Person, _quantity="hi") with pytest.raises(InvalidQuantityException): baker.make(_model=models.Person, _quantity=-1) with pytest.raises(InvalidQuantityException): baker.make(_model=models.Person, _quantity=0) def test_prepare_should_create_objects_respecting_quantity_parameter(self): people = baker.prepare(models.Person, _quantity=5) assert len(people) == 5 assert all(not p.id for p in people) people = baker.prepare(models.Person, _quantity=5, name="George Washington") assert all(p.name == "George Washington" for p in people) def test_prepare_raises_correct_exception_if_invalid_quantity(self): with pytest.raises(InvalidQuantityException): baker.prepare(_model=models.Person, _quantity="hi") with pytest.raises(InvalidQuantityException): baker.prepare(_model=models.Person, _quantity=-1) with pytest.raises(InvalidQuantityException): baker.prepare(_model=models.Person, _quantity=0) def test_accepts_generators_with_quantity(self): baker.make( models.Person, name=itertools.cycle(["a", "b", "c"]), id_document=itertools.cycle(["d1", "d2", "d3", "d4", "d5"]), _quantity=5, ) assert models.Person.objects.count() == 5 p1, p2, p3, p4, p5 = models.Person.objects.all().order_by("pk") assert p1.name == "a" assert p1.id_document == "d1" assert p2.name == "b" assert p2.id_document == "d2" assert p3.name == "c" assert p3.id_document == "d3" assert p4.name == "a" assert p4.id_document == "d4" assert p5.name == "b" assert p5.id_document == "d5" def test_accepts_generators_with_quantity_for_unique_fields(self): baker.make( models.DummyUniqueIntegerFieldModel, value=itertools.cycle([1, 2, 3]), _quantity=3, ) assert models.DummyUniqueIntegerFieldModel.objects.count() == 3 num_1, num_2, num_3 = models.DummyUniqueIntegerFieldModel.objects.all() assert num_1.value == 1 assert num_2.value == 2 assert num_3.value == 3 # skip if auth app is not installed @pytest.mark.skipif( not apps.is_installed("django.contrib.auth"), reason="Django auth app is not installed", ) def test_generators_work_with_user_model(self): from django.contrib.auth import get_user_model User = get_user_model() baker.make(User, username=itertools.cycle(["a", "b", "c"]), _quantity=3) assert User.objects.count() == 3 u1, u2, u3 = User.objects.all() assert u1.username == "a" assert u2.username == "b" assert u3.username == "c" @pytest.mark.django_db class TestBakerPrepareSavingRelatedInstances: def test_default_behaviour_for_and_fk(self): dog = baker.prepare(models.Dog) assert dog.pk is None assert dog.owner.pk is None with pytest.raises(ValueError): assert dog.friends_with def test_create_fk_instances(self): dog = baker.prepare(models.Dog, _save_related=True) assert dog.pk is None assert dog.owner.pk with pytest.raises(ValueError): assert dog.friends_with def test_create_fk_instances_with_quantity(self): dog1, dog2 = baker.prepare(models.Dog, _save_related=True, _quantity=2) assert dog1.pk is None assert dog1.owner.pk with pytest.raises(ValueError): assert dog1.friends_with assert dog2.pk is None assert dog2.owner.pk with pytest.raises(ValueError): assert dog2.friends_with def test_create_one_to_one(self): lonely_person = baker.prepare(models.LonelyPerson, _save_related=True) assert lonely_person.pk is None assert lonely_person.only_friend.pk def test_recipe_prepare_model_with_one_to_one_and_save_related(self): lonely_person = baker_recipes.lonely_person.prepare(_save_related=True) assert lonely_person.pk is None assert lonely_person.only_friend.pk @pytest.mark.django_db class TestBakerCreatesAssociatedModels(TestCase): def test_dependent_models_with_ForeignKey(self): dog = baker.make(models.Dog) assert isinstance(dog.owner, models.Person) def test_foreign_key_on_parent_should_create_one_object(self): person_count = models.Person.objects.count() baker.make(models.GuardDog) assert models.Person.objects.count() == person_count + 1 def test_foreign_key_on_parent_is_not_created(self): """Foreign key on parent doesn't get created using owner.""" owner = baker.make(models.Person) person_count = models.Person.objects.count() dog = baker.make(models.GuardDog, owner=owner) assert models.Person.objects.count() == person_count assert dog.owner == owner def test_foreign_key_on_parent_id_is_not_created(self): """Foreign key on parent doesn't get created using owner_id.""" owner = baker.make(models.Person) person_count = models.Person.objects.count() dog = baker.make(models.GuardDog, owner_id=owner.id) assert models.Person.objects.count() == person_count assert models.GuardDog.objects.get(pk=dog.pk).owner == owner def test_auto_now_add_on_parent_should_work(self): person_count = models.Person.objects.count() dog = baker.make(models.GuardDog) assert models.Person.objects.count() == person_count + 1 assert dog.created def test_attrs_on_related_model_through_parent(self): baker.make(models.GuardDog, owner__name="john") for person in models.Person.objects.all(): assert person.name == "john" def test_access_related_name_of_m2m(self): try: baker.make(models.Person, classroom_set=[baker.make(models.Classroom)]) except TypeError: raise AssertionError("type error raised") def test_save_object_instances_when_handling_one_to_many_relations(self): owner = baker.make(models.Person) dogs_set = baker.prepare( models.Dog, owner=owner, _quantity=2, ) assert models.Dog.objects.count() == 0 # ensure there are no dogs in our db home = baker.make( models.Home, owner=owner, dogs=dogs_set, ) assert home.dogs.count() == 2 assert models.Dog.objects.count() == 2 # dogs in dogs_set were created def test_prepare_fk(self): dog = baker.prepare(models.Dog) assert isinstance(dog, models.Dog) assert isinstance(dog.owner, models.Person) assert models.Person.objects.all().count() == 0 assert models.Dog.objects.all().count() == 0 def test_create_one_to_one(self): lonely_person = baker.make(models.LonelyPerson) assert models.LonelyPerson.objects.all().count() == 1 assert isinstance(lonely_person.only_friend, models.Person) assert models.Person.objects.all().count() == 1 def test_create_multiple_one_to_one(self): baker.make(models.LonelyPerson, _quantity=5) assert models.LonelyPerson.objects.all().count() == 5 assert models.Person.objects.all().count() == 5 def test_bulk_create_multiple_one_to_one(self): query_count = 6 with self.assertNumQueries(query_count): baker.make(models.LonelyPerson, _quantity=5, _bulk_create=True) assert models.LonelyPerson.objects.all().count() == 5 assert models.Person.objects.all().count() == 5 def test_chaining_bulk_create_reduces_query_count(self): query_count = 3 with self.assertNumQueries(query_count): baker.make(models.Person, _quantity=5, _bulk_create=True) person_iter = models.Person.objects.all().iterator() baker.make( models.LonelyPerson, only_friend=person_iter, _quantity=5, _bulk_create=True, ) # 2 bulk create and 1 select on Person assert models.LonelyPerson.objects.all().count() == 5 assert models.Person.objects.all().count() == 5 def test_bulk_create_multiple_fk(self): query_count = 6 with self.assertNumQueries(query_count): baker.make(models.PaymentBill, _quantity=5, _bulk_create=True) assert models.PaymentBill.objects.all().count() == 5 assert models.User.objects.all().count() == 5 def test_create_many_to_many_if_flagged(self): store = baker.make(models.Store, make_m2m=True) assert store.employees.count() == 5 assert store.customers.count() == 5 def test_regression_many_to_many_field_is_accepted_as_kwargs(self): employees = baker.make(models.Person, _quantity=3) customers = baker.make(models.Person, _quantity=3) store = baker.make(models.Store, employees=employees, customers=customers) assert store.employees.count() == 3 assert store.customers.count() == 3 assert models.Person.objects.count() == 6 def test_create_many_to_many_with_iter(self): students = baker.make(models.Person, _quantity=3) classrooms = baker.make(models.Classroom, _quantity=3, students=iter(students)) assert classrooms[0].students.count() == 1 assert classrooms[0].students.first() == students[0] assert classrooms[1].students.count() == 1 assert classrooms[1].students.first() == students[1] assert classrooms[2].students.count() == 1 assert classrooms[2].students.first() == students[2] def test_create_many_to_many_with_unsaved_iter(self): students = baker.prepare(models.Person, _quantity=3) classrooms = baker.make(models.Classroom, _quantity=3, students=iter(students)) assert students[0].pk is not None assert students[1].pk is not None assert students[2].pk is not None assert classrooms[0].students.count() == 1 assert classrooms[0].students.first() == students[0] assert classrooms[1].students.count() == 1 assert classrooms[1].students.first() == students[1] assert classrooms[2].students.count() == 1 assert classrooms[2].students.first() == students[2] def test_create_many_to_many_with_through_and_iter(self): students = baker.make(models.Person, _quantity=3) schools = baker.make( models.School, _quantity=3, students=iter(students), ) assert schools[0].students.count() == 1 assert schools[0].students.first() == students[0] assert schools[1].students.count() == 1 assert schools[1].students.first() == students[1] assert schools[2].students.count() == 1 assert schools[2].students.first() == students[2] def test_create_many_to_many_with_set_default_quantity(self): store = baker.make(models.Store, make_m2m=True) assert store.employees.count() == baker.MAX_MANY_QUANTITY assert store.customers.count() == baker.MAX_MANY_QUANTITY def test_create_many_to_many_with_through_option(self): # School student's attr is a m2m relationship with a model through school = baker.make(models.School, make_m2m=True) assert models.School.objects.count() == 1 assert school.students.count() == baker.MAX_MANY_QUANTITY assert models.SchoolEnrollment.objects.count() == baker.MAX_MANY_QUANTITY assert models.Person.objects.count() == baker.MAX_MANY_QUANTITY def test_does_not_create_many_to_many_as_default(self): store = baker.make(models.Store) assert store.employees.count() == 0 assert store.customers.count() == 0 def test_does_not_create_nullable_many_to_many_for_relations(self): classroom = baker.make(models.Classroom, make_m2m=False) assert classroom.students.count() == 0 def test_nullable_many_to_many_is_not_created_even_if_flagged(self): classroom = baker.make(models.Classroom, make_m2m=True) assert classroom.students.count() == 0 def test_m2m_changed_signal_is_fired(self): # TODO: Use object attrs instead of mocks for Django 1.4 compat self.m2m_changed_fired = False def test_m2m_changed(*args, **kwargs): self.m2m_changed_fired = True m2m_changed.connect(test_m2m_changed, dispatch_uid="test_m2m_changed") baker.make(models.Store, make_m2m=True) assert self.m2m_changed_fired def test_simple_creating_person_with_parameters(self): kid = baker.make(models.Person, enjoy_jards_macale=True, age=10, name="Mike") assert kid.age == 10 assert kid.enjoy_jards_macale is True assert kid.name == "Mike" def test_creating_person_from_factory_using_parameters(self): person_baker_ = baker.Baker(models.Person) person = person_baker_.make( enjoy_jards_macale=False, age=20, gender="M", name="John" ) assert person.age == 20 assert person.enjoy_jards_macale is False assert person.name == "John" assert person.gender == "M" def test_ForeignKey_model_field_population(self): dog = baker.make(models.Dog, breed="X1", owner__name="Bob") assert dog.breed == "X1" assert dog.owner.name == "Bob" def test_ForeignKey_model_field_population_should_work_with_prepare(self): dog = baker.prepare(models.Dog, breed="X1", owner__name="Bob") assert dog.breed == "X1" assert dog.owner.name == "Bob" def test_ForeignKey_model_field_population_for_not_required_fk(self): user = baker.make(models.User, profile__email="a@b.com") assert user.profile.email == "a@b.com" def test_does_not_creates_null_ForeignKey(self): user = baker.make(models.User) assert not user.profile def test_passing_m2m_value(self): store = baker.make(models.Store, customers=[baker.make(models.Person)]) assert store.customers.count() == 1 def test_ensure_recursive_ForeignKey_population(self): bill = baker.make(models.PaymentBill, user__profile__email="a@b.com") assert bill.user.profile.email == "a@b.com" def test_field_lookup_for_m2m_relationship(self): store = baker.make(models.Store, suppliers__gender="M") suppliers = store.suppliers.all() assert suppliers for supplier in suppliers: assert supplier.gender == "M" def test_field_lookup_for_one_to_one_relationship(self): lonely_person = baker.make(models.LonelyPerson, only_friend__name="Bob") assert lonely_person.only_friend.name == "Bob" def test_allow_create_fk_related_model(self): try: person = baker.make( models.Person, dog_set=[baker.make(models.Dog), baker.make(models.Dog)] ) except TypeError: raise AssertionError("type error raised") assert person.dog_set.count() == 2 def test_field_lookup_for_related_field(self): person = baker.make( models.Person, one_related__name="Foo", fk_related__name="Bar", ) assert person.pk assert person.one_related.pk assert person.fk_related.count() == 1 assert person.one_related.name == "Foo" assert person.fk_related.get().name == "Bar" def test_field_lookup_for_related_field_does_not_work_with_prepare(self): person = baker.prepare( models.Person, one_related__name="Foo", fk_related__name="Bar", ) assert not person.pk assert models.RelatedNamesModel.objects.count() == 0 def test_ensure_reverse_fk_for_many_to_one_is_working(self): """This is a regression test to make sure issue 291 is fixed.""" fk1, fk2 = baker.prepare( models.Issue291Model3, fk_model_2=None, name="custom name", _quantity=2 ) obj = baker.make( models.Issue291Model2, m2m_model_1=[baker.make(models.Issue291Model1)], bazs=[fk1, fk2], ) assert obj.bazs.count() == 2 related_1, related_2 = obj.bazs.all() assert related_1.name == "custom name" assert related_2.name == "custom name" @pytest.mark.django_db class TestHandlingUnsupportedModels: def test_unsupported_model_raises_an_explanatory_exception(self): try: baker.make(models.UnsupportedModel) raise AssertionError("Should have raised a TypeError") except TypeError as e: assert "not supported" in repr(e) assert "field unsupported_field" in repr(e) @pytest.mark.skipif( not BAKER_CONTENTTYPES, reason="Django contenttypes framework is not installed" ) @pytest.mark.django_db class TestHandlingModelsWithGenericRelationFields: def test_create_model_with_generic_relation(self): dummy = baker.make(models.DummyGenericRelationModel) assert isinstance(dummy, models.DummyGenericRelationModel) @pytest.mark.skipif( not BAKER_CONTENTTYPES, reason="Django contenttypes framework is not installed" ) @pytest.mark.django_db class TestHandlingContentTypeField: def test_create_model_with_contenttype_field(self): from django.contrib.contenttypes.models import ContentType dummy = baker.make(models.DummyGenericForeignKeyModel) assert isinstance(dummy, models.DummyGenericForeignKeyModel) assert isinstance(dummy.content_type, ContentType) def test_create_model_with_contenttype_with_content_object(self): """Test creating model with contenttype field and populating that field by function.""" from django.contrib.contenttypes.models import ContentType def get_dummy_key(): return baker.make("Person") dummy = baker.make( models.DummyGenericForeignKeyModel, content_object=get_dummy_key ) assert isinstance(dummy, models.DummyGenericForeignKeyModel) assert isinstance(dummy.content_type, ContentType) assert isinstance(dummy.content_object, models.Person) def test_create_model_with_contenttype_field_and_proxy_model(self): from django.contrib.contenttypes.models import ContentType class ProxyPerson(models.Person): class Meta: proxy = True app_label = "generic" dummy = baker.make( models.DummyGenericForeignKeyModel, content_object=baker.make(ProxyPerson, name="John Doe"), ) dummy.refresh_from_db() assert isinstance(dummy, models.DummyGenericForeignKeyModel) assert isinstance(dummy.content_type, ContentType) assert isinstance(dummy.content_object, ProxyPerson) assert dummy.content_object.name == "John Doe" @pytest.mark.skipif( not BAKER_CONTENTTYPES, reason="Django contenttypes framework is not installed" ) class TestHandlingContentTypeFieldNoQueries: def test_create_model_with_contenttype_field(self): from django.contrib.contenttypes.models import ContentType # Clear ContentType's internal cache so that it *will* try to connect to # the database to fetch the corresponding ContentType model for # a randomly chosen model. ContentType.objects.clear_cache() with pytest.warns( UserWarning, match="Database access disabled, returning ContentType raw instance", ): dummy = baker.prepare(models.DummyGenericForeignKeyModel) assert isinstance(dummy, models.DummyGenericForeignKeyModel) assert isinstance(dummy.content_type, ContentType) @pytest.mark.django_db class TestSkipNullsTestCase: def test_skip_null(self): dummy = baker.make(models.DummyNullFieldsModel) assert dummy.null_foreign_key is None assert dummy.null_integer_field is None @pytest.mark.django_db class TestFillNullsTestCase: def test_create_nullable_many_to_many_if_flagged_and_fill_field_optional(self): classroom = baker.make( models.Classroom, make_m2m=True, _fill_optional=["students"] ) assert classroom.students.count() == 5 def test_create_nullable_many_to_many_if_flagged_and_fill_optional(self): classroom = baker.make(models.Classroom, make_m2m=True, _fill_optional=True) assert classroom.students.count() == 5 def test_nullable_many_to_many_is_not_created_if_not_flagged_and_fill_optional( self, ): classroom = baker.make(models.Classroom, make_m2m=False, _fill_optional=True) assert classroom.students.count() == 0 @pytest.mark.django_db class TestSkipBlanksTestCase: def test_skip_blank(self): dummy = baker.make(models.DummyBlankFieldsModel) assert dummy.blank_char_field == "" assert dummy.blank_text_field == "" def test_skip_blank_with_argument(self): dummy = baker.make(models.DummyBlankFieldsModel, _fill_optional=False) assert dummy.blank_char_field == "" assert dummy.blank_text_field == "" def test_skip_blank_when_preparing(self): dummy = baker.prepare(models.DummyBlankFieldsModel) assert dummy.blank_char_field == "" assert dummy.blank_text_field == "" def test_skip_blank_when_preparing_with_argument(self): dummy = baker.prepare(models.DummyBlankFieldsModel, _fill_optional=False) assert dummy.blank_char_field == "" assert dummy.blank_text_field == "" @pytest.mark.django_db class TestFillBlanksTestCase: def test_fill_field_optional(self): dummy = baker.make( models.DummyBlankFieldsModel, _fill_optional=["blank_char_field"] ) assert len(dummy.blank_char_field) == 50 def test_fill_field_optional_when_preparing(self): dummy = baker.prepare( models.DummyBlankFieldsModel, _fill_optional=["blank_char_field"] ) assert len(dummy.blank_char_field) == 50 def test_fill_wrong_field(self): with pytest.raises(AttributeError) as exc_info: baker.make( models.DummyBlankFieldsModel, _fill_optional=["blank_char_field", "wrong"], ) msg = "_fill_optional field(s) ['wrong'] are not related to model DummyBlankFieldsModel" assert msg in str(exc_info.value) def test_fill_wrong_fields_with_parent(self): with pytest.raises(AttributeError): baker.make(models.SubclassOfAbstract, _fill_optional=["name", "wrong"]) def test_fill_many_optional(self): dummy = baker.make( models.DummyBlankFieldsModel, _fill_optional=["blank_char_field", "blank_text_field"], ) assert len(dummy.blank_text_field) == 300 def test_fill_all_optional(self): dummy = baker.make(models.DummyBlankFieldsModel, _fill_optional=True) assert len(dummy.blank_char_field) == 50 assert len(dummy.blank_text_field) == 300 def test_fill_all_optional_when_preparing(self): dummy = baker.prepare(models.DummyBlankFieldsModel, _fill_optional=True) assert len(dummy.blank_char_field) == 50 assert len(dummy.blank_text_field) == 300 def test_fill_optional_with_integer(self): with pytest.raises(TypeError): baker.make(models.DummyBlankFieldsModel, _fill_optional=1) def test_fill_optional_with_default(self): dummy = baker.make(models.DummyDefaultFieldsModel, _fill_optional=True) assert dummy.default_callable_int_field == 12 assert isinstance(dummy.default_callable_datetime_field, datetime.datetime) def test_fill_optional_with_default_unknown_class(self): dummy = baker.make(models.DummyDefaultFieldsModel, _fill_optional=True) assert dummy.default_unknown_class_field == 42 @pytest.mark.django_db class TestFillAutoFieldsTestCase: def test_fill_autofields_with_provided_value(self): baker.make(models.DummyEmptyModel, id=237) saved_dummy = models.DummyEmptyModel.objects.get() assert saved_dummy.id == 237 def test_keeps_prepare_autovalues(self): dummy = baker.prepare(models.DummyEmptyModel, id=543) assert dummy.id == 543 dummy.save() saved_dummy = models.DummyEmptyModel.objects.get() assert saved_dummy.id == 543 @pytest.mark.django_db class TestSkipDefaultsTestCase: def test_skip_fields_with_default(self): dummy = baker.make(models.DummyDefaultFieldsModel) assert dummy.default_char_field == "default" assert dummy.default_text_field == "default" assert dummy.default_int_field == 123 assert dummy.default_float_field == 123.0 assert dummy.default_date_field == "2012-01-01" assert dummy.default_date_time_field == tz_aware(datetime.datetime(2012, 1, 1)) assert dummy.default_time_field == "00:00:00" assert dummy.default_decimal_field == Decimal("0") assert dummy.default_email_field == "foo@bar.org" assert dummy.default_slug_field == "a-slug" assert dummy.default_unknown_class_field == 42 assert dummy.default_callable_int_field == 12 assert isinstance(dummy.default_callable_datetime_field, datetime.datetime) @pytest.mark.django_db class TestBakerHandlesModelWithNext: def test_creates_instance_for_model_with_next(self): instance = baker.make( models.BaseModelForNext, fk=baker.make(models.ModelWithNext), ) assert instance.id assert instance.fk.id assert instance.fk.attr assert instance.fk.next() == "foo" @pytest.mark.django_db class TestBakerHandlesModelWithList: def test_creates_instance_for_model_with_list(self): instance = baker.make(models.BaseModelForList, fk=["foo"]) assert instance.id assert instance.fk == ["foo"] @pytest.mark.django_db class TestBakerGeneratesIPAddresses: def test_create_model_with_valid_ips(self): form_data = { "ipv4_field": random_gen.gen_ipv4(), "ipv6_field": random_gen.gen_ipv6(), "ipv46_field": random_gen.gen_ipv46(), } assert DummyGenericIPAddressFieldForm(form_data).is_valid() class TestBakerAllowsSaveParameters(TestCase): databases = ["default", settings.EXTRA_DB] def test_allows_save_kwargs_on_baker_make(self): owner = baker.make(models.Person) dog = baker.make(models.ModelWithOverwrittenSave, _save_kwargs={"owner": owner}) assert owner == dog.owner dog1, dog2 = baker.make( models.ModelWithOverwrittenSave, _save_kwargs={"owner": owner}, _quantity=2 ) assert dog1.owner == owner assert dog2.owner == owner def test_allow_user_to_specify_database_via_save_kwargs_for_retro_compatibility( self, ): profile = baker.make(models.Profile, _save_kwargs={"using": settings.EXTRA_DB}) qs = models.Profile.objects.using(settings.EXTRA_DB).all() assert qs.count() == 1 assert profile in qs class TestBakerSupportsMultiDatabase(TestCase): databases = ["default", settings.EXTRA_DB] def test_allow_user_to_specify_database_via_using(self): profile = baker.make(models.Profile, _using=settings.EXTRA_DB) qs = models.Profile.objects.using(settings.EXTRA_DB).all() assert qs.count() == 1 assert profile in qs def test_related_fk_database_specified_via_using_kwarg(self): dog = baker.make(models.Dog, _using=settings.EXTRA_DB) dog_qs = models.Dog.objects.using(settings.EXTRA_DB).all() assert dog_qs.count() == 1 assert dog in dog_qs person_qs = models.Person.objects.using(settings.EXTRA_DB).all() assert person_qs.count() == 1 assert dog.owner in person_qs def test_allow_user_to_specify_database_via_using_combined_with_bulk_create( self, ): baker.make( models.Profile, _using=settings.EXTRA_DB, _quantity=10, _bulk_create=True ) qs = models.Profile.objects.using(settings.EXTRA_DB).all() assert qs.count() == 10 def test_related_fk_database_specified_via_using_kwarg_combined_with_quantity(self): dogs = baker.make(models.Dog, _using=settings.EXTRA_DB, _quantity=5) dog_qs = models.Dog.objects.using(settings.EXTRA_DB).all() person_qs = models.Person.objects.using(settings.EXTRA_DB).all() assert person_qs.count() == 5 assert dog_qs.count() == 5 for dog in dogs: assert dog in dog_qs assert dog.owner in person_qs def test_related_fk_database_specified_via_using_kwarg_combined_with_bulk_create( self, ): # A custom router must be used when using bulk create and saving # related objects in a multi-database setting. class AllowRelationRouter: """Custom router that allows saving of relations.""" def allow_relation(self, obj1, obj2, **hints): """Allow all relations.""" return True with override_settings(DATABASE_ROUTERS=[AllowRelationRouter()]): baker.make( models.PaymentBill, _quantity=5, _bulk_create=True, _using=settings.EXTRA_DB, ) assert models.PaymentBill.objects.all().using(settings.EXTRA_DB).count() == 5 assert models.User.objects.all().using(settings.EXTRA_DB).count() == 5 # Default router will not be able to save the related objects with pytest.raises(ValueError): baker.make( models.PaymentBill, _quantity=5, _bulk_create=True, _using=settings.EXTRA_DB, ) def test_allow_recipe_to_specify_database_via_using(self): dog = baker.make_recipe("generic.homeless_dog", _using=settings.EXTRA_DB) qs = models.Dog.objects.using(settings.EXTRA_DB).all() assert qs.count() == 1 assert dog in qs def test_recipe_related_fk_database_specified_via_using_kwarg(self): dog = baker.make_recipe("generic.dog", _using=settings.EXTRA_DB) dog_qs = models.Dog.objects.using(settings.EXTRA_DB).all() person_qs = models.Person.objects.using(settings.EXTRA_DB).all() dog.refresh_from_db() assert dog.owner assert dog_qs.count() == 1 assert dog in dog_qs assert person_qs.count() == 1 assert dog.owner in person_qs def test_recipe_related_fk_database_specified_via_using_kwarg_and_quantity(self): dogs = baker.make_recipe("generic.dog", _using=settings.EXTRA_DB, _quantity=5) dog_qs = models.Dog.objects.using(settings.EXTRA_DB).all() person_qs = models.Person.objects.using(settings.EXTRA_DB).all() assert dog_qs.count() == 5 # since we're using recipes, all dogs belong to the same owner assert person_qs.count() == 1 for dog in dogs: dog.refresh_from_db() assert dog in dog_qs assert dog.owner in person_qs def test_related_m2m_database_specified_via_using_kwarg(self): baker.make(models.Dog, _using=settings.EXTRA_DB, make_m2m=True) dog_qs = models.Dog.objects.using(settings.EXTRA_DB).all() assert dog_qs.count() == MAX_MANY_QUANTITY + 1 assert not models.Dog.objects.exists() def test_related_m2m_database_specified_via_using_kwarg_and_quantity(self): qtd = 3 baker.make(models.Dog, _using=settings.EXTRA_DB, make_m2m=True, _quantity=qtd) dog_qs = models.Dog.objects.using(settings.EXTRA_DB).all() assert dog_qs.count() == MAX_MANY_QUANTITY * qtd + qtd assert not models.Dog.objects.exists() def test_create_many_to_many_with_through_option_and_custom_db(self): # School student's attr is a m2m relationship with a model through school = baker.make(models.School, make_m2m=True, _using=settings.EXTRA_DB) assert models.School.objects.using(settings.EXTRA_DB).count() == 1 assert ( school.students.using(settings.EXTRA_DB).count() == baker.MAX_MANY_QUANTITY ) assert ( models.SchoolEnrollment.objects.using(settings.EXTRA_DB).count() == baker.MAX_MANY_QUANTITY ) assert ( models.Person.objects.using(settings.EXTRA_DB).count() == baker.MAX_MANY_QUANTITY ) def test_recipe_m2m_with_custom_db(self): school = baker.make_recipe( "generic.paulo_freire_school", _using=settings.EXTRA_DB, make_m2m=True ) school.refresh_from_db() assert school.pk assert school.name == "Escola Municipal Paulo Freire" assert models.School.objects.using(settings.EXTRA_DB).count() == 1 assert ( school.students.using(settings.EXTRA_DB).count() == baker.MAX_MANY_QUANTITY ) assert ( models.SchoolEnrollment.objects.using(settings.EXTRA_DB).count() == baker.MAX_MANY_QUANTITY ) assert ( models.Person.objects.using(settings.EXTRA_DB).count() == baker.MAX_MANY_QUANTITY ) assert not models.School.objects.exists() assert not models.SchoolEnrollment.objects.exists() assert not models.Person.objects.exists() @pytest.mark.django_db class TestBakerAutomaticallyRefreshFromDB: def test_refresh_from_db_if_true(self): person = baker.make( models.Person, birthday="2017-02-01", _refresh_after_create=True ) assert person.birthday == datetime.date(2017, 2, 1) def test_do_not_refresh_from_db_if_false(self): person = baker.make( models.Person, birthday="2017-02-01", _refresh_after_create=False ) assert person.birthday == "2017-02-01" assert person.birthday != datetime.date(2017, 2, 1) def test_do_not_refresh_from_db_by_default(self): person = baker.make(models.Person, birthday="2017-02-01") assert person.birthday == "2017-02-01" assert person.birthday != datetime.date(2017, 2, 1) @pytest.mark.django_db class TestBakerMakeCanFetchInstanceFromDefaultManager: def test_annotation_within_manager_get_queryset_are_run_on_make(self): """A custom model Manager can be used within make(). Passing ``_from_manager='objects'`` will force ``baker.make()`` to return an instance that has been going through a given Manager, thus calling its ``get_queryset()`` method and associated code, like default annotations. As such the instance will have the same fields as one created in the application. """ movie = baker.make(models.MovieWithAnnotation) with pytest.raises(AttributeError): assert movie.name movie = baker.make( models.MovieWithAnnotation, title="Old Boy", _from_manager="objects", ) assert movie.title == movie.name @pytest.mark.django_db class TestCreateM2MWhenBulkCreate(TestCase): def test_create(self): person = baker.make(models.Person) with self.assertNumQueries(11): baker.make( models.Classroom, students=[person], _quantity=10, _bulk_create=True ) c1, c2 = models.Classroom.objects.all()[:2] assert list(c1.students.all()) == list(c2.students.all()) == [person] def test_make_should_create_objects_using_reverse_name(self): classroom = baker.make(models.Classroom) with self.assertNumQueries(21): baker.make( models.Person, classroom_set=[classroom], _quantity=10, _bulk_create=True, ) s1, s2 = models.Person.objects.all()[:2] assert ( list(s1.classroom_set.all()) == list(s2.classroom_set.all()) == [classroom] ) class TestBakerSeeded: @pytest.fixture() def reset_seed(self): old_state = random_gen.baker_random.getstate() yield random_gen.baker_random.setstate(old_state) baker.Baker._global_seed = baker.Baker.SENTINEL @pytest.mark.django_db def test_seed(self, reset_seed): baker.seed(1) assert baker.Baker._global_seed == 1 assert random_gen.gen_integer() == 55195912693 @pytest.mark.django_db def test_unseeded(self): assert baker.Baker._global_seed is baker.Baker.SENTINEL class TestAutoNowFields: @pytest.mark.django_db @pytest.mark.parametrize("use_tz", [False, True]) def test_make_with_auto_now(self, use_tz, settings): settings.USE_TZ = use_tz tzinfo = datetime.timezone.utc if use_tz else None now = datetime.datetime(2023, 10, 20, 15, 30).replace(tzinfo=tzinfo) instance = baker.make( models.ModelWithAutoNowFields, created=now, updated=now, sent_date=now, ) assert instance.created == now assert instance.updated == now assert instance.sent_date == now # Should not update after refreshing from the db instance.refresh_from_db() assert instance.created == now assert instance.updated == now assert instance.sent_date == now @pytest.mark.django_db def test_make_with_auto_now_and_fill_optional(self): instance = baker.make( models.ModelWithAutoNowFields, _fill_optional=True, ) created, updated, sent_date = ( instance.created, instance.updated, instance.sent_date, ) # Should not update after refreshing from the db instance.refresh_from_db() assert instance.created == created assert instance.updated == updated assert instance.sent_date == sent_date model_bakery-1.20.4/tests/test_extending_bakery.py000066400000000000000000000072341475767230300223540ustar00rootroot00000000000000import pytest from model_bakery import baker from model_bakery.exceptions import CustomBakerNotFound, InvalidCustomBaker from model_bakery.random_gen import gen_from_list from tests.generic.models import Person def gen_opposite(default): return not default def gen_age(): # forever young return 18 class ExperientBaker(baker.Baker): age_list = range(40, 60) attr_mapping = {"age": gen_from_list(age_list)} class TeenagerBaker(baker.Baker): attr_mapping = {"age": gen_age} class SadPeopleBaker(baker.Baker): attr_mapping = { "enjoy_jards_macale": gen_opposite, "like_metal_music": gen_opposite, "name": gen_opposite, # Use a field without `default` } @pytest.mark.django_db class TestSimpleExtendBaker: def test_list_generator_respects_values_from_list(self): experient_bakers_factory = ExperientBaker(Person) experient_baker = experient_bakers_factory.make() assert experient_baker.age in ExperientBaker.age_list @pytest.mark.django_db class TestLessSimpleExtendBaker: def test_nonexistent_required_field(self): gen_opposite.required = ["house"] sad_people_factory = SadPeopleBaker(Person) with pytest.raises(AttributeError): sad_people_factory.make() def test_string_to_generator_required(self): gen_opposite.required = ["default"] enjoy_jards_macale_field = Person._meta.get_field("enjoy_jards_macale") like_metal_music_field = Person._meta.get_field("like_metal_music") sad_people_factory = SadPeopleBaker(Person) person = sad_people_factory.make() assert person.enjoy_jards_macale is enjoy_jards_macale_field.default assert person.like_metal_music is like_metal_music_field.default def test_kwarg_used_over_attr_mapping_generator(self): sad_people_factory = SadPeopleBaker(Person) person = sad_people_factory.make(name="test") assert person.name == "test" @pytest.mark.parametrize("value", [18, 18.5, [], {}, True]) def test_fail_pass_non_string_to_generator_required(self, value): teens_bakers_factory = TeenagerBaker(Person) gen_age.required = [value] with pytest.raises(ValueError): teens_bakers_factory.make() class ClassWithoutMake: def prepare(self): pass class ClassWithoutPrepare: def make(self): pass class BakerSubclass(baker.Baker): pass class BakerDuck: def __init__(*args, **kwargs): pass def make(self): pass def prepare(self): pass class TestCustomizeBakerClassViaSettings: def class_to_import_string(self, class_to_convert): return f"{self.__module__}.{class_to_convert.__name__}" def test_create_vanilla_baker_used_by_default(self): baker_instance = baker.Baker.create(Person) assert baker_instance.__class__ == baker.Baker def test_create_fail_on_custom_baker_load_error(self, settings): settings.BAKER_CUSTOM_CLASS = "invalid_module.invalid_class" with pytest.raises(CustomBakerNotFound): baker.Baker.create(Person) @pytest.mark.parametrize("cls", [ClassWithoutMake, ClassWithoutPrepare]) def test_create_fail_on_missing_required_functions(self, settings, cls): settings.BAKER_CUSTOM_CLASS = self.class_to_import_string(cls) with pytest.raises(InvalidCustomBaker): baker.Baker.create(Person) @pytest.mark.parametrize("cls", [BakerSubclass, BakerDuck]) def test_create_succeeds_with_valid_custom_baker(self, settings, cls): settings.BAKER_CUSTOM_CLASS = self.class_to_import_string(cls) assert baker.Baker.create(Person).__class__ == cls model_bakery-1.20.4/tests/test_filling_fields.py000066400000000000000000000660001475767230300220000ustar00rootroot00000000000000import uuid from datetime import date, datetime, time, timedelta from decimal import Decimal from os.path import abspath from tempfile import gettempdir import django from django.conf import settings from django.core.validators import ( validate_ipv4_address, validate_ipv6_address, validate_ipv46_address, ) from django.db import connection from django.db.models import FileField, ImageField, fields import pytest from model_bakery import baker from model_bakery.content_types import BAKER_CONTENTTYPES from model_bakery.gis import BAKER_GIS from model_bakery.random_gen import MAX_LENGTH, gen_related from tests.generic import generators, models try: from django.db.models import JSONField except ImportError: JSONField = None try: from django.contrib.postgres.fields import ( ArrayField, CICharField, CIEmailField, CITextField, HStoreField, JSONField as PostgresJSONField, ) from django.contrib.postgres.fields.ranges import ( BigIntegerRangeField, DateRangeField, DateTimeRangeField, DecimalRangeField, IntegerRangeField, ) except ImportError: ArrayField = None PostgresJSONField = None HStoreField = None CICharField = None CIEmailField = None CITextField = None IntegerRangeField = None BigIntegerRangeField = None DateRangeField = None DateTimeRangeField = None DecimalRangeField = None @pytest.fixture def person(db): return baker.make("generic.Person") @pytest.fixture() def custom_cfg(): yield None if hasattr(settings, "BAKER_CUSTOM_FIELDS_GEN"): delattr(settings, "BAKER_CUSTOM_FIELDS_GEN") baker.generators.add("tests.generic.fields.CustomFieldWithGenerator", None) baker.generators.add("django.db.models.fields.CharField", None) class TestFillingFromChoice: def test_if_gender_is_populated_from_choices(self, person): from tests.generic.models import GENDER_CHOICES assert person.gender in (x[0] for x in GENDER_CHOICES) def test_if_occupation_populated_from_choices(self, person): from tests.generic.models import OCCUPATION_CHOICES occupations = [item[0] for lst in OCCUPATION_CHOICES for item in lst[1]] assert person.occupation in occupations class TestStringFieldsFilling: def test_fill_CharField_with_a_random_str(self, person): person_name_field = models.Person._meta.get_field("name") assert isinstance(person_name_field, fields.CharField) assert isinstance(person.name, str) assert len(person.name) == person_name_field.max_length def test_fill_SlugField_with_a_random_str(self, person): person_nickname_field = models.Person._meta.get_field("nickname") assert isinstance(person_nickname_field, fields.SlugField) assert isinstance(person.nickname, str) assert len(person.nickname) == person_nickname_field.max_length def test_fill_TextField_with_a_random_str(self, person): person_bio_field = models.Person._meta.get_field("bio") assert isinstance(person_bio_field, fields.TextField) assert isinstance(person.bio, str) assert len(person.bio) == MAX_LENGTH def test_fill_TextField_with_max_length_str(self, person): person_bio_summary_field = models.Person._meta.get_field("bio_summary") assert isinstance(person_bio_summary_field, fields.TextField) assert isinstance(person.bio_summary, str) assert len(person.bio_summary) == person_bio_summary_field.max_length class TestBinaryFieldsFilling: def test_fill_BinaryField_with_random_binary(self, person): name_hash_field = models.Person._meta.get_field("name_hash") assert isinstance(name_hash_field, fields.BinaryField) name_hash = person.name_hash assert isinstance(name_hash, bytes) assert len(name_hash) == name_hash_field.max_length class TestsDurationFieldsFilling: def test_fill_DurationField_with_random_interval_in_miliseconds(self, person): duration_of_sleep_field = models.Person._meta.get_field("duration_of_sleep") assert isinstance(duration_of_sleep_field, fields.DurationField) duration_of_sleep = person.duration_of_sleep assert isinstance(duration_of_sleep, timedelta) class TestBooleanFieldsFilling: def test_fill_BooleanField_with_boolean(self, person): enjoy_jards_macale_field = models.Person._meta.get_field("enjoy_jards_macale") assert isinstance(enjoy_jards_macale_field, fields.BooleanField) assert isinstance(person.enjoy_jards_macale, bool) assert person.enjoy_jards_macale is True def test_fill_BooleanField_with_false_if_default_is_false(self, person): like_metal_music_field = models.Person._meta.get_field("like_metal_music") assert isinstance(like_metal_music_field, fields.BooleanField) assert isinstance(person.like_metal_music, bool) assert person.like_metal_music is False class TestDateFieldsFilling: def test_fill_DateField_with_a_date(self, person): birthday_field = models.Person._meta.get_field("birthday") assert isinstance(birthday_field, fields.DateField) assert isinstance(person.birthday, date) class TestDateTimeFieldsFilling: def test_fill_DateTimeField_with_a_datetime(self, person): appointment_field = models.Person._meta.get_field("appointment") assert isinstance(appointment_field, fields.DateTimeField) assert isinstance(person.appointment, datetime) class TestTimeFieldsFilling: def test_fill_TimeField_with_a_time(self, person): birth_time_field = models.Person._meta.get_field("birth_time") assert isinstance(birth_time_field, fields.TimeField) assert isinstance(person.birth_time, time) class TestUUIDFieldsFilling: def test_fill_UUIDField_with_uuid_object(self, person): uuid_field = models.Person._meta.get_field("uuid") assert isinstance(uuid_field, fields.UUIDField) assert isinstance(person.uuid, uuid.UUID) @pytest.mark.skipif(JSONField is None, reason="JSONField could not be imported") class TestJSONFieldsFilling: def test_fill_JSONField_with_uuid_object(self, person): json_field = models.Person._meta.get_field("data") assert isinstance(json_field, JSONField) assert isinstance(person.data, dict) @pytest.mark.django_db class TestFillingIntFields: def test_fill_IntegerField_with_a_random_number(self): dummy_int_model = baker.make(models.DummyIntModel) int_field = models.DummyIntModel._meta.get_field("int_field") assert isinstance(int_field, fields.IntegerField) assert isinstance(dummy_int_model.int_field, int) def test_fill_BigIntegerField_with_a_random_number(self): dummy_int_model = baker.make(models.DummyIntModel) big_int_field = models.DummyIntModel._meta.get_field("big_int_field") assert isinstance(big_int_field, fields.BigIntegerField) assert isinstance(dummy_int_model.big_int_field, int) def test_fill_SmallIntegerField_with_a_random_number(self): dummy_int_model = baker.make(models.DummyIntModel) small_int_field = models.DummyIntModel._meta.get_field("small_int_field") assert isinstance(small_int_field, fields.SmallIntegerField) assert isinstance(dummy_int_model.small_int_field, int) @pytest.mark.skipif( django.VERSION < (5, 0), reason="The db_default field attribute was added after 5.0", ) def test_respects_db_default(self): person = baker.make(models.Person, age=10) assert person.age == 10 assert person.retirement_age == 20 @pytest.mark.django_db class TestFillingPositiveIntFields: def test_fill_PositiveSmallIntegerField_with_a_random_number(self): dummy_positive_int_model = baker.make(models.DummyPositiveIntModel) field = models.DummyPositiveIntModel._meta.get_field("positive_small_int_field") positive_small_int_field = field assert isinstance(positive_small_int_field, fields.PositiveSmallIntegerField) assert isinstance(dummy_positive_int_model.positive_small_int_field, int) assert dummy_positive_int_model.positive_small_int_field > 0 def test_fill_PositiveIntegerField_with_a_random_number(self): dummy_positive_int_model = baker.make(models.DummyPositiveIntModel) positive_int_field = models.DummyPositiveIntModel._meta.get_field( "positive_int_field" ) assert isinstance(positive_int_field, fields.PositiveIntegerField) assert isinstance(dummy_positive_int_model.positive_int_field, int) assert dummy_positive_int_model.positive_int_field > 0 def test_fill_PositiveBigIntegerField_with_a_random_number(self): dummy_positive_int_model = baker.make(models.DummyPositiveIntModel) positive_big_int_field = models.DummyPositiveIntModel._meta.get_field( "positive_big_int_field" ) assert isinstance(positive_big_int_field, fields.PositiveBigIntegerField) assert isinstance(dummy_positive_int_model.positive_big_int_field, int) assert dummy_positive_int_model.positive_big_int_field > 0 @pytest.mark.django_db class TestFillingOthersNumericFields: def test_filling_FloatField_with_a_random_float(self): self.dummy_numbers_model = baker.make(models.DummyNumbersModel) float_field = models.DummyNumbersModel._meta.get_field("float_field") assert isinstance(float_field, fields.FloatField) assert isinstance(self.dummy_numbers_model.float_field, float) def test_filling_DecimalField_with_random_decimal(self): self.dummy_decimal_model = baker.make(models.DummyDecimalModel) decimal_field = models.DummyDecimalModel._meta.get_field("decimal_field") assert isinstance(decimal_field, fields.DecimalField) assert isinstance(self.dummy_decimal_model.decimal_field, Decimal) class TestURLFieldsFilling: def test_fill_URLField_with_valid_url(self, person): blog_field = models.Person._meta.get_field("blog") assert isinstance(blog_field, fields.URLField) assert isinstance(person.blog, str) class TestFillingEmailField: def test_filling_EmailField(self, person): field = models.Person._meta.get_field("email") assert isinstance(field, fields.EmailField) assert isinstance(person.email, str) @pytest.mark.django_db class TestFillingIPAddressField: def test_filling_IPAddressField(self): obj = baker.make(models.DummyGenericIPAddressFieldModel) field = models.DummyGenericIPAddressFieldModel._meta.get_field("ipv4_field") assert isinstance(field, fields.GenericIPAddressField) assert isinstance(obj.ipv4_field, str) validate_ipv4_address(obj.ipv4_field) if hasattr(obj, "ipv6_field"): assert isinstance(obj.ipv6_field, str) assert isinstance(obj.ipv46_field, str) validate_ipv6_address(obj.ipv6_field) validate_ipv46_address(obj.ipv46_field) # skipif @pytest.mark.skipif( not BAKER_CONTENTTYPES, reason="Django contenttypes framework is not installed" ) @pytest.mark.django_db class TestFillingGenericForeignKeyField: def test_content_type_field(self): from django.contrib.contenttypes.models import ContentType dummy = baker.make(models.DummyGenericForeignKeyModel) assert isinstance(dummy.content_type, ContentType) assert dummy.content_type.model_class() is not None def test_with_content_object(self): from django.contrib.contenttypes.models import ContentType profile = baker.make(models.Profile) dummy = baker.make( models.DummyGenericForeignKeyModel, content_object=profile, ) assert dummy.content_object == profile assert dummy.content_type == ContentType.objects.get_for_model(models.Profile) assert dummy.object_id == profile.pk def test_with_content_object_none(self): dummy = baker.make( models.DummyGenericForeignKeyModel, content_object=None, ) assert dummy.content_object is None def test_with_prepare(self): from django.contrib.contenttypes.models import ContentType profile = baker.prepare(models.Profile, id=1) dummy = baker.prepare( models.DummyGenericForeignKeyModel, content_object=profile, ) assert dummy.content_object == profile assert dummy.content_type == ContentType.objects.get_for_model(models.Profile) assert dummy.object_id == profile.pk == 1 def test_with_iter(self): """ Ensures private_fields are included in ``Baker.get_fields()``. Otherwise, calling ``next()`` when a GFK is in ``iterator_attrs`` would be bypassed. """ from django.contrib.contenttypes.models import ContentType objects = baker.make(models.Profile, _quantity=2) dummies = baker.make( models.DummyGenericForeignKeyModel, content_object=iter(objects), _quantity=2, ) expected_content_type = ContentType.objects.get_for_model(models.Profile) assert dummies[0].content_object == objects[0] assert dummies[0].content_type == expected_content_type assert dummies[0].object_id == objects[0].pk assert dummies[1].content_object == objects[1] assert dummies[1].content_type == expected_content_type assert dummies[1].object_id == objects[1].pk def test_with_none_in_iter(self): from django.contrib.contenttypes.models import ContentType profile = baker.make(models.Profile) dummies = baker.make( models.DummyGenericForeignKeyModel, content_object=iter((None, profile)), _quantity=2, ) expected_content_type = ContentType.objects.get_for_model(models.Profile) assert dummies[0].content_object is None assert dummies[1].content_object == profile assert dummies[1].content_type == expected_content_type assert dummies[1].object_id == profile.pk def test_with_fill_optional(self): from django.contrib.contenttypes.models import ContentType dummy = baker.make(models.DummyGenericForeignKeyModel, _fill_optional=True) assert isinstance(dummy.content_type, ContentType) assert dummy.content_type.model_class() is not None def test_with_fill_optional_but_content_object_none(self): dummy = baker.make( models.GenericForeignKeyModelWithOptionalData, content_object=None, _fill_optional=True, ) assert dummy.content_object is None assert dummy.content_type is None assert dummy.object_id is None @pytest.mark.django_db class TestFillingForeignKeyFieldWithDefaultFunctionReturningId: def test_filling_foreignkey_with_default_id(self): dummy = baker.make(models.RelatedNamesWithDefaultsModel) assert dummy.cake.__class__.objects.count() == 1 assert dummy.cake.id == models.get_default_cake_id() assert dummy.cake.name == "Muffin" def test_filling_foreignkey_with_default_id_with_custom_arguments(self): dummy = baker.make( models.RelatedNamesWithDefaultsModel, cake__name="Baumkuchen" ) assert dummy.cake.__class__.objects.count() == 1 assert dummy.cake.id == dummy.cake.__class__.objects.get().id assert dummy.cake.name == "Baumkuchen" @pytest.mark.django_db class TestFillingOptionalForeignKeyField: def test_not_filling_optional_foreignkey(self): dummy = baker.make(models.RelatedNamesWithEmptyDefaultsModel) assert dummy.cake is None def test_filling_optional_foreignkey_implicitly(self): dummy = baker.make( models.RelatedNamesWithEmptyDefaultsModel, cake__name="Carrot cake" ) assert dummy.cake.__class__.objects.count() == 1 assert dummy.cake.name == "Carrot cake" @pytest.mark.django_db class TestsFillingFileField: def test_filling_file_field(self): dummy = baker.make(models.DummyFileFieldModel, _create_files=True) field = models.DummyFileFieldModel._meta.get_field("file_field") assert isinstance(field, FileField) import time path = f"{gettempdir()}/{time.strftime('%Y/%m/%d')}/mock_file.txt" assert abspath(path) == abspath(dummy.file_field.path) dummy.file_field.delete() dummy.delete() def test_does_not_create_file_if_not_flagged(self): dummy = baker.make(models.DummyFileFieldModel) with pytest.raises(ValueError): # Django raises ValueError if file does not exist assert dummy.file_field.path def test_filling_nested_file_fields(self): dummy = baker.make(models.NestedFileFieldModel, _create_files=True) assert dummy.attached_file.file_field.path dummy.attached_file.file_field.delete() dummy.delete() def test_does_not_fill_nested_file_fields_unflagged(self): dummy = baker.make(models.NestedFileFieldModel) with pytest.raises(ValueError): assert dummy.attached_file.file_field.path dummy.delete() @pytest.mark.django_db class TestFillingCustomFields: def test_raises_unsupported_field_for_custom_field(self, custom_cfg): """Should raise an exception if a generator is not provided for a custom field.""" with pytest.raises(TypeError): baker.make(models.CustomFieldWithoutGeneratorModel) def test_uses_generator_defined_on_settings_for_custom_field(self, custom_cfg): """Should use the function defined in settings as a generator.""" generator_dict = { "tests.generic.fields.CustomFieldWithGenerator": generators.gen_value_string } settings.BAKER_CUSTOM_FIELDS_GEN = generator_dict obj = baker.make(models.CustomFieldWithGeneratorModel) assert obj.custom_value == "value" def test_uses_generator_defined_as_string_on_settings_for_custom_field( self, custom_cfg ): """Should import and use the function present in the import path defined in settings.""" # fmt: off generator_dict = { "tests.generic.fields.CustomFieldWithGenerator": "tests.generic.generators.gen_value_string" } # fmt: on settings.BAKER_CUSTOM_FIELDS_GEN = generator_dict obj = baker.make(models.CustomFieldWithGeneratorModel) assert obj.custom_value == "value" def test_uses_generator_defined_on_settings_for_custom_foreignkey(self, custom_cfg): """Should use the function defined in the import path for a foreign key field.""" generator_dict = { "tests.generic.fields.CustomForeignKey": "model_bakery.random_gen.gen_related" } settings.BAKER_CUSTOM_FIELDS_GEN = generator_dict obj = baker.make( models.CustomForeignKeyWithGeneratorModel, custom_fk__email="a@b.com" ) assert obj.custom_fk.email == "a@b.com" def test_uses_generator_defined_as_string_for_custom_field(self, custom_cfg): """Should import and use the generator function used in the add method.""" baker.generators.add( "tests.generic.fields.CustomFieldWithGenerator", "tests.generic.generators.gen_value_string", ) obj = baker.make(models.CustomFieldWithGeneratorModel) assert obj.custom_value == "value" def test_uses_generator_function_for_custom_foreignkey(self, custom_cfg): """Should use the generator function passed as a value for the add method.""" baker.generators.add("tests.generic.fields.CustomForeignKey", gen_related) obj = baker.make( models.CustomForeignKeyWithGeneratorModel, custom_fk__email="a@b.com" ) assert obj.custom_fk.email == "a@b.com" def test_can_override_django_default_field_functions_generator(self, custom_cfg): def gen_char(): return "Some value" baker.generators.add("django.db.models.fields.CharField", gen_char) person = baker.make(models.Person) assert person.name == "Some value" def test_ensure_adding_generators_via_settings_works(self): obj = baker.make(models.CustomFieldViaSettingsModel) assert obj.custom_value == "always the same text" @pytest.mark.django_db class TestFillingAutoFields: def test_filling_AutoField(self): obj = baker.make(models.DummyEmptyModel) field = models.DummyEmptyModel._meta.get_field("id") assert isinstance(field, fields.AutoField) assert isinstance(obj.id, int) @pytest.mark.django_db @pytest.mark.skipif(not models.has_pil, reason="PIL is required to test ImageField") class TestFillingImageFileField: def test_filling_image_file_field(self): dummy = baker.make(models.DummyImageFieldModel, _create_files=True) field = models.DummyImageFieldModel._meta.get_field("image_field") assert isinstance(field, ImageField) import time path = f"{gettempdir()}/{time.strftime('%Y/%m/%d')}/mock_img.jpeg" # These require the file to exist in earlier versions of Django assert abspath(path) == abspath(dummy.image_field.path) assert dummy.image_field.width assert dummy.image_field.height dummy.image_field.delete() def test_does_not_create_file_if_not_flagged(self): dummy = baker.make(models.DummyImageFieldModel) with pytest.raises(ValueError): # Django raises ValueError if image does not exist assert dummy.image_field.path @pytest.mark.skipif( connection.vendor != "postgresql", reason="PostgreSQL specific tests" ) class TestCIStringFieldsFilling: def test_fill_cicharfield_with_a_random_str(self, person): ci_char_field = models.Person._meta.get_field("ci_char") assert isinstance(ci_char_field, CICharField) assert isinstance(person.ci_char, str) assert len(person.ci_char) == ci_char_field.max_length def test_filling_ciemailfield(self, person): ci_email_field = models.Person._meta.get_field("ci_email") assert isinstance(ci_email_field, CIEmailField) assert isinstance(person.ci_email, str) def test_filling_citextfield(self, person): ci_text_field = models.Person._meta.get_field("ci_text") assert isinstance(ci_text_field, CITextField) assert isinstance(person.ci_text, str) assert len(person.ci_text) == MAX_LENGTH def test_filling_citextfield_with_max_length(self, person): ci_text_max_length_field = models.Person._meta.get_field("ci_text_max_length") assert isinstance(ci_text_max_length_field, CITextField) assert isinstance(person.ci_text_max_length, str) assert len(person.ci_text_max_length) == ci_text_max_length_field.max_length def test_filling_decimal_range_field(self, person): try: from psycopg.types.range import Range except ImportError: from psycopg2._range import NumericRange as Range decimal_range_field = models.Person._meta.get_field("decimal_range") assert isinstance(decimal_range_field, DecimalRangeField) assert isinstance(person.decimal_range, Range) assert isinstance(person.decimal_range.lower, Decimal) assert isinstance(person.decimal_range.upper, Decimal) assert person.decimal_range.lower < person.decimal_range.upper def test_filling_integer_range_field(self, person): try: from psycopg.types.range import Range except ImportError: from psycopg2._range import NumericRange as Range int_range_field = models.Person._meta.get_field("int_range") assert isinstance(int_range_field, IntegerRangeField) assert isinstance(person.int_range, Range) assert isinstance(person.int_range.lower, int) assert isinstance(person.int_range.upper, int) assert person.int_range.lower < person.int_range.upper def test_filling_integer_range_field_for_big_int(self, person): try: from psycopg.types.range import Range except ImportError: from psycopg2._range import NumericRange as Range bigint_range_field = models.Person._meta.get_field("bigint_range") assert isinstance(bigint_range_field, BigIntegerRangeField) assert isinstance(person.bigint_range, Range) assert isinstance(person.bigint_range.lower, int) assert isinstance(person.bigint_range.upper, int) assert person.bigint_range.lower < person.bigint_range.upper def test_filling_date_range_field(self, person): try: from psycopg.types.range import DateRange except ImportError: from psycopg2.extras import DateRange date_range_field = models.Person._meta.get_field("date_range") assert isinstance(date_range_field, DateRangeField) assert isinstance(person.date_range, DateRange) assert isinstance(person.date_range.lower, date) assert isinstance(person.date_range.upper, date) assert person.date_range.lower < person.date_range.upper def test_filling_datetime_range_field(self, person): try: from psycopg.types.range import TimestamptzRange except ImportError: from psycopg2.extras import DateTimeTZRange as TimestamptzRange datetime_range_field = models.Person._meta.get_field("datetime_range") assert isinstance(datetime_range_field, DateTimeRangeField) assert isinstance(person.datetime_range, TimestamptzRange) assert isinstance(person.datetime_range.lower, datetime) assert isinstance(person.datetime_range.upper, datetime) assert person.datetime_range.lower < person.datetime_range.upper @pytest.mark.skipif( connection.vendor != "postgresql", reason="PostgreSQL specific tests" ) class TestPostgreSQLFieldsFilling: def test_fill_arrayfield_with_empty_array(self, person): assert person.acquaintances == [] def test_fill_hstorefield_with_empty_dict(self, person): assert person.hstore_data == {} @pytest.mark.skipif(not BAKER_GIS, reason="GIS support required for GIS fields") class TestGisFieldsFilling: def assertGeomValid(self, geom): assert geom.valid is True, geom.valid_reason def test_fill_PointField_valid(self, person): self.assertGeomValid(person.point) def test_fill_LineStringField_valid(self, person): self.assertGeomValid(person.line_string) def test_fill_PolygonField_valid(self, person): self.assertGeomValid(person.polygon) def test_fill_MultiPointField_valid(self, person): self.assertGeomValid(person.multi_point) def test_fill_MultiLineStringField_valid(self, person): self.assertGeomValid(person.multi_line_string) def test_fill_MultiPolygonField_valid(self, person): self.assertGeomValid(person.multi_polygon) def test_fill_GeometryField_valid(self, person): self.assertGeomValid(person.geom) def test_fill_GeometryCollectionField_valid(self, person): self.assertGeomValid(person.geom_collection) model_bakery-1.20.4/tests/test_recipes.py000066400000000000000000000670741475767230300204740ustar00rootroot00000000000000import itertools from datetime import timedelta from decimal import Decimal from random import choice # noqa from unittest.mock import patch from django.db import connection from django.utils.timezone import now import pytest from model_bakery import baker from model_bakery.exceptions import InvalidQuantityException, RecipeIteratorEmpty from model_bakery.recipe import Recipe, RecipeForeignKey, foreign_key, seq from model_bakery.timezone import tz_aware from tests.generic.baker_recipes import SmallDogRecipe, pug from tests.generic.models import ( TEST_TIME, Dog, DummyBlankFieldsModel, DummyNumbersModel, LonelyPerson, ModelWithAutoNowFields, Person, Profile, User, ) recipe_attrs = { "name": "John Doe", "nickname": "joe", "age": 18, "bio": "Someone in the crowd", "birthday": now().date(), "appointment": now(), "blog": "https://joe.example.com", "days_since_last_login": 4, "birth_time": now(), "data": {"one": 1}, } person_recipe = Recipe(Person, **recipe_attrs) user_recipe = Recipe(User) lonely_person_recipe = Recipe(LonelyPerson) def test_import_seq_from_recipe(): """Import seq method directly from recipe module.""" try: from model_bakery.recipe import seq # NoQA except ImportError: pytest.fail(f"{ImportError.__name__} raised") def test_import_recipes(): """Test imports works both for full import paths and for `app_name.recipe_name` strings.""" assert baker.prepare_recipe("generic.dog"), baker.prepare_recipe( "tests.generic.dog" ) @pytest.mark.django_db class TestDefiningRecipes: def test_flat_model_make_recipe_with_the_correct_attributes(self): """Test a 'flat model' - without associations, like FK, M2M and O2O.""" person = person_recipe.make() assert person.name == recipe_attrs["name"] assert person.nickname == recipe_attrs["nickname"] assert person.age == recipe_attrs["age"] assert person.bio == recipe_attrs["bio"] assert person.birthday == recipe_attrs["birthday"] assert person.appointment == recipe_attrs["appointment"] assert person.blog == recipe_attrs["blog"] assert person.days_since_last_login == recipe_attrs["days_since_last_login"] assert person.data is not recipe_attrs["data"] assert person.data == recipe_attrs["data"] assert person.id is not None def test_flat_model_prepare_recipe_with_the_correct_attributes(self): person = person_recipe.prepare() assert person.name == recipe_attrs["name"] assert person.nickname == recipe_attrs["nickname"] assert person.age == recipe_attrs["age"] assert person.bio == recipe_attrs["bio"] assert person.birthday == recipe_attrs["birthday"] assert person.appointment == recipe_attrs["appointment"] assert person.blog == recipe_attrs["blog"] assert person.days_since_last_login == recipe_attrs["days_since_last_login"] assert person.data is not recipe_attrs["data"] assert person.data == recipe_attrs["data"] assert person.id is None def test_accepts_callable(self): r = Recipe(DummyBlankFieldsModel, blank_char_field=lambda: "callable!!") value = r.make().blank_char_field assert value == "callable!!" def test_always_calls_when_creating(self): with patch("tests.test_recipes.choice") as choice_mock: choice_mock.return_value = "foo" lst = ["foo", "bar", "spam", "eggs"] r = Recipe(DummyBlankFieldsModel, blank_char_field=lambda: choice_mock(lst)) assert r.make().blank_char_field assert r.make().blank_char_field assert choice_mock.call_count == 2 def test_always_calls_with_quantity(self): with patch("tests.test_recipes.choice") as choice_mock: choice_mock.return_value = "foo" lst = ["foo", "bar", "spam", "eggs"] r = Recipe(DummyBlankFieldsModel, blank_char_field=lambda: choice_mock(lst)) r.make(_quantity=3) assert choice_mock.call_count == 3 def test_make_recipes_with_args(self): """Overriding some fields values at recipe execution.""" person = person_recipe.make(name="Guido", age=56) assert person.name != recipe_attrs["name"] assert person.name == "Guido" assert person.age != recipe_attrs["age"] assert person.age == 56 assert person.nickname == recipe_attrs["nickname"] assert person.bio == recipe_attrs["bio"] assert person.birthday == recipe_attrs["birthday"] assert person.appointment == recipe_attrs["appointment"] assert person.blog == recipe_attrs["blog"] assert person.days_since_last_login == recipe_attrs["days_since_last_login"] assert person.id is not None def test_prepare_recipes_with_args(self): """Overriding some fields values at recipe execution.""" person = person_recipe.prepare(name="Guido", age=56) assert person.name != recipe_attrs["name"] assert person.name == "Guido" assert person.age != recipe_attrs["age"] assert person.age == 56 assert person.nickname == recipe_attrs["nickname"] assert person.bio == recipe_attrs["bio"] assert person.birthday == recipe_attrs["birthday"] assert person.appointment == recipe_attrs["appointment"] assert person.blog == recipe_attrs["blog"] assert person.days_since_last_login == recipe_attrs["days_since_last_login"] assert person.id is None def test_make_recipe_without_all_model_needed_data(self): person_recipe = Recipe(Person, name="John Doe") person = person_recipe.make() assert person.name == "John Doe" assert person.nickname assert person.age assert person.bio assert person.birthday assert person.appointment assert person.blog assert person.days_since_last_login assert person.id def test_prepare_recipe_without_all_model_needed_data(self): person_recipe = Recipe(Person, name="John Doe") person = person_recipe.prepare() assert person.name == "John Doe" assert person.nickname assert person.age assert person.bio assert person.birthday assert person.appointment assert person.blog assert person.days_since_last_login assert not person.id def test_defining_recipes_str(self): p = Recipe("generic.Person", name=seq("foo")) try: p.make(_quantity=5) except AttributeError as e: pytest.fail(f"{e}") def test_recipe_dict_attribute_isolation(self): person1 = person_recipe.make() person2 = person_recipe.make() person2.data["two"] = 2 person3 = person_recipe.make() # Mutation on instances must have no side effect on their recipe definition, # or on other instances of the same recipe. assert person1.data == {"one": 1} assert person2.data == {"one": 1, "two": 2} assert person3.data == {"one": 1} @pytest.mark.skipif( connection.vendor != "postgresql", reason="PostgreSQL specific tests" ) def test_recipe_list_attribute_isolation(self): pg_person_recipe = person_recipe.extend(acquaintances=[1, 2, 3]) person1 = pg_person_recipe.make() person2 = pg_person_recipe.make() person2.acquaintances.append(4) person3 = pg_person_recipe.make() # Mutation on instances must have no side effect on their recipe definition, # or on other instances of the same recipe. assert person1.acquaintances == [1, 2, 3] assert person2.acquaintances == [1, 2, 3, 4] assert person3.acquaintances == [1, 2, 3] @pytest.mark.django_db class TestExecutingRecipes: """Tests for calling recipes defined in baker_recipes.py.""" def test_model_with_foreign_key(self): dog = baker.make_recipe("tests.generic.dog") assert dog.breed == "Pug" assert isinstance(dog.owner, Person) assert dog.owner.id dog = baker.prepare_recipe("tests.generic.dog") assert dog.breed == "Pug" assert isinstance(dog.owner, Person) assert dog.owner.id is None dog = baker.prepare_recipe("tests.generic.dog", _save_related=True) assert dog.breed == "Pug" assert isinstance(dog.owner, Person) assert dog.owner.id dogs = baker.make_recipe("tests.generic.dog", _quantity=2) owner = dogs[0].owner for dog in dogs: assert dog.breed == "Pug" assert dog.owner == owner def test_model_with_foreign_key_as_str(self): dog = baker.make_recipe("tests.generic.other_dog") assert dog.breed == "Basset" assert isinstance(dog.owner, Person) assert dog.owner.id dog = baker.prepare_recipe("tests.generic.other_dog") assert dog.breed == "Basset" assert isinstance(dog.owner, Person) assert dog.owner.id is None def test_model_with_foreign_key_as_unicode(self): dog = baker.make_recipe("tests.generic.other_dog_unicode") assert dog.breed == "Basset" assert isinstance(dog.owner, Person) assert dog.owner.id dog = baker.prepare_recipe("tests.generic.other_dog_unicode") assert dog.breed == "Basset" assert isinstance(dog.owner, Person) assert dog.owner.id is None def test_make_recipe(self): person = baker.make_recipe("tests.generic.person") assert isinstance(person, Person) assert person.id def test_make_recipe_with_quantity_parameter(self): people = baker.make_recipe("tests.generic.person", _quantity=3) assert len(people) == 3 for person in people: assert isinstance(person, Person) assert person.id def test_make_extended_recipe(self): extended_dog = baker.make_recipe("tests.generic.extended_dog") assert extended_dog.breed == "Super basset" # No side effects happened due to a recipe extension base_dog = baker.make_recipe("tests.generic.dog") assert base_dog.breed == "Pug" def test_extended_recipe_type(self): assert isinstance(pug, SmallDogRecipe) def test_save_related_instances_on_prepare_recipe(self): dog = baker.prepare_recipe("tests.generic.homeless_dog") assert not dog.id assert not dog.owner_id dog = baker.prepare_recipe("tests.generic.homeless_dog", _save_related=True) assert not dog.id assert dog.owner.id def test_make_recipe_with_quantity_parameter_respecting_model_args(self): people = baker.make_recipe( "tests.generic.person", _quantity=3, name="Dennis Ritchie", age=70 ) assert len(people) == 3 for person in people: assert person.name == "Dennis Ritchie" assert person.age == 70 def test_make_recipe_raises_correct_exception_if_invalid_quantity(self): with pytest.raises(InvalidQuantityException): baker.make_recipe("tests.generic.person", _quantity="hi") with pytest.raises(InvalidQuantityException): baker.make_recipe("tests.generic.person", _quantity=-1) def test_prepare_recipe_with_foreign_key(self): person_recipe = Recipe(Person, name="John Doe") dog_recipe = Recipe( Dog, owner=foreign_key(person_recipe), ) dog = dog_recipe.prepare() assert dog.id is None assert dog.owner.id is None def test_prepare_recipe_with_quantity_parameter(self): people = baker.prepare_recipe("tests.generic.person", _quantity=3) assert len(people) == 3 for person in people: assert isinstance(person, Person) assert person.id is None def test_prepare_recipe_with_quantity_parameter_respecting_model_args(self): people = baker.prepare_recipe( "tests.generic.person", _quantity=3, name="Dennis Ritchie", age=70 ) assert len(people) == 3 for person in people: assert person.name == "Dennis Ritchie" assert person.age == 70 def test_prepare_recipe_raises_correct_exception_if_invalid_quantity(self): with pytest.raises(InvalidQuantityException): baker.prepare_recipe("tests.generic.person", _quantity="hi") with pytest.raises(InvalidQuantityException): baker.prepare_recipe("tests.generic.person", _quantity=-1) def test_prepare_recipe(self): person = baker.prepare_recipe("tests.generic.person") assert isinstance(person, Person) assert not person.id def test_make_recipe_with_args(self): person = baker.make_recipe( "tests.generic.person", name="Dennis Ritchie", age=70 ) assert person.name == "Dennis Ritchie" assert person.age == 70 def test_prepare_recipe_with_args(self): person = baker.prepare_recipe( "tests.generic.person", name="Dennis Ritchie", age=70 ) assert person.name == "Dennis Ritchie" assert person.age == 70 def test_import_recipe_inside_deeper_modules(self): recipe_name = "tests.generic.tests.sub_package.person" person = baker.prepare_recipe(recipe_name) assert person.name == "John Deeper" def test_pass_save_kwargs(self): owner = baker.make(Person) dog = baker.make_recipe( "tests.generic.overwritten_save", _save_kwargs={"owner": owner} ) assert owner == dog.owner def test_pass_save_kwargs_in_recipe_definition(self): dog = baker.make_recipe("tests.generic.with_save_kwargs") assert dog.breed == "updated_breed" def test_ip_fields_with_start(self): first, second = baker.make_recipe("tests.generic.ip_fields", _quantity=2) assert first.ipv4_field == "127.0.0.2" assert first.ipv6_field == "2001:12f8:0:28::4" assert second.ipv4_field == "127.0.0.4" assert second.ipv6_field == "2001:12f8:0:28::6" @pytest.mark.django_db class TestForeignKey: def test_foreign_key_method_returns_a_recipe_foreign_key_object(self): number_recipe = Recipe(DummyNumbersModel, float_field=1.6) obj = foreign_key(number_recipe) assert isinstance(obj, RecipeForeignKey) def test_not_accept_other_type(self): with pytest.raises(TypeError) as c: foreign_key(2) exception = c.value assert str(exception) == "Not a recipe" def test_load_from_other_module_recipe(self): dog = Recipe(Dog, owner=foreign_key("tests.generic.person")).make() assert dog.owner.name == "John Doe" def test_fail_load_invalid_recipe(self): with pytest.raises(AttributeError): foreign_key("tests.generic.nonexisting_recipe") def test_class_directly_with_string(self): with pytest.raises(TypeError): RecipeForeignKey("foo") def test_do_not_create_related_model(self): """It should not create another object when passing the object as argument.""" person = baker.make_recipe("tests.generic.person") assert Person.objects.count() == 1 baker.make_recipe("tests.generic.dog", owner=person) assert Person.objects.count() == 1 baker.prepare_recipe("tests.generic.dog", owner=person) assert Person.objects.count() == 1 def test_do_query_lookup_for_recipes_make_method(self): """It should not create another object when using query lookup syntax.""" dog = baker.make_recipe("tests.generic.dog", owner__name="James") assert Person.objects.count() == 1 assert dog.owner.name == "James" def test_do_query_lookup_for_recipes_prepare_method(self): """It should not create another object when using query lookup syntax.""" dog = baker.prepare_recipe("tests.generic.dog", owner__name="James") assert dog.owner.name == "James" def test_do_query_lookup_empty_recipes(self): """It should not create another object when using query lookup syntax.""" dog_recipe = Recipe(Dog) dog = dog_recipe.make(owner__name="James") assert Person.objects.count() == 1 assert dog.owner.name == "James" dog = dog_recipe.prepare(owner__name="Zezin") assert Person.objects.count() == 1 assert dog.owner.name == "Zezin" def test_related_models_recipes(self): lady = baker.make_recipe("tests.generic.dog_lady") assert lady.dog_set.count() == 2 assert lady.dog_set.all()[0].breed == "Pug" assert lady.dog_set.all()[1].breed == "Basset" def test_related_models_recipes_make_mutiple(self): ladies = baker.make_recipe("tests.generic.dog_lady", _quantity=2) assert ladies[0].dog_set.count() == 2 assert ladies[1].dog_set.count() == 2 def test_nullable_related(self): nullable = baker.make_recipe("tests.generic.nullable_related") assert nullable.dummynullfieldsmodel_set.count() == 1 def test_chained_related(self): movie = baker.make_recipe("tests.generic.movie_with_cast") assert movie.cast_members.count() == 2 def test_one_to_one_relationship(self): lonely_people = baker.make_recipe("tests.generic.lonely_person", _quantity=2) friend_ids = {x.only_friend.id for x in lonely_people} assert len(friend_ids) == 2 @pytest.mark.django_db class TestM2MField: def test_create_many_to_many(self): dog = baker.make_recipe("tests.generic.dog_with_friends") assert len(dog.friends_with.all()) == 2 for friend in dog.friends_with.all(): assert friend.breed == "Pug" assert friend.owner.name == "John Doe" def test_create_nested(self): dog = baker.make_recipe("tests.generic.dog_with_more_friends") assert len(dog.friends_with.all()) == 1 friend = dog.friends_with.all()[0] assert len(friend.friends_with.all()) == 2 @pytest.mark.django_db class TestSequences: def test_increment_for_strings(self): person = baker.make_recipe("tests.generic.serial_person") assert person.name == "joe1" person = baker.prepare_recipe("tests.generic.serial_person") assert person.name == "joe2" person = baker.make_recipe("tests.generic.serial_person") assert person.name == "joe3" def test_increment_for_strings_with_suffix(self): fred_person = person_recipe.extend(email=seq("fred", suffix="@example.com")) person = fred_person.make() assert person.email == "fred1@example.com" person = fred_person.make() assert person.email == "fred2@example.com" person = fred_person.make() assert person.email == "fred3@example.com" def test_increment_for_fks(self): profiles = baker.make(Profile, _quantity=3) start_id = profiles[0].id seq_user = user_recipe.extend(username="name", profile_id=seq(start_id)) user = seq_user.make() assert user.profile_id == start_id + 1 user = seq_user.make() assert user.profile_id == start_id + 2 def test_increment_for_one_to_one(self): people = baker.make(Person, _quantity=3) start_id = people[0].id seq_lonely_person = lonely_person_recipe.extend(only_friend_id=seq(start_id)) person = seq_lonely_person.make() assert person.only_friend_id == start_id + 1 user = seq_lonely_person.make() assert user.only_friend_id == start_id + 2 def test_increment_for_strings_with_bad_suffix(self): bob_person = person_recipe.extend(email=seq("bob", suffix=42)) with pytest.raises(TypeError) as exc: bob_person.make() assert str(exc.value) == "Sequences suffix can only be a string" def test_increment_for_strings_with_suffix_and_start(self): fred_person = person_recipe.extend( email=seq("fred", start=5, suffix="@example.com") ) person = fred_person.make() assert person.email == "fred5@example.com" person = fred_person.make() assert person.email == "fred6@example.com" person = fred_person.make() assert person.email == "fred7@example.com" def test_increment_for_numbers(self): dummy = baker.make_recipe("tests.generic.serial_numbers") assert dummy.default_int_field == 11 assert dummy.default_decimal_field == Decimal("21.1") assert dummy.default_float_field == 2.23 dummy = baker.make_recipe("tests.generic.serial_numbers") assert dummy.default_int_field == 12 assert dummy.default_decimal_field == Decimal("22.1") assert dummy.default_float_field == 3.23 dummy = baker.prepare_recipe("tests.generic.serial_numbers") assert dummy.default_int_field == 13 assert dummy.default_decimal_field == Decimal("23.1") assert dummy.default_float_field == 4.23 def test_increment_for_numbers_2(self): """Repeated test to ensure Sequences atomicity.""" dummy = baker.make_recipe("tests.generic.serial_numbers") assert dummy.default_int_field == 11 assert dummy.default_decimal_field == Decimal("21.1") assert dummy.default_float_field == 2.23 dummy = baker.make_recipe("tests.generic.serial_numbers") assert dummy.default_int_field == 12 assert dummy.default_decimal_field == Decimal("22.1") assert dummy.default_float_field == 3.23 dummy = baker.prepare_recipe("tests.generic.serial_numbers") assert dummy.default_int_field == 13 assert dummy.default_decimal_field == Decimal("23.1") assert dummy.default_float_field == 4.23 def test_increment_for_numbers_with_suffix(self): with pytest.raises(TypeError) as exc: baker.make_recipe( "tests.generic.serial_numbers", default_int_field=seq(1, suffix="will not work"), ) assert ( str(exc.value) == "Sequences with suffix can only be used with text values" ) def test_creates_unique_field_recipe_using_for_iterator(self): for i in range(1, 4): dummy = baker.make_recipe("tests.generic.dummy_unique_field") assert dummy.value == 10 + i def test_creates_unique_field_recipe_using_quantity_argument(self): dummies = baker.make_recipe("tests.generic.dummy_unique_field", _quantity=3) assert dummies[0].value == 11 assert dummies[1].value == 12 assert dummies[2].value == 13 def test_increment_by_3(self): dummy = baker.make_recipe("tests.generic.serial_numbers_by") assert dummy.default_int_field == 13 assert dummy.default_decimal_field == Decimal("22.5") assert dummy.default_float_field == pytest.approx(3.030000) dummy = baker.make_recipe("tests.generic.serial_numbers_by") assert dummy.default_int_field == 16 assert dummy.default_decimal_field == Decimal("24.9") assert dummy.default_float_field == pytest.approx(4.83) dummy = baker.prepare_recipe("tests.generic.serial_numbers_by") assert dummy.default_int_field == 19 assert dummy.default_decimal_field == Decimal("27.3") assert dummy.default_float_field == pytest.approx(6.63) def test_increment_by_timedelta(self): dummy = baker.make_recipe("tests.generic.serial_datetime") assert dummy.default_date_field == (TEST_TIME.date() + timedelta(days=1)) assert dummy.default_date_time_field == tz_aware(TEST_TIME + timedelta(hours=3)) assert dummy.default_time_field == (TEST_TIME + timedelta(seconds=15)).time() dummy = baker.make_recipe("tests.generic.serial_datetime") assert dummy.default_date_field == (TEST_TIME.date() + timedelta(days=2)) assert dummy.default_date_time_field == tz_aware(TEST_TIME + timedelta(hours=6)) assert dummy.default_time_field == (TEST_TIME + timedelta(seconds=30)).time() def test_increment_by_timedelta_seq_combined_with_quantity(self): quantity = 5 entries = baker.make_recipe("tests.generic.serial_datetime", _quantity=quantity) for i, e in enumerate(entries): index = i + 1 assert e.default_date_field == ( TEST_TIME.date() + timedelta(days=1 * index) ) assert e.default_date_time_field == tz_aware( TEST_TIME + timedelta(hours=3 * index) ) assert ( e.default_time_field == (TEST_TIME + timedelta(seconds=15 * index)).time() ) def test_creates_unique_timedelta_recipe_using_quantity_argument(self): dummies = baker.make_recipe("tests.generic.serial_datetime", _quantity=3) assert dummies[0].default_date_field == TEST_TIME.date() + timedelta(days=1) assert dummies[1].default_date_field == TEST_TIME.date() + timedelta(days=2) assert dummies[2].default_date_field == TEST_TIME.date() + timedelta(days=3) def test_increment_after_override_definition_field(self): person = baker.make_recipe("tests.generic.serial_person", name="tom") assert person.name == "tom" person = baker.make_recipe("tests.generic.serial_person") assert person.name == "joe4" person = baker.prepare_recipe("tests.generic.serial_person") assert person.name == "joe5" @pytest.mark.django_db class TestIterators: def test_accepts_generators(self): r = Recipe(DummyBlankFieldsModel, blank_char_field=itertools.cycle(["a", "b"])) assert r.make().blank_char_field == "a" assert r.make().blank_char_field == "b" assert r.make().blank_char_field == "a" def test_accepts_iterators(self): r = Recipe(DummyBlankFieldsModel, blank_char_field=iter(["a", "b", "c"])) assert r.make().blank_char_field == "a" assert r.make().blank_char_field == "b" assert r.make().blank_char_field == "c" def test_empty_iterator_exception(self): r = Recipe(DummyBlankFieldsModel, blank_char_field=iter(["a", "b"])) assert r.make().blank_char_field == "a" assert r.make().blank_char_field == "b" with pytest.raises(RecipeIteratorEmpty): r.make() def test_only_iterators_not_iteratables_are_iterated(self): """Ensure we only iterate explicit iterators. Consider "iterable" vs "iterator": Something like a string is "iterable", but not an "iterator". We don't want to iterate "iterables", only explicit "iterators". """ r = Recipe( DummyBlankFieldsModel, blank_text_field="not an iterator, so don't iterate!" ) assert r.make().blank_text_field == "not an iterator, so don't iterate!" class TestAutoNowFields: @pytest.mark.django_db def test_make_with_auto_now_using_datetime_generator(self): delta = timedelta(minutes=1) def gen(): idx = 0 while True: idx += 1 yield tz_aware(TEST_TIME) + idx * delta r = Recipe( ModelWithAutoNowFields, created=gen(), ) assert r.make().created == tz_aware(TEST_TIME + 1 * delta) assert r.make().created == tz_aware(TEST_TIME + 2 * delta) @pytest.mark.django_db def test_make_with_auto_now_using_datetime_seq(self): delta = timedelta(minutes=1) r = Recipe( ModelWithAutoNowFields, created=seq( tz_aware(TEST_TIME), increment_by=delta, ), ) assert r.make().created == tz_aware(TEST_TIME + 1 * delta) assert r.make().created == tz_aware(TEST_TIME + 2 * delta) model_bakery-1.20.4/tests/test_utils.py000066400000000000000000000151501475767230300201660ustar00rootroot00000000000000import datetime from decimal import Decimal from inspect import getmodule import pytest from model_bakery.utils import get_calling_module, import_from_str, seq from tests.generic.models import User def test_import_from_str(): with pytest.raises(AttributeError): import_from_str("tests.generic.UndefinedObject") with pytest.raises(ImportError): import_from_str("tests.generic.undefined_path.User") assert import_from_str("tests.generic.models.User") == User assert import_from_str(User) == User assert import_from_str("generic.User") == User def test_get_calling_module(): # Reference to this very module this_module = getmodule(test_get_calling_module) # Once removed is the `pytest` module calling this function pytest_module = get_calling_module(1) assert pytest_module != this_module assert "pytest" in pytest_module.__name__ # Test functions def dummy_secondary_method(): return get_calling_module(2), get_calling_module(3) def dummy_method(): return (*dummy_secondary_method(), get_calling_module(1), get_calling_module(2)) # Unpack results from the function chain sec_mod, sec_pytest_mod, dummy_mod, pytest_mod = dummy_method() assert sec_mod == this_module assert "pytest" in sec_pytest_mod.__name__ assert dummy_mod == this_module assert "pytest" in pytest_mod.__name__ # Raise an `IndexError` when attempting to access too many frames removed with pytest.raises(IndexError): assert get_calling_module(100) class TestSeq: def test_string(self): sequence = seq("muffin") assert next(sequence) == "muffin1" assert next(sequence) == "muffin2" assert next(sequence) == "muffin3" def test_string_start(self): sequence = seq("muffin", start=9) assert next(sequence) == "muffin9" assert next(sequence) == "muffin10" assert next(sequence) == "muffin11" def test_string_suffix(self): sequence = seq("cookie", suffix="@example.com") assert next(sequence) == "cookie1@example.com" assert next(sequence) == "cookie2@example.com" assert next(sequence) == "cookie3@example.com" def test_string_suffix_and_start(self): sequence = seq("cookie", start=111, suffix="@example.com") assert next(sequence) == "cookie111@example.com" assert next(sequence) == "cookie112@example.com" assert next(sequence) == "cookie113@example.com" def test_string_invalid_suffix(self): with pytest.raises(TypeError) as exc: next(seq("cookie", suffix=42)) assert str(exc.value) == "Sequences suffix can only be a string" def test_int(self): sequence = seq(1) assert next(sequence) == 2 assert next(sequence) == 3 assert next(sequence) == 4 def test_int_increment_by(self): sequence = seq(1, increment_by=3) assert next(sequence) == 4 assert next(sequence) == 7 assert next(sequence) == 10 def test_decimal(self): sequence = seq(Decimal("36.6")) assert next(sequence) == Decimal("37.6") assert next(sequence) == Decimal("38.6") assert next(sequence) == Decimal("39.6") def test_decimal_increment_by(self): sequence = seq(Decimal("36.6"), increment_by=Decimal("2.4")) assert next(sequence) == Decimal("39.0") assert next(sequence) == Decimal("41.4") assert next(sequence) == Decimal("43.8") def test_float(self): sequence = seq(1.23) assert next(sequence) == 2.23 assert next(sequence) == 3.23 assert next(sequence) == 4.23 def test_float_increment_by(self): sequence = seq(1.23, increment_by=1.8) assert next(sequence) == pytest.approx(3.03) assert next(sequence) == pytest.approx(4.83) assert next(sequence) == pytest.approx(6.63) def test_numbers_start_from_zero(self): sequence = seq(0, start=0) assert next(sequence) == 0 assert next(sequence) == 1 assert next(sequence) == 2 def test_numbers_with_suffix(self): with pytest.raises(TypeError) as exc: next(seq(1, suffix="iamnotanumber")) assert ( str(exc.value) == "Sequences with suffix can only be used with text values" ) def test_date(self): sequence = seq( datetime.date(2021, 2, 11), increment_by=datetime.timedelta(days=6) ) assert next(sequence) == datetime.date(2021, 2, 17) assert next(sequence) == datetime.date(2021, 2, 23) assert next(sequence) == datetime.date(2021, 3, 1) def test_time(self): sequence = seq( datetime.time(15, 39, 58, 457698), increment_by=datetime.timedelta(minutes=59), ) assert next(sequence) == datetime.time(16, 38, 58, 457698) assert next(sequence) == datetime.time(17, 37, 58, 457698) assert next(sequence) == datetime.time(18, 36, 58, 457698) @pytest.mark.parametrize("use_tz", [False, True]) def test_datetime(self, settings, use_tz): settings.USE_TZ = use_tz tzinfo = datetime.timezone.utc if use_tz else None # Starting with tz-unaware (naive) datetime sequence = seq( datetime.datetime(2021, 2, 11, 15, 39, 58, 457698), increment_by=datetime.timedelta(hours=3), ) assert next(sequence) == datetime.datetime( 2021, 2, 11, 18, 39, 58, 457698 ).replace(tzinfo=tzinfo) assert next(sequence) == datetime.datetime( 2021, 2, 11, 21, 39, 58, 457698 ).replace(tzinfo=tzinfo) assert next(sequence) == datetime.datetime( 2021, 2, 12, 00, 39, 58, 457698 ).replace(tzinfo=tzinfo) # Starting with tz-aware datetime sequence = seq( datetime.datetime(2021, 2, 11, 15, 39, 58, 457698, tzinfo=tzinfo), increment_by=datetime.timedelta(hours=3), ) assert next(sequence) == datetime.datetime( 2021, 2, 11, 18, 39, 58, 457698 ).replace(tzinfo=tzinfo) @pytest.mark.parametrize( "value", [ datetime.datetime(2021, 2, 11, 15, 39, 58, 457698), datetime.date(2021, 2, 11), datetime.time(15, 39, 58, 457698), ], ) def test_should_raise_exception_for_datetime_instances(self, value): with pytest.raises(TypeError) as exc: next(seq(value)) assert str(exc.value) == ( "Sequences with values datetime.datetime, datetime.date and datetime.time, " "incremente_by must be a datetime.timedelta." ) model_bakery-1.20.4/tests/uninstalled/000077500000000000000000000000001475767230300177355ustar00rootroot00000000000000model_bakery-1.20.4/tests/uninstalled/baker_recipes.py000066400000000000000000000005651475767230300231130ustar00rootroot00000000000000from django.utils.timezone import now from model_bakery.recipe import Recipe from tests.generic.models import Person person = Recipe( Person, name="Uninstalled", nickname="uninstalled", age=18, bio="Uninstalled", blog="http://uninstalled.com", days_since_last_login=4, birthday=now().date(), appointment=now(), birth_time=now(), ) model_bakery-1.20.4/tox.ini000066400000000000000000000022201475767230300155600ustar00rootroot00000000000000[tox] env_list = py{38,39}-django{42}-{postgresql,sqlite} py{310,311}-django{42,50,51}-{postgresql,sqlite} py{311,312}-django{42,50,51}-{postgresql-psycopg3} py312-django{50,51}-{postgresql-contenttypes} py313-django{50,51}-{sqlite,postgresql-psycopg3,postgresql-contenttypes} [testenv] package = wheel setenv = PYTHONPATH={toxinidir} postgresql: TEST_DB=postgis postgresql: PGUSER=postgres postgresql: PGPASSWORD=postgres postgresql-psycopg3: TEST_DB=postgis postgresql-psycopg3: PGUSER=postgres postgresql-psycopg3: PGPASSWORD=postgres postgresql-contenttypes: USE_CONTENTTYPES=True postgresql-contenttypes: TEST_DB=postgresql postgresql-contenttypes: PGUSER=postgres postgresql-contenttypes: PGPASSWORD=postgres sqlite: TEST_DB=sqlite sqlite: USE_TZ=True deps = coverage pillow pytest pytest-django django42: Django>=4.2,<5 django50: Django>=5.0,<5.1 django51: Django>=5.1,<5.2 postgresql: psycopg2-binary postgresql-psycopg3: psycopg postgresql-contenttypes: psycopg commands = python \ -m coverage run \ -m pytest {posargs:tests} model_bakery-1.20.4/utils/000077500000000000000000000000001475767230300154115ustar00rootroot00000000000000model_bakery-1.20.4/utils/from_mommy_to_bakery.py000066400000000000000000000062661475767230300222150ustar00rootroot00000000000000""" Migrate from model_mommy to model_bakery. ``python from_mommy_to_bakery.py --dry-run`` Please check your dependency files. """ import argparse import os import re PACKAGE_NAME = r"\bmodel_mommy\b" PACKAGE_NAME_PATTERN = re.compile(PACKAGE_NAME) PACKAGE_RECIPES = r"\bmommy_recipes\b" PACKAGE_RECIPES_PATTERN = re.compile(PACKAGE_RECIPES) PACKAGE_LEGACY_MODULE = r"\bmommy\b" PACKAGE_LEGACY_MODULE_PATTERN = re.compile(PACKAGE_LEGACY_MODULE) LEGACY_AND_NEW = [ {"old": PACKAGE_NAME_PATTERN, "new": r"model_bakery"}, {"old": PACKAGE_RECIPES_PATTERN, "new": r"baker_recipes"}, {"old": PACKAGE_LEGACY_MODULE_PATTERN, "new": r"baker"}, ] EXCLUDE = [ "node_modules", "venv", ".git", "sql", "docs", "from_mommy_to_bakery.py", "Pipfile", "Pipfile.lock", ] def _find_changes(content, pattern, new_value): new_content, substitutions = re.subn(pattern, new_value, content) return new_content, substitutions > 0 def _rename_recipe_file(to_be_renamed, dry_run): if dry_run is True: print("Will be renamed:") for old_recipe_file in to_be_renamed: root = old_recipe_file[: old_recipe_file.rfind("/")] if dry_run is False: os.rename(old_recipe_file, f"{root}/baker_recipes.py") else: print(old_recipe_file) def _replace_legacy_terms(file_path, dry_run): with open(file_path, encoding="utf-8") as f: content = f.read() changed = [] for patterns in LEGACY_AND_NEW: old, new = patterns["old"], patterns["new"] content, has_changed = _find_changes(content, old, new) changed.append(has_changed) if any(changed): if dry_run is False: with open(file_path, "w", encoding="utf-8") as f: f.write(content) else: print(file_path) def _sanitize_folder_or_file(folder_or_file): folder_or_file = folder_or_file.strip() if folder_or_file.endswith("/"): # Remove trailing slash e.g.: '.tox/' -> '.tox' folder_or_file = folder_or_file[:-1] return folder_or_file def check_files(dry_run): with open(".gitignore") as f: excluded_by_gitignore = [ _sanitize_folder_or_file(folder_or_file) for folder_or_file in f.readlines() ] exclude = EXCLUDE[:] exclude.extend(excluded_by_gitignore) to_be_renamed = [] for root, dirs, files in os.walk(".", topdown=True): dirs[:] = [directory for directory in dirs if directory not in exclude] for file_ in files: if file_ in exclude or not file_.endswith(".py"): continue file_path = f"{root}/{file_}" if file_ == "mommy_recipes.py": to_be_renamed.append(file_path) _replace_legacy_terms(file_path, dry_run) _rename_recipe_file(to_be_renamed, dry_run) if __name__ == "__main__": description = "Help you to migrate from model_mommy to model_bakery." parser = argparse.ArgumentParser(description=description) parser.add_argument( "--dry-run", dest="dry_run", action="store_true", help="See which files will be changed.", ) args = parser.parse_args() check_files(args.dry_run)